Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .changeset/11939-lazy-designers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@object-ui/app-shell': minor
'@object-ui/console': patch
---

The built-in metadata designers leave the console's first load: `@object-ui/app-shell` registers them from a chunk it loads on its own, once the package entry has run (objectui#11939, step 2 of objectui#6795).

Every built-in Preview, scoped Inspector and default Inspector used to be registered while the package entry was being evaluated, so their code sat on every page's first load although only metadata authors render them. The entry now starts loading them with a dynamic `import()` at its own module scope and registers them when that chunk arrives. Importing the package still registers every built-in designer with no action from the host; they are available a moment later than before instead of during the import. The entry for step 1 (the observable designer registries) says every built-in designer is still registered when the package loads: that held at step 1, and this step replaces it.

What a reader sees in that moment: a surface that renders before the chunk arrives shows its existing "designer missing" state, then the designer, with no reload. That is the observable registries of step 1 at work, through the `useRegistered*` hooks. A read with `getMetadataPreview`, `getMetadataInspector`, `listMetadataPreviewTypes` or `listMetadataInspectorTypes` returns what is registered at the moment of the call, so code that reads right after importing the package can find no built-in designer yet. During render, read through the hooks.

A host's own designer is kept, whichever arrives first. The built-ins are registered only for a type that has no designer yet: a host that calls `registerMetadataPreview` or `registerMetadataInspector` right after importing the package keeps its component when the built-ins land later, and a host that registers after they landed replaces the built-in, as before.

The three registrations whose registries are not observable stay where they were, during the entry's evaluation: the Related tab's anchors, the generic form's fallback schemas and the datasource resource.

`sideEffects` in `@object-ui/app-shell`'s `package.json` is unchanged. The module that registers the designers performs no registration at load time: the entry calls the function it exports.

No export is added, removed or changed. The console's dev-only designer gallery waits for the built-in designers to arrive before it renders its list, so it never lists zero designers.
15 changes: 15 additions & 0 deletions apps/console/src/__tests__/App.agentChatLink-10640.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));

import { getMetadataPreview } from '@object-ui/app-shell';
import { App } from '../App';
// The built-in designers. Importing the @object-ui/app-shell root registers them
// from a chunk it loads with a dynamic `import()` (objectui#11939 step 2), so
// they land after this module has run. Loaded and registered here, at module
// scope — the import phase, outside every test's window — by the function the
// root calls when that chunk arrives, so the registry read below returns the
// real built-in designer.
//
// `vi.importActual` and a workspace SOURCE path, not an `import` statement: the
// package publishes no subpath and the function is not on its barrel, and a
// static import would put the designers' sources into this app's `tsc` program,
// which checks them under rules their own package does not apply.
const { registerBuiltinDesigners } = await vi.importActual<{ registerBuiltinDesigners: () => void }>(
'../../../../packages/app-shell/src/views/metadata-admin/register-builtin-designers',
);
registerBuiltinDesigners();

/** The pre-fix link, byte for byte what `AgentPreview` used to draw. */
const PRE_FIX_HREF = '/console/ai/agents/sales_copilot/chat';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,30 @@
*
* The inspector is taken from the registry rather than imported, which is the
* route the console itself uses (`getMetadataInspector('object')`, registered by
* app-shell's `register-builtins` on package entry). So this renders the
* designer as WIRED, not a component picked out by hand.
* app-shell's `registerBuiltinDesigners`, which the package entry runs once the
* chunk it imports dynamically has loaded). So this renders the designer as
* WIRED, not a component picked out by hand.
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import { getMetadataInspector } from '@object-ui/app-shell';
import { SAMPLES } from '../preview-samples';
// The built-in designers. Importing the @object-ui/app-shell root registers them
// from a chunk it loads with a dynamic `import()` (objectui#11939 step 2), so
// they land after this module has run. Loaded and registered here, at module
// scope — the import phase, outside every test's window — by the function the
// root calls when that chunk arrives, so the registry read below returns the
// real built-in designer.
//
// `vi.importActual` and a workspace SOURCE path, not an `import` statement: the
// package publishes no subpath and the function is not on its barrel, and a
// static import would put the designers' sources into this app's `tsc` program,
// which checks them under rules their own package does not apply.
const { registerBuiltinDesigners } = await vi.importActual<{ registerBuiltinDesigners: () => void }>(
'../../../../packages/app-shell/src/views/metadata-admin/register-builtin-designers',
);
registerBuiltinDesigners();

/**
* ONE fetch double, installed at module scope and never torn down
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ import { systemRoutes } from '../../AppContent';
// and the registry both lazy-import this module, and ESM caches it by resolved
// specifier, so their factories resolve at once (AGENTS.md §测试纪律).
import { ApiConsolePage } from './ApiConsolePage';
// The built-in designers. Importing the @object-ui/app-shell root registers them
// from a chunk it loads with a dynamic `import()` (objectui#11939 step 2), so
// they land after this module has run. Loaded and registered here, at module
// scope — the import phase, outside every test's window — by the function the
// root calls when that chunk arrives, so the registry read below returns the
// real built-in designer.
//
// `vi.importActual` and a workspace SOURCE path, not an `import` statement: the
// package publishes no subpath and the function is not on its barrel, and a
// static import would put the designers' sources into this app's `tsc` program,
// which checks them under rules their own package does not apply.
const { registerBuiltinDesigners } = await vi.importActual<{ registerBuiltinDesigners: () => void }>(
'../../../../../packages/app-shell/src/views/metadata-admin/register-builtin-designers',
);
registerBuiltinDesigners();
// Side effect under test: registers `developer:api-console`, the way `main.tsx` does.
import '../../registerDeveloperComponents';

Expand Down
24 changes: 21 additions & 3 deletions apps/console/src/preview-gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ import {
import type { MetadataSelection } from '@object-ui/app-shell';
import { SAMPLES } from './preview-samples';

// Importing the @object-ui/app-shell root registers built-in previews and
// inspectors as a module side-effect, so no explicit registration is needed.
// Importing the @object-ui/app-shell root registers the built-in previews and
// inspectors, from a chunk the package entry loads with a dynamic `import()`
// (objectui#11939 step 2) — so they land AFTER this module has rendered once.
// The gallery is the designers' browser-verification path (the `verify` skill),
// so it waits for that chunk (`GalleryOnceDesignersArrive` below) and never
// lists zero designers while it is in flight.

const ORDER = [
'object',
Expand Down Expand Up @@ -375,10 +379,24 @@ function Gallery() {
);
}

/**
* The await of the package entry's own designer chunk. Its registrations
* notify the observable registry (objectui#11939), so this re-renders when they
* land and only then mounts the gallery. Until then it says what is happening;
* if the chunk fails, the package entry reports why on the console.
*/
function GalleryOnceDesignersArrive() {
const types = useRegisteredMetadataPreviewTypes();
if (types.length === 0) {
return <p className="p-6 text-sm text-muted-foreground">Loading the built-in designers…</p>;
}
return <Gallery />;
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nProvider config={{ defaultLanguage: 'en' }}>
<Gallery />
<GalleryOnceDesignersArrive />
</I18nProvider>
</React.StrictMode>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* objectui#11939 step 2 — importing the package entry still registers every
* built-in designer, with no call from the host.
*
* The entry no longer reaches the previews and inspectors statically: it loads
* `views/metadata-admin/register-builtin-designers.ts` with a dynamic
* `import()` at its own module scope and calls `registerBuiltinDesigners()`
* when that chunk arrives. Ruling A on objectui#11798 refused the lazy
* sub-entry ("drops every host relying on auto-registration without telling
* it"), so auto-registration is the contract this file holds the entry to.
*
* ⛔ What would turn this red: the entry's dynamic import deleted, or its
* `.then` no longer calling the registration. Every other suite registers the
* designers itself and stays green through either, and so does the console's
* eager-closure gate (the designers' chunk simply stops being emitted).
*
* The registries are read empty BEFORE the entry is imported, so the designers
* found afterwards can only have come from the entry. Identity, not presence:
* the registered component must BE the built-in one.
*
* The built-in components are imported at module scope, which loads the
* designers' module graph in the import phase; the entry's own `import()` then
* resolves from the module cache, and the wait below covers only its `.then`
* (AGENTS.md 测试纪律). Nothing here CALLS `registerBuiltinDesigners`.
*/

import { describe, it, expect, vi } from 'vitest';
import { getMetadataPreview, listMetadataPreviewTypes } from '../views/metadata-admin/preview-registry';
import { getMetadataInspector } from '../views/metadata-admin/inspector-registry';
import { getMetadataDefaultInspector } from '../views/metadata-admin/default-inspector-registry';
import { FlowPreview } from '../views/metadata-admin/previews/FlowPreview';
import { FlowInspector } from '../views/metadata-admin/inspectors/FlowInspector';
import { HookDefaultInspector } from '../views/metadata-admin/inspectors/HookDefaultInspector';

const typesBeforeTheEntry = listMetadataPreviewTypes();
await import('../index');

describe('the package entry registers the built-in designers (objectui#11939 step 2)', () => {
it('every built-in designer is registered once the entry’s chunk lands — no host call', async () => {
expect(typesBeforeTheEntry).toEqual([]);

await vi.waitFor(() => {
expect(getMetadataPreview('flow')).toBe(FlowPreview);
});
expect(getMetadataInspector('flow')).toBe(FlowInspector);
expect(getMetadataDefaultInspector('hook')).toBe(HookDefaultInspector);
});
});
33 changes: 26 additions & 7 deletions packages/app-shell/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,33 @@ import './views/global-notifications-renderer.js';
// `nav:menu`, is not in the eager `PALETTE_PLACEHOLDER_BLOCKS` set).
import './views/app-launcher-renderer.js';
import './views/nav-menu-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.
// The metadata-admin engine's three eager load-time registrations (built-in
// anchors, default JSONSchemas, the datasource resource). 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';
// The built-in metadata DESIGNERS (every Preview and Inspector) are registered
// from a chunk of their own, loaded here, at the entry's module scope, so
// importing the package still registers them with no action from the host —
// once the chunk has arrived rather than during this module's evaluation
// (objectui#11939 step 2). Their three registries are observable (step 1), so
// a reader that rendered first shows its "designer missing" state and then the
// designer. A host's own registration is kept whichever comes first: see
// `registerAsBuiltIns` in `views/metadata-admin/preview-registry.ts`.
//
// ⛔ Keep this edge DYNAMIC. A static import of that module, or of the
// previews/inspectors it registers, from anything this entry reaches statically
// puts every designer back on every console page's first load, and
// `scripts/check-eager-closure-budget.mjs` is sized on the bytes that left.
import('./views/metadata-admin/register-builtin-designers.js')
.then(({ registerBuiltinDesigners }) => registerBuiltinDesigners())
.catch((error: unknown) => {
// The readers keep their "designer missing" states; say why once, here.
console.error('[@object-ui/app-shell] The built-in metadata designers failed to load.', error);
});

// Phase 3c — generic metadata admin engine. Re-exported so plugins
// can call `registerMetadataResource()` to override the per-type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ import { MetadataResourceEditPage } from './ResourceEditPage';
// The load-time registrations, exactly as the package entry runs them, so the
// branch the page takes below is the branch production takes.
import './register-builtins';
// The built-in designers, which the package entry registers from a chunk it
// loads with a dynamic `import()` (objectui#11939 step 2): called here, at
// module scope, so they are registered before the first render.
import { registerBuiltinDesigners } from './register-builtin-designers';
registerBuiltinDesigners();

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ import { MetadataResourceEditPage } from './ResourceEditPage';
// The load-time registrations, exactly as the package entry runs them, so the
// object canvas and field inspector mounted here are the ones production mounts.
import './register-builtins';
// The built-in designers, which the package entry registers from a chunk it
// loads with a dynamic `import()` (objectui#11939 step 2): called here, at
// module scope, so they are registered before the first render.
import { registerBuiltinDesigners } from './register-builtin-designers';
registerBuiltinDesigners();

afterEach(() => {
cleanup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ import { MetadataResourceEditPage } from './ResourceEditPage';
// The load-time registrations, exactly as the package entry runs them, so the
// branch this case takes is the branch production takes.
import './register-builtins';
// The built-in designers, which the package entry registers from a chunk it
// loads with a dynamic `import()` (objectui#11939 step 2): called here, at
// module scope, so they are registered before the first render.
import { registerBuiltinDesigners } from './register-builtin-designers';
registerBuiltinDesigners();

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ import { MetadataResourceEditPage } from './ResourceEditPage';
import { MetadataResourceListPage } from './ResourceListPage';
import './register-builtins';
import '../../services/builtinComponents.js';
// The built-in designers, which the package entry registers from a chunk it
// loads with a dynamic `import()` (objectui#11939 step 2): called here, at
// module scope, so they are registered before the first render.
import { registerBuiltinDesigners } from './register-builtin-designers';
registerBuiltinDesigners();

// Pre-declared so a regression to an enabled button or a link reads by name.
const WRITE_VERB = /^(new|create|save|edit|delete|reset|publish|add)\b/i;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ import { getMetadataResource } from './registry';
// package entry runs them.
import './register-builtins';
import '../../services/builtinComponents.js';
// The built-in designers, which the package entry registers from a chunk it
// loads with a dynamic `import()` (objectui#11939 step 2): called here, at
// module scope, so they are registered before the first render.
import { registerBuiltinDesigners } from './register-builtin-designers';
registerBuiltinDesigners();

afterEach(() => {
cleanup();
Expand Down
Loading
Loading