diff --git a/.changeset/6776-metadata-admin-lazy-registration.md b/.changeset/6776-metadata-admin-lazy-registration.md new file mode 100644 index 000000000..17bdd4d2d --- /dev/null +++ b/.changeset/6776-metadata-admin-lazy-registration.md @@ -0,0 +1,40 @@ +--- +'@object-ui/app-shell': patch +--- + +Take the metadata-admin engine out of the console's eager closure (objectui#6776). + +`AppContent` has declared six `lazy()` imports of `views/metadata-admin/index.ts` +for a long time, and none of them deferred anything: the module ran five +registrations at load, so the package's published `sideEffects` array named it, +an array entry is unshakeable, and the package barrel re-exported 25 runtime +values from it — an ordinary static edge from an entry every consumer imports. +Every page, preview and inspector under `views/metadata-admin/` was therefore +fetched and parsed before first render. Measured from +`apps/console/dist/eager-closure.json`: **3,254,230 → 3,222,314 gzipped bytes, +−31,916 B**, and the 172,945-byte `metadata-admin` chunk leaves the eager set +entirely. + +**Published surface — two contract-bearing changes, no signature change:** + +- `packages/app-shell/package.json`'s `sideEffects` array now names + `views/metadata-admin/register-builtins` (the new leaf that performs the five + registrations) instead of `views/metadata-admin/index`. The five + registrations still run at package load, bare-imported by the package entry, + so nothing a consumer could observe changes — but the array is a contract + every consumer's bundler reads, so the swap is stated here rather than left + to a diff. +- The package barrel's 25 metadata-admin runtime re-exports (and 11 type-only + ones) now point at their leaf modules. **Same names, same types.** They are + unreachable from outside the package by any other path — `exports` is + root-only — so no import an out-of-package consumer can write is affected. + +`registerAppComponent`'s signature is unchanged. `metadata:directory` and +`metadata:resource` are now registered as `lazy()` values, each wrapping itself +in its own `Suspense` boundary, which is the shape the already-lazy +registrations in `apps/console` use; no render site changes. + +Also re-baselined `MAX_EAGER_CLOSURE_GZIP_BYTES` in the same change, from +3,300,000 to 3,268,000. Taking 31,916 bytes out without moving the ceiling would +leave 0.85x of the 89 KiB regression the gate exists to catch as headroom — +near-blind — so the ratchet advances with the win rather than after it. diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index 7c4c3ed9a..c9eae8183 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -12,7 +12,7 @@ "./dist/services/builtinComponents.js", "./dist/views/global-notifications-renderer.js", "./dist/views/global-search-renderer.js", - "./dist/views/metadata-admin/index.js", + "./dist/views/metadata-admin/register-builtins.js", "./dist/views/record-approvals-renderer.js", "./dist/views/record-attachments-renderer.js", "./dist/views/studio-design/studio-canvas-preview.js", @@ -25,7 +25,7 @@ "./src/services/builtinComponents.tsx", "./src/views/global-notifications-renderer.tsx", "./src/views/global-search-renderer.tsx", - "./src/views/metadata-admin/index.ts", + "./src/views/metadata-admin/register-builtins.ts", "./src/views/record-approvals-renderer.tsx", "./src/views/record-attachments-renderer.tsx", "./src/views/studio-design/studio-canvas-preview.tsx", diff --git a/packages/app-shell/src/index.ts b/packages/app-shell/src/index.ts index 6200b1b67..22003e143 100644 --- a/packages/app-shell/src/index.ts +++ b/packages/app-shell/src/index.ts @@ -309,57 +309,88 @@ import './views/record-approvals-renderer.js'; // `global:notifications`. import './views/global-search-renderer.js'; import './views/global-notifications-renderer.js'; +// The metadata-admin engine's five load-time registrations (built-in anchors, +// default JSONSchemas, the datasource resource, built-in previews, built-in +// inspectors). objectui#6776 moved them OUT of `views/metadata-admin/index.ts` +// into this leaf so the page barrel became shakeable; the bare import lives +// HERE, on the package entry, and must not be moved onto the page barrel — +// `scripts/vite-declared-lazy-views.ts` reads a bare import as "this module is +// not pure" and the whole eager closure comes back. See the leaf's own header. +import './views/metadata-admin/register-builtins.js'; // Phase 3c — generic metadata admin engine. Re-exported so plugins // can call `registerMetadataResource()` to override the per-type // list / edit / create components, and host apps can compose the // page primitives directly when needed. +// +// ⚠️ These 25 runtime re-exports name the LEAF modules, never +// `./views/metadata-admin/index.js` (objectui#6776). The names and their types +// are unchanged — an out-of-package consumer imports exactly what it imported +// before — but a named re-export is an ordinary STATIC EDGE, and the console's +// entry imports this barrel, so pointing them at the page barrel made that +// barrel (and every page, preview and inspector it reaches) eager on every +// console page load, past the six `lazy()` declarations `AppContent` writes for +// it. The 11 TYPE-ONLY re-exports below are erased at build and carry no edge; +// they are grouped separately for that reason and not because they are less +// public. +export { MetadataDirectoryPage } from './views/metadata-admin/DirectoryPage.js'; +export { MetadataResourceRouter } from './views/metadata-admin/ResourceRouter.js'; +export { MetadataResourceListPage } from './views/metadata-admin/ResourceListPage.js'; +export { MetadataResourceEditPage } from './views/metadata-admin/ResourceEditPage.js'; +export { MetadataResourceHistoryPage } from './views/metadata-admin/ResourceHistoryPage.js'; +export { MetadataDiagnosticsPage } from './views/metadata-admin/DiagnosticsPage.js'; +export { MetadataQuickFind } from './views/metadata-admin/QuickFind.js'; +export { PageShell as MetadataPageShell } from './views/metadata-admin/PageShell.js'; +export { SchemaForm } from './views/metadata-admin/SchemaForm.js'; +export { LayeredDiff } from './views/metadata-admin/LayeredDiff.js'; export { - MetadataDirectoryPage, - MetadataResourceRouter, - MetadataResourceListPage, - MetadataResourceEditPage, - MetadataResourceHistoryPage, - MetadataDiagnosticsPage, - MetadataQuickFind, - MetadataPageShell, - SchemaForm, - LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, +} from './views/metadata-admin/registry.js'; +export { useMetadataClient, useMetadataTypes, useTypesIndex, useGlobalDiagnostics, matchesQuery, +} from './views/metadata-admin/useMetadata.js'; +export { registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, +} from './views/metadata-admin/preview-registry.js'; +export { registerMetadataInspector, getMetadataInspector, listMetadataInspectorTypes, -} from './views/metadata-admin/index.js'; +} from './views/metadata-admin/inspector-registry.js'; export type { MetadataResourceConfig, MetadataDomain, - RichMetadataTypeEntry, +} from './views/metadata-admin/registry.js'; +export type { RichMetadataTypeEntry } from './views/metadata-admin/useMetadata.js'; +export type { MetadataPreview, MetadataPreviewProps, MetadataSelection, +} from './views/metadata-admin/preview-registry.js'; +export type { MetadataInspector, MetadataInspectorProps, - // The form authoring surface, in ONE declaration per layer: the field - // (objectui#5040 / #5542) and the two containers above it (objectui#5596). - // `apps/console` renders the same authored `FormView` documents this - // package's metadata-admin does; before it could import these names it kept - // its own hand-written copies of all three shapes. See the note on the - // re-export in `views/metadata-admin/index.ts`. +} from './views/metadata-admin/inspector-registry.js'; +// The form authoring surface, in ONE declaration per layer: the field +// (objectui#5040 / #5542) and the two containers above it (objectui#5596). +// `apps/console` renders the same authored `FormView` documents this package's +// metadata-admin does; before it could import these names it kept its own +// hand-written copies of all three shapes. See the note on the re-export in +// `views/metadata-admin/index.ts`. +export type { FormFieldSpec, FormSectionSpec, FormViewSpec, -} from './views/metadata-admin/index.js'; +} from './views/metadata-admin/form-spec.js'; // Studio WYSIWYG design surface (ADR-0080) — the open-source design surface. // The left AI copilot is an injected `aiSlot`; OSS renders three zones. diff --git a/packages/app-shell/src/services/builtinComponents.tsx b/packages/app-shell/src/services/builtinComponents.tsx index 6e706838f..2caa77109 100644 --- a/packages/app-shell/src/services/builtinComponents.tsx +++ b/packages/app-shell/src/services/builtinComponents.tsx @@ -15,12 +15,9 @@ * metadata type. */ +import { lazy, Suspense } from 'react'; import { registerAppComponent } from './componentRegistry.js'; -import { - MetadataDirectoryPage, - MetadataResourceRouter, - registerMetadataResource, -} from '../views/metadata-admin/index.js'; +import { registerMetadataResource } from '../views/metadata-admin/registry.js'; import { PermissionMatrixEditPage } from '../views/metadata-admin/PermissionMatrixEditor.js'; import { PackagesPage } from '../views/metadata-admin/PackagesPage.js'; import { PackagedAutomationPage } from '../views/setup/PackagedAutomationPage.js'; @@ -33,18 +30,96 @@ import { /* 1) Top-level admin pages — bound to `metadata:directory` + `metadata:resource` */ /* -------------------------------------------------------------------------- */ +/** + * ⚠️ TRAP — read this before "just making something here lazy" (objectui#6776). + * + * The two registrations below hold their pages behind `lazy()` + `Suspense`, and + * that is ONE HALF of a two-part change. The other half is that + * `packages/app-shell/src/index.ts` re-exports the metadata-admin names from + * their LEAF modules rather than from `views/metadata-admin/index.ts`, and that + * the five load-time registrations moved to + * `views/metadata-admin/register-builtins.ts`. Doing the `lazy()` here WITHOUT + * the other half is the smallest, most in-fence-looking version of this change, + * and it is worth almost nothing: + * + * Measured on `fab4802e3` — a full console build of that commit with ONLY this + * file's two registrations turned into `lazy()` values, against the same + * commit unmodified: + * + * eager closure 3,254,230 -> 3,254,441 B gzipped (+211 B) + * `metadata-admin` chunk 172,945 -> 173,341 B gzipped (+396 B) + * eager chunk count 45 -> 45 of 513 (UNCHANGED) + * the chunk itself still EAGER + * + * So it does not merely fail to pay — it costs bytes in both places, and it is + * the `lazy()`/`Suspense` scaffolding itself that it spends them on. And every + * gate stays GREEN while it does: that build exits 0, the + * `ineffective-dynamic-import` ledger prints its usual 43 pinned entries with no + * 44th, and `declared-lazy-views` prints "2 eager, all pinned". The ledger + * cannot see this because the static edge that defeats the `import()` does not + * live in this module at all — it lives in the package barrel's re-export. That + * is the objectui#5486 shape: code that CLAIMS a code split it does not have, + * with a loading fallback no user can ever reach. + * + * (The ruling that ordered this comment predicted −30 B and +189 B. The + * direction of the chunk growth and the green gates reproduced; the closure + * figure did not, and it came back POSITIVE. The measured numbers are the ones + * above.) + * + * ⚠️ And a second, INDEPENDENT rebuild of the same variant disagreed with that + * closure figure on its SIGN: −7 B where the run above measured +211 B. Both + * stand as what their run measured; together they say only that this delta is + * small and sensitive to the exact byte-form of the edit, so the sign is not a + * finding and neither is the "it costs bytes" reading of it. What both + * rebuilds reproduced identically IS the finding: the chunk stays EAGER and the + * eager chunk count stays 45 of 513 (build exit 0, every gate green). Cite + * those two, never a signed byte delta. + * + * So: a `lazy()` in this file is only ever true when nothing in the package's + * EAGER graph still names the same module statically. Check the barrel first, + * and measure from `apps/console/dist/eager-closure.json` and the emitted + * chunk's own module list — never from a source-level search, which cannot see + * chunk co-tenancy (objectui#6680, objectui#6681). + */ +const MetadataDirectoryPage = lazy(() => + import('../views/metadata-admin/index.js').then((m) => ({ default: m.MetadataDirectoryPage })), +); +const MetadataResourceRouter = lazy(() => + import('../views/metadata-admin/index.js').then((m) => ({ default: m.MetadataResourceRouter })), +); + +function MetadataAdminFallback({ label }: { label: string }) { + return
Loading {label}…
; +} + +/** + * The Suspense boundary lives INSIDE the registration value, which is the shape + * every other lazy `registerAppComponent` entry already uses + * (`apps/console/src/registerAccountComponents.tsx`, + * `registerDeveloperComponents.tsx`, `registerApprovalsComponents.tsx`). It + * keeps `registerAppComponent`'s published signature unchanged — a component + * VALUE, as before — and it means no render site has to learn about pending. + */ registerAppComponent({ ref: 'metadata:directory', label: 'All Metadata Types', source: '@object-ui/app-shell', - component: MetadataDirectoryPage, + component: (props: any) => ( + }> + + + ), }); registerAppComponent({ ref: 'metadata:resource', label: 'Metadata Resource', source: '@object-ui/app-shell', - component: MetadataResourceRouter, + component: (props: any) => ( + }> + + + ), }); registerAppComponent({ diff --git a/packages/app-shell/src/views/metadata-admin/index.ts b/packages/app-shell/src/views/metadata-admin/index.ts index d0a2697f8..10edaac8c 100644 --- a/packages/app-shell/src/views/metadata-admin/index.ts +++ b/packages/app-shell/src/views/metadata-admin/index.ts @@ -66,30 +66,26 @@ export type { MetadataAnchor, } from './registry.js'; -// Side-effect: register the built-in anchor relationships so the Related -// tab works out of the box for objects (hooks, views, pages, …). -import { registerBuiltinAnchors } from './anchors.js'; -registerBuiltinAnchors(); - -// Side-effect: register fallback JSONSchemas for the 12 writable types -// so the generic SchemaForm renders a real form (vs raw-JSON fallback) -// until the framework wires Zod→JSONSchema generation into /meta/types. -import { registerDefaultMetadataSchemas } from './default-schemas.js'; -registerDefaultMetadataSchemas(); -import { registerDatasourceResource } from './datasource/register.js'; -registerDatasourceResource(); - -// Side-effect: register built-in Preview-tab renderers (page, view, -// dashboard, report, app, object, email_template). Plugins can add or -// override entries via `registerMetadataPreview()`. -import { registerBuiltinPreviews } from './previews/index.js'; -registerBuiltinPreviews(); - -// Side-effect: register built-in scoped inspectors (dashboard widget, -// …). Plugins can add or override entries via -// `registerMetadataInspector()`. -import { registerBuiltinInspectors } from './inspectors/index.js'; -registerBuiltinInspectors(); +/** + * ⛔ NO LOAD-TIME REGISTRATION BELONGS IN THIS FILE (objectui#6776). + * + * The five built-in registrations (`registerBuiltinAnchors`, + * `registerDefaultMetadataSchemas`, `registerDatasourceResource`, + * `registerBuiltinPreviews`, `registerBuiltinInspectors`) used to run here. + * They now live in `./register-builtins.js`, which the PACKAGE ENTRY + * (`packages/app-shell/src/index.ts`) bare-imports; that file carries the + * reasoning. In one line: a module that registers at load time is named by + * `@object-ui/app-shell`'s published `sideEffects` array, an array entry is + * unshakeable, and the package barrel re-exports 25 runtime values from HERE — + * so a registration in this file drags every page, preview and inspector under + * `views/metadata-admin/` into the console's eager closure, past the six + * `lazy()` boundaries `AppContent` declares for it. + * + * That also rules out the tidy-looking version of the same mistake: a bare + * `import './register-builtins.js';` on THIS module. `bareSideEffectImport` in + * `scripts/vite-declared-lazy-views.ts` reads that line and refuses to declare + * this barrel pure, which puts the closure straight back. + */ export { registerMetadataPreview, diff --git a/packages/app-shell/src/views/metadata-admin/register-builtins.ts b/packages/app-shell/src/views/metadata-admin/register-builtins.ts new file mode 100644 index 000000000..576060a08 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/register-builtins.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The metadata-admin engine's five LOAD-TIME registrations, in a leaf module of + * their own (objectui#6776). + * + * ## Why they are not in `./index.ts` any more + * + * They used to sit at the bottom of the directory barrel, and that placement — + * not the registrations themselves — is what held 172,945 gzipped bytes of + * metadata-admin in the console's EAGER closure. The mechanism, in two links: + * + * 1. a module that registers at load time cannot be tree-shaken, so + * `@object-ui/app-shell`'s published `sideEffects` array named + * `views/metadata-admin/index.ts` (objectui#6683) and every bundler kept + * it whole; and + * 2. the package barrel (`packages/app-shell/src/index.ts`) re-exported 25 + * runtime values FROM that barrel, and the console's entry imports the + * package barrel statically. A named re-export is an ordinary static edge, + * so the unshakeable module — and its whole import closure, every page, + * preview and inspector under `views/metadata-admin/` — was reachable + * eagerly even though `AppContent` declares six `lazy()` imports of it. + * + * Neither link can be cut where it is observed. Deleting the registrations is + * not on the table: they are load-bearing (drop them and the Related tab, the + * generic SchemaForm, the datasource resource, and every built-in Preview and + * Inspector go missing), and `scripts/vite-declared-lazy-views.ts` carries a + * guard that refuses to declare the module pure for exactly that reason. + * + * So the two concerns are SPLIT instead. This module is the side-effectful half + * and nothing re-exports through it; `./index.ts` is the pure re-export half and + * registers nothing. The package entry bare-imports THIS file, which keeps all + * five registrations exactly as eager as they were, while `./index.ts` becomes + * shakeable and reaches the browser only through the `lazy()` boundaries that + * always claimed to defer it. + * + * ## ⛔ Do not attach this import to the page barrel + * + * The bare import belongs to the PACKAGE ENTRY (`packages/app-shell/src/index.ts`). + * Putting it back on `./index.ts` — even as `import './register-builtins.js';` — + * re-creates the defect under a new name: `bareSideEffectImport` in + * `scripts/vite-declared-lazy-views.ts` reads that line and refuses to declare + * the barrel pure, so the barrel is unshakeable again and the closure returns. + * + * ## Ordering + * + * These five are independent of one another and of `./index.ts`: each writes + * into a registry module that holds a plain `Map`, and no registrar reads + * another's table at module scope. The order below is the order they were + * written in the barrel, kept so a `git log -p` of the move reads as a move. + */ + +import { registerBuiltinAnchors } from './anchors.js'; +import { registerDefaultMetadataSchemas } from './default-schemas.js'; +import { registerDatasourceResource } from './datasource/register.js'; +import { registerBuiltinPreviews } from './previews/index.js'; +import { registerBuiltinInspectors } from './inspectors/index.js'; + +// Register the built-in anchor relationships so the Related tab works out of +// the box for objects (hooks, views, pages, ...). +registerBuiltinAnchors(); + +// Register fallback JSONSchemas for the 12 writable types so the generic +// SchemaForm renders a real form (vs raw-JSON fallback) until the framework +// wires Zod->JSONSchema generation into /meta/types. +registerDefaultMetadataSchemas(); + +registerDatasourceResource(); + +// Register built-in Preview-tab renderers (page, view, dashboard, report, app, +// object, email_template). Plugins can add or override entries via +// `registerMetadataPreview()`. +registerBuiltinPreviews(); + +// Register built-in scoped inspectors (dashboard widget, ...). Plugins can add +// or override entries via `registerMetadataInspector()`. +registerBuiltinInspectors(); diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts index b3ac408d6..a602f15ba 100644 --- a/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/scripts/__tests__/check-eager-closure-budget.test.ts @@ -470,10 +470,11 @@ describe('ceiling sensitivity, judged live (objectui#5924)', () => { ]); // A passing run still prints every measurement, so a reader watching a // ceiling drift upward sees it coming rather than the day it reds. The - // literal is `BASELINE.gzipBytes` rendered, re-taken when objectui#6683 - // re-baselined it downward — a rendering derived in the test would agree - // with the renderer by construction and pin nothing. - expect(result.message).toContain('3177.7'); + // literal is `BASELINE.gzipBytes` rendered, re-taken each time the baseline + // moves (objectui#6683 down to 3177.7, objectui#6776 down to 3146.8) — a + // rendering derived in the test would agree with the renderer by + // construction and pin nothing. + expect(result.message).toContain('3146.8'); }); it('is exactly one regression wide, from either side of the line', () => { @@ -648,7 +649,7 @@ describe('main', () => { expect(code).toBe(0); expect(outputs.closure_status).toBe('pass'); expect(outputs.closure_chunks).toBe('5'); - expect(outputs.closure_gzip_kb).toBe('3177.7'); + expect(outputs.closure_gzip_kb).toBe('3146.8'); }); it('exits 1 — a verdict about the BUNDLE — when over budget', () => { diff --git a/scripts/__tests__/vite-declared-lazy-views.test.ts b/scripts/__tests__/vite-declared-lazy-views.test.ts index d4088c98e..7cb4a66c4 100644 --- a/scripts/__tests__/vite-declared-lazy-views.test.ts +++ b/scripts/__tests__/vite-declared-lazy-views.test.ts @@ -188,17 +188,22 @@ describe('declaredSideEffectful', () => { expect(declaredSideEffectful(['./src/**/*.css'], 'src/views/Alpha.tsx')).toContain('glob'); }); - it('agrees with the real package: metadata-admin/index.ts is declared side-effectful', () => { + it('agrees with the real package: register-builtins.ts is declared side-effectful', () => { // The subject. This is the file whose FIVE top-level registration calls // `bareSideEffectImport` cannot see, so without this guard the plugin would // declare it pure the moment someone deleted its ledger line - // (objectui#6681). - const owner = nearestPackage('packages/app-shell/src/views/metadata-admin/index.ts', REPO_ROOT); + // (objectui#6681). objectui#6776 MOVED those five calls out of the page + // barrel and into this leaf; the guard's job and its blind spot are + // unchanged, only the file that carries them. + const owner = nearestPackage( + 'packages/app-shell/src/views/metadata-admin/register-builtins.ts', + REPO_ROOT, + ); expect(owner?.packageJsonPath).toBe('packages/app-shell/package.json'); - expect(owner?.packageRelative).toBe('src/views/metadata-admin/index.ts'); + expect(owner?.packageRelative).toBe('src/views/metadata-admin/register-builtins.ts'); const manifest = JSON.parse(read(owner!.packageJsonPath)) as { sideEffects?: unknown }; expect(declaredSideEffectful(manifest.sideEffects, owner!.packageRelative)).toBe( - './src/views/metadata-admin/index.ts', + './src/views/metadata-admin/register-builtins.ts', ); // The positive control in the same query shape: a declared-lazy module the // array does NOT name, so a matcher that answered "side-effectful" to @@ -208,6 +213,21 @@ describe('declaredSideEffectful', () => { ).toBeNull(); }); + it('the page barrel is NOT declared side-effectful any more (objectui#6776)', () => { + // The other half of the move, stated as a pin rather than left to the + // ledger: `views/metadata-admin/index.ts` is what the console's six + // `lazy()` declarations name, and it is shakeable ONLY while the published + // `sideEffects` array does not name it. Re-adding it there — or putting a + // bare `import './register-builtins.js';` back on the barrel, which + // `bareSideEffectImport` reads as the same claim — puts 172,945 gzipped + // bytes back into every console page load, silently. + const owner = nearestPackage('packages/app-shell/src/views/metadata-admin/index.ts', REPO_ROOT); + expect(owner?.packageJsonPath).toBe('packages/app-shell/package.json'); + const manifest = JSON.parse(read(owner!.packageJsonPath)) as { sideEffects?: unknown }; + expect(declaredSideEffectful(manifest.sideEffects, owner!.packageRelative)).toBeNull(); + expect(bareSideEffectImport(read('packages/app-shell/src/views/metadata-admin/index.ts'))).toBeNull(); + }); + it('the source-reading guard is blind to it, which is why this one exists', () => { // Stated as a test rather than a comment: if `bareSideEffectImport` ever // learns to see top-level calls, this expectation flips and the reader is diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index 186ff4501..314b15e0d 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -138,6 +138,26 @@ * reopening the same day it was measured shut. The two numbers move in ONE * commit for the reason the paragraph above gives. * + * objectui#6776 lowered it a third time, to 3,268,000 over 3,222,314, and this + * one was earned the same way. `views/metadata-admin/index.ts` stopped being a + * registering module — its five load-time registrations moved to a leaf the + * PACKAGE ENTRY bare-imports — so the `sideEffects` array stopped naming it, the + * package barrel's 25 runtime re-exports were re-pointed at leaf modules, and + * the whole `metadata-admin` chunk (172,945 gzipped bytes, 144 modules) left the + * eager closure: 3,254,230 -> 3,222,314, −31,916 bytes, measured on two full + * console builds. The maintainer ruling of 2026-08-30 made the re-baseline part + * of the change rather than a follow-up, in its own words: + * + * ⛔ ceiling 处置写死(不作实施者临场判断):-31KB 把余量推到 ~0.89x 门禁 + * 89KB 回归阈值(近盲),同批重设 `MAX_EAGER_CLOSURE_GZIP_BYTES`;抬 ceiling + * 是有申报程序的 ratchet,裁决原话引入 PR 正文。 + * + * Measured, the drift was 0.85x rather than 0.89x — the gate printed + * `headroom 75.9 KB = 0.85x the 89.0 KB regression` on the post-change build + * before this constant moved. Either way it is the blind band reopening, and the + * direction of this edit is DOWN: no build that passed before it and measures + * under 3,268,000 fails after it. + * * ⚠️ Read the direction correctly: the closure did NOT fall by the 242.6 KB the * objectui#6683 card projected. That figure was measured for * `"sideEffects": false`, which is closed by measurement because it also DROPS @@ -157,12 +177,12 @@ * become an excuse to widen it — a ceiling that rises while the sensitivity * relaxes is a gate quietly retiring itself. * - * This is a truthful CURRENT-STATE ceiling, not a target. 3.15 MB gzipped + * This is a truthful CURRENT-STATE ceiling, not a target. 3.07 MB gzipped * before first render is a bad payload, and the honest long-term line is far * below it — but lowering the line to a TARGET is a separate decision with its * own work behind it (objectui#5324 names the candidates), and re-baselining * onto a fresh measurement is not that. Nothing here should be read as a - * finding that 3.19 MB is acceptable. + * finding that 3.12 MB is acceptable. * * ## Per-chunk ceilings (objectui#5490) * @@ -210,10 +230,10 @@ import { isEntrypoint } from './invoked-as.mjs'; /** * Ceiling for the console eager closure, in gzipped bytes. See the header for - * how this number was chosen; measured 3,254,004 on `bd2a7ec50`. + * how this number was chosen; measured 3,222,314 on `3d257c85a`. * - * Re-baselined DOWNWARD twice, each time toward a measurement the payload had - * already fallen to: + * Re-baselined DOWNWARD three times, each time toward a measurement the payload + * had already fallen to: * * - objectui#5924, from 4,086,000 (derived from the 4,005,911 reading on * `4c1623c0c`) to 3,345,000 over 3,299,898 on `48e53814e`. @@ -224,10 +244,17 @@ import { isEntrypoint } from './invoked-as.mjs'; * at 1.00x its own sensitivity and, on the next byte of shrink, tripped the * exit-2 verdict about the gauge. Lowering it in the SAME change is the * tightening the maintainer ruling of 2026-08-29 asked for. - * - * Headroom is 45,996 bytes — 0.50x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. + * - objectui#6776, to 3,268,000 over 3,222,314. The metadata-admin engine's + * five load-time registrations moved out of the page barrel, so the barrel + * stopped being named by the `sideEffects` array and the 172,945-byte + * `metadata-admin` chunk left the eager closure (−31,916 bytes). The + * 3,300,000 ceiling was measured carrying 75.9 KB of headroom afterwards — + * 0.85x the regression this gate must catch, the blind band reopening — and + * the 2026-08-30 ruling made moving it part of the same change. + * + * Headroom is 45,686 bytes — 0.50x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. */ -export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_300_000; +export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_268_000; /** * The measurement the ceiling above was derived from. Exported so the two @@ -240,17 +267,17 @@ export const BASELINE = Object.freeze({ /** * `emitEagerClosureReport`'s `eagerGzipBytes` on this commit. * - * `bd2a7ec50` is the commit that carries the array and the gates; this - * constant was written one commit later, and the two trees differ ONLY by - * this recorded identifier. That is safe to state rather than hope: the + * `3d257c85a` is the commit that carries the metadata-admin split + * (objectui#6776); this constant was written one commit later, and the two + * trees differ ONLY by this file. That is safe to state rather than hope, and + * it is the same argument the previous baseline (`bd2a7ec50`) made: the * console build's turbo `inputs` cover `scripts/vite-*.ts`, not - * `scripts/check-*.mjs`, so nothing in this file reaches the bundler. The - * figure was re-measured on the later commit and came back identical. + * `scripts/check-*.mjs`, so nothing in this file reaches the bundler. */ - gzipBytes: 3_254_004, + gzipBytes: 3_222_314, chunks: 48, - totalChunks: 513, - commit: 'bd2a7ec50', + totalChunks: 517, + commit: '3d257c85a', }); /** diff --git a/scripts/vite-declared-lazy-views.ts b/scripts/vite-declared-lazy-views.ts index e1971ad30..6dc67961a 100644 --- a/scripts/vite-declared-lazy-views.ts +++ b/scripts/vite-declared-lazy-views.ts @@ -104,7 +104,7 @@ import type { Plugin, Rollup } from 'vite'; * * | chunk | gz eager | mechanism | * |----------------------------|----------|------------------------------------| - * | `metadata-admin` | 172,651 | real static edges — PINNED below | + * | `metadata-admin` | 172,651 | real static edges — FIXED, #6776 | * | `MarketplacePackagePage` | 7,647 | chunk co-tenancy — FIXED | * | `MarketplaceInstalledPage` | 1,836 | chunk co-tenancy — FIXED | * | `MarketplacePage` | 0 | already lazy (the control) | @@ -178,52 +178,73 @@ export const EAGER_WALK_CONTROL = 'packages/app-shell/src/views/ObjectView.tsx'; * (`scripts/__tests__/vite-declared-lazy-views.test.ts` checks that, and that * every entry still names a file that exists). * - * Two entries stand, and neither stands for the barrel re-export objectui#6535 - * removed: + * ONE entry stands, and it does not stand for the barrel re-export + * objectui#6535 removed: * * - `RecordDetailView` — a real static edge. * `packages/app-shell/src/views/ObjectView.tsx` imports it by name, and * `ObjectView` sits in AppContent's own "eagerly loaded — always needed" * block. Splitting it would mean giving `ObjectView` a lazy boundary. * - * - `views/metadata-admin/index.ts` — the LARGEST single entry this ledger has - * ever carried (172,651 bytes gzipped, 144 modules, 5.3% of the whole eager - * closure) and the one most likely to be "fixed" by someone who has not read - * why it stands. It is the target of SIX `lazy()` declarations in AppContent - * and it is statically imported by two modules that are eager by - * construction, both measured from the emitted chunk's module list on - * `b98352a15` (objectui#6681): - * - * 1. `packages/app-shell/src/index.ts` — the package barrel, which - * re-exports eleven runtime values from it (`registerMetadataPreview`, - * `useMetadataClient`, …). The console's entry imports that barrel. - * 2. `packages/app-shell/src/services/builtinComponents.tsx` — which the - * barrel BARE-imports (`import './services/builtinComponents.js';`) for - * its ComponentRegistry registrations, and which imports - * `MetadataDirectoryPage` and `MetadataResourceRouter` from this module - * BY VALUE. A registry entry that names a component must hold the - * component. - * - * Neither edge is removable inside a bundling change, and the module cannot - * be declared pure: it performs FIVE top-level registrations at module load - * (`registerBuiltinAnchors`, `registerDefaultMetadataSchemas`, - * `registerDatasourceResource`, `registerBuiltinPreviews`, - * `registerBuiltinInspectors`), which is why - * `@object-ui/app-shell`'s own `sideEffects` array names it (objectui#6683). - * ⚠️ Those five are CALLS, not bare imports, so {@link bareSideEffectImport} - * returns `null` for this file — the guard that actually refuses to declare - * it pure is {@link declaredSideEffectful}, reading the package's own array. - * - * The six pages the declarations name (`DirectoryPage`, `StudioHomePage`, - * `ResourceListPage`, `ResourceEditPage`, `ResourceHistoryPage`, - * `DiagnosticsPage`) have NO dynamic importer of their own in the emitted - * graph — they are reached only through this barrel's static re-exports — - * so no chunking policy separates them from it. Making this lazy means - * changing what `registerAppComponent` accepts (a component VALUE today) and - * what the package barrel re-exports; that is a published-contract decision, - * not a bundling one, and it is recorded as such rather than attempted here. - * - * ## Two entries were REMOVED here, and that removal is a recorded win + * ## `views/metadata-admin/index.ts` was the third removal — objectui#6776 + * + * It was the LARGEST single entry this ledger ever carried: 172,945 bytes + * gzipped over 144 modules, 5.3% of the eager closure, the target of SIX + * `lazy()` declarations in AppContent that deferred nothing. Two static edges + * held it, both measured from the emitted chunk's module list (objectui#6681): + * + * 1. `packages/app-shell/src/index.ts` — the package barrel re-exported 25 + * runtime values from it (`registerMetadataPreview`, `useMetadataClient`, + * …; the earlier note here said eleven, which counted only part of the + * list and omitted the 11 type-only names that carry no edge at all — + * corrected by objectui#6785). The console's entry imports that barrel. + * 2. `packages/app-shell/src/services/builtinComponents.tsx` — which the + * barrel BARE-imports for its ComponentRegistry registrations, and which + * imported `MetadataDirectoryPage` and `MetadataResourceRouter` from this + * module BY VALUE. A registry entry that names a component must hold the + * component. + * + * The note that stood here said neither edge was removable inside a bundling + * change, and that was right — the repair was NOT a bundling change. Under the + * maintainer ruling of 2026-08-30 (objectui#6776), three things moved together: + * + * - the five top-level registrations (`registerBuiltinAnchors`, + * `registerDefaultMetadataSchemas`, `registerDatasourceResource`, + * `registerBuiltinPreviews`, `registerBuiltinInspectors`) moved to + * `views/metadata-admin/register-builtins.ts`, bare-imported by the PACKAGE + * ENTRY, so they stay exactly as eager as they were while the page barrel + * stops being side-effectful. `@object-ui/app-shell`'s `sideEffects` array + * names the new leaf instead of the barrel; + * - the package barrel's 25 runtime re-exports now name the LEAF modules, + * same names and same types, which removes edge 1; and + * - `builtinComponents.tsx` registers the two pages as `lazy()` values, each + * behind its own `Suspense` inside the registration value, which removes + * edge 2 without touching `registerAppComponent`'s signature. + * + * ⚠️ The `lazy()` in `builtinComponents.tsx` is the half that looks sufficient + * and is not. Measured on `fab4802e3` with ONLY that half applied: the eager + * closure went UP by 211 bytes, the `metadata-admin` chunk went up by 396, the + * eager chunk count did not move, the chunk stayed EAGER — and the build exited + * 0 with this plugin printing "2 eager, all pinned" and + * `scripts/vite-ineffective-dynamic-imports.ts` printing its usual 43, because + * neither can see a static edge that lives in another module. That trap is + * written up beside the code in `builtinComponents.tsx`. + * + * Read that 211 as a magnitude, not a direction: an INDEPENDENT rebuild of the + * same variant measured the closure delta at -7 bytes. Both figures stand as + * what their run measured, and the disagreement is the point — the delta is + * small and sensitive to the byte-form of the edit, so its sign is not a + * finding. The finding is the pair that reproduced on both rebuilds: the chunk + * stayed EAGER and the eager chunk count did not move (45 of 513). + * + * ⛔ What has NOT changed: the five registrations are still load-bearing and + * still unshakeable. {@link declaredSideEffectful} — not + * {@link bareSideEffectImport}, which returns `null` for a top-level CALL — + * is the guard that refuses to declare their module pure, and it now reads + * `register-builtins.ts` out of the package's own array. Moving the bare import + * back onto the page barrel would re-arm the whole defect under a new name. + * + * ## Two entries were REMOVED here BEFORE that, and both removals are wins * * `RecordFormPage` and `ReportView` were pinned for a third reason, the one no * `grep` over the source shows: CHUNK CO-TENANCY (objectui#6680). Rolldown @@ -246,7 +267,6 @@ export const EAGER_WALK_CONTROL = 'packages/app-shell/src/views/ObjectView.tsx'; */ export const DECLARED_LAZY_VIEWS_STILL_EAGER: readonly string[] = Object.freeze([ 'packages/app-shell/src/views/RecordDetailView.tsx', - 'packages/app-shell/src/views/metadata-admin/index.ts', ]); /**