Backoffice: Unified manifests boot (AB#63380)#22903
Conversation
…983) Set experimentalMinChunkSize=10_000 as the default in the shared Vite helper. Every workspace inherits the coalescing automatically; the threshold can still be overridden per workspace (pass 0 to disable). Impact on dist-cms output: - packages/core .js files: 981 -> 272 (-72%) - All workspaces combined .js files: 2194 -> 1401 (-36%) - Welcome dashboard .js requests: 510 -> 497 (-2.5%) - packages/ufm requests in particular: 23 -> 12 (-48%) - Gzipped bundle total: -1.2% - Raw bytes: +1.4% (small overhead from merged chunks; gzip wins it back) All entry chunks are preserved, so every public @umbraco-cms/backoffice/<sub> import keeps resolving without changes to package.json exports or tsconfig paths. Further consolidation (collapsing core's per-subpath entries into a single bundle with stubs) was prototyped but hits a TDZ cycle between the eager entry and its dynamic-import descendants. Tracked for v18, not part of this change.
Aligns the two outliers with the conventions used by the other 38
first-party packages:
- documents/umbraco-package.ts now uses the lazy bundle pattern
(type: 'bundle', js: () => import('./manifests.js')) instead of
eagerly importing manifests at module evaluation. The bundle
initializer auto-loads the manifests at boot, so behaviour is
unchanged.
- umbraco-news/manifests.ts now exports `manifests: Array<...>`
instead of a bare `dashboard` object. The bundle initializer
enumerates exports regardless of name, so behaviour is unchanged.
Preparatory cleanup so future build-time manifest aggregation can
treat every workspace uniformly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the measured comparison between today's two-wave boot manifest layer (187 eager chunks, 267 KB gzip) and a Step 2 unified-merge target (36 eager chunks, 208 KB gzip), proposes the Vite plugin shape, the app.element.ts boot change, dev-server behaviour, the measurement gate via measure-ttfe.mjs, and the rollback path. Notes that the previously-assumed prerequisite codemod (#68481, by-value manifests imports → lazy) is no longer a blocker: trial measurements showed by-value classes are already in the eager set today, so merging is byte-neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five-task plan covering the Vite plugin (with TDD on the entry-source generator), build wiring, importmap exposure, app.element.ts switch, and end-to-end smoke + measurement. Each task is bite-sized with exact commands and code blocks for the implementing agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a build-time plugin that walks src/packages/*/manifests.ts and exposes a virtual:unified-manifests module aggregating every package's manifests export into one allManifests array. Includes HMR invalidation so dev mode picks up manifest edits. Pure entry-source generator covered by node --test.
Dedicated Vite config consumes the unified-manifests plugin and emits dist-cms/manifests-all/index.js. Slotted between build:workspaces and generate:manifest in build:for:cms so the importmap step picks up the new artifact.
Adds the new artifact to package.json exports so the importmap pipeline auto-includes it at @umbraco-cms/backoffice/manifests-all.
Drops the 37-entry CORE_PACKAGES dynamic-import array in favour of one import of @umbraco-cms/backoffice/manifests-all. The merged artifact contains every first-party package's manifests, eliminating the bundle-initializer second wave for first-party packages. Third-party umbraco-package.ts entries are unaffected — the bundle extension type is still supported for plugin authors.
- Plugin: track every discovered manifest path via this.addWatchFile so brand-new packages created during a dev session trigger HMR. - Plugin: document the alphabetical determinism on discoverPackages. - Build config: comment the preserveEntrySignatures rationale next to the lib: undefined comment. - app.element.ts: explicit .catch handler logging the underlying import failure before the generic 'Extensions failed loading' page swallows it. - Stub: move the import-type line above the file-header comment block. No behaviour change in the happy path; surfaces clearer diagnostics and catches a dev-mode HMR edge case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies simplify-skill review feedback:
- Plugin: collapse the dual `discovered` + `cachedSource` closure state into
a single nullable `cache` record so the invariant (entries↔source) is
obvious from one variable.
- Plugin: unexport `toIdentifier` and `discoverPackages` — they were never
consumed externally. The pure public surface is now `buildEntrySource`
and `unifiedManifestsPlugin` only.
- Plugin: compute each package's identifier once via a pre-mapped list
instead of twice across the two `.map` passes in `buildEntrySource`.
- Plugin: `discoverPackages` now uses `readdirSync({ withFileTypes: true })`
+ a single `statSync` on the manifest path, halving syscalls per
package and removing the existsSync/statSync TOCTOU pattern.
- Plugin: `handleHotUpdate` no longer re-walks the packages directory for
content edits to an already-known file — only rebuilds the entry source.
A full rediscover only fires when a new manifests.ts file appears. Also
short-circuits the virtual-module invalidation when the rebuilt source
is identical to the previous one (no-op saves don't cascade through
Vite's module graph).
- Vite config: drop the dead Array.isArray branch on baseOutput in favour
of a typed cast, since getDefaultConfig contractually returns a single
output object.
No behaviour change in the happy path; ~halves dev-server syscall load on
HMR and tightens the module's public surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the cache-rebuild branch into a pure helper nextCacheForChange so the hook body is straight-line code (early-return guard, rebuild, source diff, invalidation). CodeScene flagged the previous implementation at cyclomatic complexity ≥10; TypeScript threshold is <9. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#63380) The build:manifests-all step failed on Windows CI because absolute paths produced by node:path.join use backslash separators. The plugin embedded those paths directly into a JS template literal, so Rollup saw escape sequences like \a, \1 inside string literals — "Legacy octal escape" and related parse errors. Wrap the path with JSON.stringify in buildEntrySource so any character (backslashes, quotes, control chars) is correctly encoded. Added a regression test that asserts a Windows-style D:\… path is emitted as "D:\\…" in the generated source. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ts (AB#63380) Three packages (media, property-editors, templating) declared backofficeEntryPoint extensions inline in their umbraco-package.ts files rather than in manifests.ts. The unified-manifests boot path only reads each package's manifests.ts, so these three entry points were silently dropped — which broke E2E smoke tests on Windows where auth.setup.ts timed out waiting for the backoffice chrome to render after login. Resolved per entry-point shape: - media: the entry-point file only re-exports components. Moved the components/index.js side-effect import into manifests.ts directly and dropped the backofficeEntryPoint manifest entry. - templating: same shape (pure side-effect imports), same treatment. - property-editors: has a real onInit hook (proxying the entity-data- picker init). Kept the backofficeEntryPoint manifest in manifests.ts using the static `import * as entryPointModule` form, matching the webhook/language/user convention. Added a no-op `onUnload` export so the module satisfies UmbEntryPointModule's required shape. All three packages' umbraco-package.ts files now collapse to the uniform bundle-wrapper shape used by the other 36 first-party packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The merged manifests-all build successfully consolidates manifest
metadata, but the runtime switch (app.element.ts → single
import('@umbraco-cms/backoffice/manifests-all')) hit a deeper issue
discovered by Windows E2E tests:
The merged Rollup graph reaches element implementation source through
manifest sub-trees (manifests.ts → sub-package manifests → store
classes → side-effect element imports). Rollup bundles those elements
into chunks under dist-cms/manifests-all/. The per-workspace builds
also emit the same elements under dist-cms/packages/<pkg>/. At runtime,
externalised @umbraco-cms/backoffice/<pkg> imports resolve to the
per-workspace copy, which calls customElements.define; then the
merged graph's own copy fires the same define and the browser
throws "NotSupportedError: ... has already been used with this
registry".
Restoring CORE_PACKAGES + the original #registerExtensions /
#loadCurrentUser, so this PR ships purely as additive infrastructure:
- vite-plugin-unified-manifests + tests
- manifests-all.vite.config.ts + build:manifests-all script
- package.json exports + tsconfig path for @umbraco-cms/backoffice/manifests-all
- type stub at src/manifests-all/index.ts
The artifact is built and reachable but unused at runtime. Updating
the design doc to capture the deferred runtime switch and the three
candidate paths for resolving the duplication.
The backofficeEntryPoint normalisation (media/property-editors/
templating) is kept — the declarations now live in their respective
manifests.ts files. The bundle initializer continues to load them
per-workspace, so timing is the same as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Closing unmerged. The runtime switch (single import of That blocker is documented in detail on ADO #63380's comment 28132245, along with the realisation that Phase 1A (removing side-effect imports from barrels) is per-barrel deprecation work, not a line-removal sweep. The manifest unification has to wait for that cleanup. The branch 🤖 Closed via Claude Code. |
Prerequisites
Part of GH #21152 — backoffice load performance roadmap. Builds on PR #22896 (chunk coalescing).
Status — infrastructure only
Important
The runtime switch (
app.element.tsdroppingCORE_PACKAGESfor a single@umbraco-cms/backoffice/manifests-allimport) was attempted, reverted, and is now deferred. See the "Known issue" section below for the cause and the three candidate fixes.What this PR ships: the merged-manifests build infrastructure (Vite plugin, build script, importmap exposure, type stub) as additive groundwork, plus a normalisation that moves three packages' inlined
backofficeEntryPointextensions fromumbraco-package.tsinto theirmanifests.tsfor consistency with the other 36 packages.Description
Adds a build-time Vite plugin that walks
src/packages/*/manifests.ts, generates a virtual entry aggregating every package'smanifestsinto a flatallManifestsarray, and emits one mergeddist-cms/manifests-all/index.js. The artifact is exposed throughpackage.jsonexports as@umbraco-cms/backoffice/manifests-all. Nothing imports it at runtime in this PR — that's the deferred follow-up.Three packages (media, property-editors, templating) declared
backofficeEntryPointextensions inline in theirumbraco-package.tsinstead ofmanifests.ts. This PR moves them tomanifests.ts(with side-effect imports for the two whose entry-point.ts had noonInit, and as a proper backofficeEntryPoint manifest for property-editors which has a real init hook). The bundle initializer continues to load them per-workspace, so timing is unchanged.Design + plan
docs/superpowers/specs/2026-05-20-unified-manifests-design.mddocs/superpowers/plans/2026-05-20-unified-manifests.mdKnown issue: custom-element duplication (runtime switch deferred)
When the runtime switch was attempted, the backoffice failed to boot with
NotSupportedError: Failed to execute 'define' on 'CustomElementRegistry': the name "umb-block-action-list" has already been used with this registry.Root cause. The merged manifests-all Rollup graph statically reaches the implementation source of many custom elements (via the manifest sub-tree of each package — e.g.
block/manifests.ts→ sub-feature manifests → store / context-token modules → element side-effect imports). Rollup bundles those element sources into chunks underdist-cms/manifests-all/. The per-workspace builds also emit the same elements intodist-cms/packages/<pkg>/. At runtime, when something in the merged graph hits an externalised@umbraco-cms/backoffice/<pkg>import, the importmap resolves it to the per-workspace chunk, which callscustomElements.define(...). Then the merged graph's own copy fires the same define and crashes.Three candidate fixes (open):
manifests.tscontinues to load per-workspace via the bundle initializer'sjs()callback. Smaller win (saves one wave, not both) but no duplication.How to test
cd src/Umbraco.Web.UI.Client && npm installnpm run build:for:cms— confirmdist-cms/manifests-all/index.jsis emitted and the importmap contains@umbraco-cms/backoffice/manifests-all.dotnet run --project src/Umbraco.Web.UIfrom the repo root.v17/devdoes (boots, navigates, opens documents) — the merged artifact is unused at runtime, so behaviour should be identical.Verification done before opening this PR
npm run lint:errorscleannpm run compilecleannpm run check:circularcleannpm run check:module-dependenciesunchanged (14/15 bidirectional imports, same as before)node --test devops/build/vite-plugin-unified-manifests.test.mjs5/5 passnpm run build:for:cmscompletes end-to-endRollback
The runtime switch was already reverted within this PR. The remaining infrastructure is additive — if shipping just the build artifact + importmap entry is undesirable, the entire PR can be reverted with no impact on existing behaviour.
🤖 Generated with Claude Code