Purpose and the golden rule
Moving a web part means copying its source folder into another SPFx solution, registering it in that solution's config files, removing it from the original, and redeploying both packages. The web part's component GUID never changes; everything else about the package can.
Every page instance of a web part stores the component id from its *.manifest.json. Keep that GUID and existing pages pick the web part up from the new package. Change it and every page shows an error.
Identifier | Where it lives | When moving a web part |
|---|
Component id and alias | src/webparts/<name>/<Name>WebPart.manifest.json
| Keep unchanged |
Solution id | config/package-solution.json → solution.id
| Target package gets its own GUID; source keeps its GUID so the app catalog treats the upload as an upgrade |
Feature id | config/package-solution.json → features[].id (if present)
| Target package gets its own GUID |
Library id | .yo-rc.json → libraryId
| Target package gets its own GUID |
Bundle file name | config/config.json → outputPath (1.1.x) or bundle key (1.4+)
| Keep the same name so the CDN path stays predictable |
This guide is written for SPFx 1.1.x (Node 10, gulp 3, config.json schema 1.0). Where current SPFx differs, the difference is called out inline.
Before you start
Freeze the build environment and inventory the web part before copying anything. Both solutions must be on the same SPFx version; do not move a 1.1.x web part into a solution scaffolded by a newer generator.
Confirm gulp bundle --ship passes on the source solution as it stands today (Node 10.24.1, npm 6.14.12, gulp 3.9.1)
Commit package-lock.json; snapshot the working machine (VM image or Docker) so node_modules can be reproduced
Create a branch (or a copy of the folder) for the source solution
Install a GUID generator: PowerShell [guid]::NewGuid() or uuidgen on macOS/Linux
Record these facts for the web part being moved:
Item | Where to find it | Example |
|---|
Component GUID | src/webparts/<name>/<Name>WebPart.manifest.json → id
| 976f874b-e076-45d2-8dbc-b9e871da0e7e
|
Alias | same file → alias | NewsFeedWebPart
|
Source folder | src/webparts/<name>/
| src/webparts/newsFeed/
|
config.json entry | config/config.json → entries[] item whose manifest points at the folder
| outputPath: ./dist/news-feed.bundle.js
|
Localized resources key | config/config.json → localizedResources
| NewsFeedWebPartStrings
|
Imports outside its folder | grep -rn "from '\.\./\.\./" src/webparts/<name>/
| ../../common/services/ListService
|
Feature assets | sharepoint/assets/elements.xml entries that name the component GUID
| list instances, ClientSideComponentId |
External libraries | config/config.json → externals used by the web part
| jquery, moment
|
CDN location | config/write-manifests.json → cdnBasePath; config/deploy-azure-storage.json → container
| https://<account>.blob.core.windows.net/intranet/
|
Sites using it | App catalog / Site contents of each site where the source app is installed | needed for the cutover in Step 8 |
Step 1 — Prepare the target solution
On SPFx 1.1.x, clone the source solution to create the target; do not run yo @microsoft/sharepoint, because the installed generator (1.4.1) scaffolds a 1.4.x project with a different config.json schema and newer packages.
Approach | When | Risk |
|---|
Clone the source folder (recommended on 1.1.x) | Target must stay on the same SPFx version | None: same node_modules, same gulp tasks, same build |
Scaffold with Yeoman | Only when the target is deliberately on a newer SPFx version (a future upgrade) | The web part code must be ported, not just moved |
To clone:
Copy the whole source folder (including node_modules) to a new folder, e.g. intranet-portal-hr/.
Delete every src/webparts/<name>/ folder that will not live in the target.
Edit the identity files so the app catalog sees a different solution:
File | Field | Change to |
|---|
package.json
| name
| new name, e.g. intranet-portal-hr |
config/package-solution.json
| solution.name
| new display name shown in the app catalog |
config/package-solution.json
| solution.id
| new GUID |
config/package-solution.json
| solution.version
| 1.0.0.0
|
config/package-solution.json
| paths.zippedPackage
| solution/intranet-portal-hr.sppkg
|
config/package-solution.json
| features[].id (only if a features array exists)
| new GUID per feature |
.yo-rc.json
| libraryName, libraryId
| new name, new GUID |
config/deploy-azure-storage.json
| container
| a new container or sub-folder, e.g. intranet-portal-hr |
config/write-manifests.json
| cdnBasePath
| the matching CDN URL for that container |
config/config.json
| entries[], localizedResources
| remove every entry for web parts that were deleted in item 2 |
sharepoint/assets/elements.xml
| elements for deleted web parts | remove |
Run gulp clean then gulp build in the target. It should compile with only the web parts you kept. Fix any import that now points at a deleted folder before going on.
Step 2 — Copy the web part source files
Copy the whole src/webparts/<name>/ folder from source to target, keeping the folder name, file names and manifest exactly as they are.
Copy src/webparts/<name>/ into the target's src/webparts/. This brings the <Name>WebPart.ts, <Name>WebPart.manifest.json, .module.scss, components/, loc/ and assets/ sub-folders.
Open the copied <Name>WebPart.manifest.json and confirm id and alias are unchanged from the values recorded in "Before you start".
Copy any test files for the web part (src/webparts/<name>/tests/ or src/webparts/<name>/*.test.ts).
Do not copy lib/, dist/, temp/ or sharepoint/solution/; they are build output and are regenerated.
If the web part imports files outside its own folder, note them now; Step 5 deals with them.
Step 3 — Register it in config/config.json
The build only compiles what config.json lists, so add the web part's entry and its string resources to the target, and later remove them from the source. SPFx 1.1.x uses the schema 1.0 entries array; SPFx 1.2 and later use the schema 2.0 bundles object (gulp --upgrade converts one to the other during an SPFx upgrade).
SPFx 1.1.x (your project):
{
"entries": [
{
"entry": "./lib/webparts/newsFeed/NewsFeedWebPart.js",
"manifest": "./src/webparts/newsFeed/NewsFeedWebPart.manifest.json",
"outputPath": "./dist/news-feed.bundle.js"
}
],
"externals": {},
"localizedResources": {
"NewsFeedWebPartStrings": "webparts/newsFeed/loc/{locale}.js"
}
}
SPFx 1.4 and later, for reference:
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"news-feed-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/newsFeed/NewsFeedWebPart.js",
"manifest": "./src/webparts/newsFeed/NewsFeedWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"NewsFeedWebPartStrings": "lib/webparts/newsFeed/loc/{locale}.js"
}
}
Checks while editing:
Check | Why |
|---|
entry points at ./lib/... (compiled output), manifest at ./src/...
| Paths are relative to the solution root; a wrong one fails silently as an empty bundle |
outputPath file name is unique within the target
| Two entries writing the same .bundle.js overwrite each other |
localizedResources key equals the module name in import * as strings from 'NewsFeedWebPartStrings'
| Otherwise the build reports "Cannot find module" |
Every externals entry the web part uses is copied too | External scripts are resolved per solution, not per web part |
Cut the same entry and resources key from the source config.json in Step 7 | Leaving them in the source rebuilds the web part there and recreates the duplicate-component conflict at deployment |
Step 4 — package-solution.json, feature assets and versions
If the target's package-solution.json has no features array, nothing else is needed: the packager creates one feature per component automatically. If it does have features, the moved web part's GUID must be listed in the target and removed from the source.
Situation | Target solution | Source solution |
|---|
No features property (typical 1.1.x project) | Nothing to add | Nothing to remove |
features[].componentIds present
| Add the component GUID | Remove it |
features[].assets.elementManifests references an elements.xml with entries for this web part (list instances, fields, ClientSideComponentId)
| Copy those elements into the target's sharepoint/assets/elements.xml and reference the file in the target feature | Remove the elements |
webApiPermissionRequests (SPFx 1.4.1 and later only)
| Add the scopes this web part needs | Remove scopes nothing else uses |
Versions to set:
File | Field | Target | Source |
|---|
config/package-solution.json
| solution.version
| 1.0.0.0 (first release of the new package)
| Increment, e.g. 1.4.0.0 → 2.0.0.0; same solution.id, so the catalog treats it as an upgrade |
config/package-solution.json
| features[].version
| 1.0.0.0
| Increment if a features array exists |
package.json
| version
| 1.0.0
| Increment; documentation only |
An elements.xml that provisions a list for the moved web part needs a new feature GUID in the target. Provisioned lists already on sites are not recreated or removed by the move; only new installs run the element manifests.
Step 5 — Shared code and imports
Any file the web part imports from outside its own folder must exist in the target, or the build fails. On SPFx 1.1.x there are no library components (SPFx 1.9+), so sharing happens at source level.
Find the dependencies:
grep -rn "from '\.\./\.\./" src/webparts/newsFeed/
grep -rn "require(" src/webparts/newsFeed/
Option | How | Use when |
|---|
Copy the shared files into the target (src/common/ in both solutions) | Same relative paths, so imports need no edits | Default for a stop-gap split; accept that fixes must be applied in each solution |
Move the shared code to a local npm package referenced with "file:../intranet-common" | One copy, each solution bundles its own build of it | Shared layer is large or changes often; needs a tsc build step for the common package |
Rewrite the web part to drop the dependency | Inline the one or two helpers it used | The shared code is tiny |
Also check:
tsconfig.json — paths, typeRoots and any include globs that cover shared folders
typings/ or src/typings/ — global .d.ts files (e.g. for require() of images or externals)
config/config.json → externals — copy entries the web part depends on
package.json dependencies the web part imports (moment, @pnp/sp, etc.) — present in the clone already; prune ones the target no longer needs later
Images and fonts referenced by require('./assets/x.png') or SCSS url() — they travel with the folder, but confirm the paths resolve
Step 6 — Build and test in the target
Build the target end to end before touching the source, so a failure here costs nothing in production.
gulp clean — removes lib/, dist/, temp/ left over from the clone.
gulp build — TypeScript and SCSS compile; fix import errors from Step 5.
gulp serve — opens the local workbench at https://localhost:4321/temp/workbench.html; add the web part, open the property pane and confirm strings and icons load.
gulp serve --nobrowser, then open https://<tenant>.sharepoint.com/sites/<dev-site>/_layouts/workbench.aspx — tests the web part against real SharePoint data while still served from localhost.
gulp bundle --ship — release bundle; check that temp/deploy/ contains <outputPath>.js and the *.manifest.json for the moved web part.
gulp deploy-azure-storage (SPFx 1.1.x hosts bundles outside the package) — uploads temp/deploy/ to the container set in deploy-azure-storage.json. If you host on a SharePoint library or the Microsoft 365 CDN instead, upload the same files there and make sure write-manifests.json → cdnBasePath is the folder URL. SPFx 1.4+ projects with includeClientSideAssets: true skip this step.
gulp package-solution --ship — produces sharepoint/solution/<name>.sppkg.
Upload the .sppkg to a dev or test tenant app catalog (or a site-collection app catalog on a test site), add the app to a site, add the web part to a page and verify.
Stop here if the browser console shows Failed to load component "<GUID>": it means the bundle URL built from cdnBasePath + bundle name is wrong or not uploaded.
Step 7 — Remove it from the source solution
The source must stop shipping the web part, otherwise the app catalog will reject the target package because the component GUID is already registered to the source solution.
Delete src/webparts/<name>/.
In config/config.json, remove the entries[] item and the localizedResources key for it.
In config/package-solution.json, remove its GUID from features[].componentIds (if that array exists) and remove its elements from sharepoint/assets/elements.xml.
Search for anything that still imports from the deleted folder: grep -rn "webparts/<name>" src/ config/. Another web part importing from this one is a sign the code should have gone to src/common/ in Step 5.
Increment solution.version in package-solution.json (keep solution.id).
gulp clean, gulp build, gulp bundle --ship, gulp deploy-azure-storage, gulp package-solution --ship.
Confirm the old bundle file is no longer in temp/deploy/. Leave the old copy on the CDN until the cutover is verified; delete it afterwards.
Step 8 — Deploy order and cutover
The app catalog allows a component GUID in only one solution at a time, so the trimmed source must go up before the target, and pages using the web part show an error between the two uploads. Plan a short maintenance window and run the sequence on a test tenant first.
SPFx 1.1.x has no tenant-wide deployment (that arrived in 1.4), so the new app must also be installed on every site that uses the web part; the sites list from "Before you start" drives that step.
Upload the CDN assets for both packages (done in Steps 6 and 7). Nothing changes for users yet.
Upload the source .sppkg (new version, web part removed) to the app catalog and select Deploy. From this point pages using the web part show "Something went wrong" until step 5 completes.
Upload the target .sppkg and select Deploy. If step 2 was skipped, this upload fails with Component ID {0} exists in solution {1} already.
On each site in the list: Site contents → New → App → add the target app. With PnP PowerShell: Install-PnPApp -Identity <target app id> per site.
On the same sites, upgrade the source app if SharePoint prompts (Site contents → ... → About → Get it) so the removed web part leaves the toolbox.
Open one page per moved web part; it renders from the new package with its existing properties intact because the component GUID is unchanged.
After a day of clean operation, delete the old bundle files from the source CDN folder.
With SPFx 1.4 or later and skipFeatureDeployment: true, step 4 becomes the "Make this solution available to all sites" checkbox at upload time.
Verification checklist
Manifest id and alias in the target match the values recorded before the move
gulp bundle --ship passes in both solutions with no warnings about missing modules
temp/deploy/ of the target contains the bundle and manifest; temp/deploy/ of the source does not
Bundle URL (cdnBasePath + file name) returns HTTP 200 in a browser
Target solution.id, features[].id and .yo-rc.json libraryId are new GUIDs; source solution.id is unchanged
Source solution.version is higher than the version currently in the app catalog
Both .sppkg files deploy to the test app catalog in the order source → target without error
Web part renders on an existing page with its saved properties, not as a blank or re-configured instance
Web part appears in the toolbox once per site (not twice, not missing)
Property pane strings display for every supported locale
Lists or fields provisioned by elements.xml are intact on existing sites
The moved web part no longer appears when searching the source solution's src/ and config/ for its folder name
Troubleshooting
Symptom | Cause | Fix |
|---|
App catalog: Component ID {0} exists in solution {1} already | The source solution still ships the web part, or its new version was not uploaded first | Finish Step 7, upload the source first, then the target |
App catalog: A solution with the same product ID already exists | Target solution.id was not changed when cloning | Set a new GUID in package-solution.json and .yo-rc.json, repackage |
App catalog: component id (0) exists more than once in current package | Stale files in dist/, temp/ or sharepoint/solution/ after deleting a web part | gulp clean, then bundle and package again
|
Page: Failed to load component "<GUID>" after deploying | Bundle not uploaded to the CDN, or cdnBasePath points elsewhere | Open the URL cdnBasePath + bundle file name; upload or correct and repackage |
Page: web part still broken after both packages deployed | Target app not installed on that site (no tenant-wide deployment on 1.1.x) | Add the app from Site contents, or Install-PnPApp |
Page: web part renders but properties are reset | Manifest id was changed; SharePoint treats it as a different web part | Restore the original GUID, repackage, redeploy |
Web part appears twice in the toolbox | Both old and new versions installed on the site | Upgrade the source app on the site so the removed web part drops out |
Build: Cannot find module 'NewsFeedWebPartStrings' | localizedResources key missing or misspelled in the target config.json
| Add the key exactly as imported in the web part |
Build: Cannot find module '../../common/...' | Shared code not copied | Step 5 |
Build: TypeScript errors in files you did not touch | Target tsconfig.json or typings/ differ from the source | Diff both against the source and align |
Property pane shows resource keys instead of text | loc/ folder not copied, or {locale}.js path wrong in localizedResources
| Copy loc/, fix the path |
Sources
Join the conversation! Your thoughts help the community grow.