Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/6776-metadata-admin-lazy-registration.md
Original file line numberDiff line numberDiff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/app-shell/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand All@@ -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",
Expand Down
69 changes: 50 additions & 19 deletions packages/app-shell/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
89 changes: 82 additions & 7 deletions packages/app-shell/src/services/builtinComponents.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand All@@ -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 <div className="p-6 text-sm text-muted-foreground">Loading {label}…</div>;
}

/**
* 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) => (
<Suspense fallback={<MetadataAdminFallback label="metadata types" />}>
<MetadataDirectoryPage {...props} />
</Suspense>
),
});

registerAppComponent({
ref: 'metadata:resource',
label: 'Metadata Resource',
source: '@object-ui/app-shell',
component: MetadataResourceRouter,
component: (props: any) => (
<Suspense fallback={<MetadataAdminFallback label="metadata resource" />}>
<MetadataResourceRouter {...props} />
</Suspense>
),
});

registerAppComponent({
Expand Down
44 changes: 20 additions & 24 deletions packages/app-shell/src/views/metadata-admin/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand Down
Loading
Loading