Multi-webpart projects
One RSPFx project can ship multiple web parts, extensions, and libraries in a single .sppkg. See Microsoft docs: Working with web part manifests.
rspfx new scaffolds the first web part; add more by duplicating the folder and manifest.
Scaffold first web part
rspfx new my-app --framework react --spfx-version 1.23 --yesCreates src/webparts/my-app/ with my-app.manifest.json and my-appWebPart.ts.
Add a second web part
Copy src/webparts/my-app/ → src/webparts/todo/ and rename three things:
Folder
src/webparts/todo/— bundle name is the folder name.Inside
todo.manifest.json— newid(node -e "console.log(crypto.randomUUID())"),alias: TodoWebPart,preconfiguredEntries[0].title: Todo.Inside
todoWebPart.ts+components/Todo.tsx— rename classTodoWebPart, importTodo, stylesstyles.Todo.
config/config.json bundles and config/package-solution.json features are not needed per web part — auto-discovery scans src/webparts/* when bundles is absent.
If config.json has explicit bundles, add a new bundle todo with its entrypoint + manifest.
Tip: Delete
config.jsonto use folder-scan mode (simplest for multi-webpart) — or keep it and add onebundles.todentry per web part.
Third, fourth, …
Repeat — each src/webparts/<folder>/ needs one *.manifest.json + one entrypoint (<Name>WebPart.ts, index.ts, or framework variant).
Keep teams/ and sharepoint/assets as project singletons.
Tip: Generate each manifest
idwithcrypto.randomUUID()— never copy the first web part'sid.
Build, dev, and package
rspfx dev— discovers all web parts and serveshttps://localhost:4321/dist/<bundle>.jsper folder;/temp/manifests.jsconcatenates everyid; local preview athttp://localhost:4321/renders a card per web part.rspfx package— one.sppkgwith every component:release/manifests/<id>.manifest.jsonper id andClientSideAssets/<bundle>.jsper bundle.
Verify:
unzip -l sharepoint/solution/<name>.sppkg
# expect: <featureId>/WebPart_<firstId>.xml, <featureId>/WebPart_<secondId>.xml, ClientSideAssets/todo.js, etc.Install once — upload single .sppkg → Deploy → each web part appears separately in the Add to page picker by its preconfiguredEntries.title.
Tip: Use
rspfx analyzeto see per-bundle sizes before packaging.
Extensions alongside web parts
rspfx new --component applicationcustomizer # or fieldcustomizer | listviewcommandset | formcustomizerExtensions live in src/extensions/ — a project can mix src/webparts/* + src/extensions/*.
Package embeds them as <featureId>/Extension_<id>.xml.
See real-tenant-validation.md.
Libraries alongside web parts
rspfx new --component libraryLibraries live in src/libraries/ — mix src/webparts/* + src/extensions/* + src/libraries/*.
Package embeds them as <featureId>/Library_<id>.xml (Type="Library").
The local preview lists libraries as non-mountable; window.__RSPFX_COMPONENTS__ still exposes them for import('<alias>').
Favicons and assets per web part
- Each web part folder has
assets/.gitkeep. - Project root
assets/favicon.svgis served at/assets/favicon.svgand shown in the local preview.
Per-webpart icons use the web part assets/ folder; shared branding uses assets/ or sharepoint/assets/.
Comparison vs official
| Area | Official | RSPFx |
|---|---|---|
| Discovery | config.json bundles only | bundles or folder scan (src/webparts/*) |
| Adding a web part | yo @microsoft/sharepoint or manual config.json | Duplicate folder + new id — no generator needed |
| Packaging | One .sppkg with all bundles | Same — WebPart_<id>.xml + Extension_<id>.xml + Library_<id>.xml |
| Dev server | All bundles on :4321 | Same — https://localhost:4321/dist/<bundle>.js |
Troubleshooting
| Symptom | Fix |
|---|---|
| Second web part not found | Folder missing *.manifest.json or entrypoint — both required |
Duplicate id error | Manifest id must be unique — crypto.randomUUID() |
404 for dist/second.js in workbench (https://localhost:4321/dist/...) | Bundle name vs entryModuleId mismatch — config.json key vs folder vs emitted file must agree |
Only first web part in .sppkg | config.json bundles is authoritative when present — add new entry or delete config.json for folder scan |