Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 899f381

Browse files
feat(core,types)!: retire the module widget source and its runtime dynamic import (objectui#13450) (#13473)
Fixes #13450 Clause-②: no (narrowing) Metadata-protocol: narrowed Retires the `module` widget source. `WidgetRegistry.load()` resolved `{ type: 'module', url }` with `new Function('url', 'return import(url)')`. That is the runtime dynamic import AGENTS.md commandment `#6` forbids ("No `eval()` / runtime dynamic imports to load components"). The claim is the PM's comment `6386913269` on the card, for branch `claude/issue-13450-retire-widget-module-source`, session `https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ`. ## What changes - **`@object-ui/types`**: the `module` member and its `WidgetSourceModule` interface are removed. `RuntimeWidgetSource` is now `WidgetSourceInline | WidgetSourceRegistry`, and the barrel no longer exports `WidgetSourceModule`. The union's docblock records the retirement and the migration. The manifest's JSDoc `@example` now uses `{ type: 'inline', component }` with a locally defined `CustomChart` component (the build-time import is named in a comment, so the block compiles in isolation under `check-doc-example-types`) instead of a URL. - **`@object-ui/core`**: the `case 'module'` arm and its Function-constructor sink are removed from `WidgetRegistry.resolveComponent()`. The `default:` arm is now exhaustive (`const unhandled: never = source`), so a union member added without an arm fails to compile. - An untyped manifest that still reaches `load()` with `type: 'module'` is refused by name before anything is fetched. Untyped means plain JS, a cast, or an older `@object-ui/types`. The error names the widget, the retired `"module"` source and both replacements. - Any other unknown type is refused with the supported types listed. - In both cases `load()` emits `widget:error` and `loadAll()` reports the error. The class docblock and its example no longer mention URL loading. - **Changeset** `.changeset/13450-retire-widget-module-source.md`: `minor` for core and types. objectui never declares `major` (`check-changeset-no-major`); the break is stated in the body as **BREAKING**. The changeset gives the FROM → TO migration and the one-line fix, and carries the `Clause-②:` / `Metadata-protocol:` lines. - `page-nav-misc-spec-parity.test.ts`: the reason string in one `it.each` row said "module/inline/registry loader union" and now says "inline/registry". It is a test title, not an assertion. ## Migration (FROM → TO) - FROM `source: { type: 'module', url: '/widgets/chart.js', exportName: 'Chart' }` - TO `import { Chart } from './widgets/chart'` and `source: { type: 'inline', component: Chart }` (imported at build time, so the bundler sees it) - OR `ComponentRegistry.register('chart', Chart)` once, and `source: { type: 'registry', registryKey: 'chart' }` ## Premise check: the card's "four emitting sites" are not widget sources The card and the triage route name four `type: 'module'` producers. Each one was read in context at `a3c0da9`. All four are the generated **`package.json`'s ESM marker** (`"type": "module"`), not a widget manifest source. Deleting them would make the scaffolded packages CommonJS and break their `import` syntax. **None was changed:** | Site (by symbol) | What it builds | Verdict | |---|---|---| | `buildAppPackageJson` in `packages/cli/src/utils/app-generator.ts` | temp app `package.json` (`name`, `private`, `type`, deps) | package.json ESM marker; kept | | `buildRoutedAppPackageJson` in the same file | routed temp app `package.json` | package.json ESM marker; kept | | `buildInitPackageJson` in `packages/cli/src/commands/init.ts` | `objectui init` project `package.json` (`scripts.dev: 'vite'`) | package.json ESM marker; kept | | `buildPackageJson` in `packages/create-plugin/src/templates.ts` | generated plugin `package.json` (`main`, `module`, `exports`) | package.json ESM marker; kept | So `@object-ui/cli` and `@object-ui/create-plugin` carry no change and no changeset, which narrows the route. No widget-manifest producer of a `module` source exists in this tree. A `git grep` for `WidgetSourceModule` before this change hit only its declaration and the barrel. `apps/console/src` and `packages/app-shell/src` construct no `module` source (triage's reading). `objectstack` and `hotcrm` reference neither `WidgetRegistry` nor `RuntimeWidgetSource` in code (the only `objectstack` hit is a doc paragraph that does not name the `module` source). Out-of-tree consumers are NOT MEASURED. No user-facing doc names the `module` widget source. `content/docs`, the package READMEs and `docs/` were checked with `git grep`; the control `ComponentRegistry` hits in `content/docs`. The two JSDoc examples were the only documentation of it, and both are rewritten. ## Tests (head `e15036c`) - `be01ada` (patch round): `check-doc-example-types` exit 1 → 0 (34 → 35 compile; `WidgetRegistry #1` back on its TS2304 ledger row). JSDoc text only. New `describe('the retired \`module\` source (objectui#13450)')` in `packages/core/src/registry/__tests__/WidgetRegistry.test.ts`: 1. A manifest with `source: { type: 'module', url }` is refused. The rejection is an `Error` that names `"test-widget"`, `"module"`, `type: 'inline'` and `type: 'registry'`; these are the named subjects, not the wording. The global `Function` constructor, proxied for construct and apply, records zero calls. Nothing is marked loaded or synced into the component registry. The literal carries `@ts-expect-error`, so the type-level refusal is pinned too: core's `type-check` runs `tsconfig.test.json`, which includes `src/**/*.test.ts`. 2. `loadAll()` reports the refusal for that widget, a sibling still loads, and `widget:error` fires. 3. An unknown type (`remote`) is refused with `"inline"` and `"registry"` named and no "retired" wording. - `pnpm exec vitest run packages/core/ --maxWorkers=2`: `Test Files 211 passed (211)`, `Tests 4089 passed | 27 skipped (4116)` - `pnpm exec vitest run packages/types/ --maxWorkers=2`: `Test Files 388 passed (388)`, `Tests 10568 passed (10568)` - `pnpm --filter @object-ui/types --filter @object-ui/core run type-check`: both `Done`, exit 0. This ran after both packages were rebuilt, so core's test program read the new `dist/*.d.ts`. **Ablation, runtime** (one-shot, through `ablation-replace.mjs`, restored to the HEAD blob): a `case 'module'` arm with the Function-constructor import was re-planted ahead of `default:`. On-disk marker count was 1, sink count 1. Result: `Tests 2 failed | 24 passed (26)`. Test 1 received `"A dynamic import callback was not specified."` instead of the refusal; test 2 failed the same way. Test 3 stayed green, which is the expected direction. After the restore, `git diff HEAD` was empty and the blob equalled HEAD. A first attempt used an anchor that the replacement re-contained; the tool refused it and nothing ran. **Reverse verification, types** (cross-package): a `{ type: 'module'; url: string }` member was re-added to `RuntimeWidgetSource` and `@object-ui/types` rebuilt; the dist marker count was 1. Core's `tsc -p tsconfig.test.json` then reported `TS2578: Unused '@ts-expect-error' directive` on the new test and `TS2322 ... is not assignable to type 'never'` on the exhaustive arm. After the restore and a rebuild, the dist marker count was 0 and the same `tsc` was green. ## Gates (local, at `e15036c`) Exit codes were captured to a file before reading: - `lint` (core + types): exit 0. 0 errors; the warnings are all pre-existing `no-explicit-any`, and none is on a changed line. - `check:new-line-citations` 0 · `check:control-bytes` 0 · `check-changeset-presence` 0 · `changeset:check` (fixed + no-major) 0 · `check:changeset-claims` 0 · `check:pending-changeset-literals` 0 · `check:spec-symbols` 0 · `check:unreferenced-sources` 0 · `check:doc-example-readers` 0 · `check:shell-escape-residue` 0 · `check:test-path-roots` 0 - `check:readme-exports`: exit 1, NOT MEASURED for the repo as a whole. The only unjudged population is `@object-ui/app-shell`, which was unbuilt; every other package was judged with `0 fabricated, 0 wrong-path`. No README names `WidgetSourceModule`. - `check:published-dist`: exit 1, NOT MEASURED. Its internal whole-workspace `turbo run build` hit a heap OOM in `@object-ui/app-shell`, so it judged nothing. That is environmental and not this diff. CI owns this gate. `origin/main` was merged before opening; it had not moved from the branch base `a3c0da9`, so the merge was a no-op. objectui#13442 is in flight in the same batch on disjoint files (`packages/core/src/actions/**` and the `DataSource` type). The only shared surface is the `@object-ui/types` barrel, where this PR removes a single line in the widget block. ## Acceptance notes - Remote widgets as a product capability (a signed manifest and an allow-listed origin, decided in spec) are the maintainer's call and are not part of this card, per the triage ruling. - `register()` still accepts a manifest with a retired or unknown source, and the refusal happens at `load()`. That matches the ruling's "refuse an old manifest loudly in `default:`" and how the registry already treats a missing `registryKey`. --- _Generated by [Claude Code](https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 320ec35 commit 899f381

6 files changed

Lines changed: 194 additions & 52 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@object-ui/core': minor
3+
'@object-ui/types': minor
4+
---
5+
6+
**BREAKING:** the `module` widget source is retired. `WidgetRegistry` no longer loads a widget's code from a URL at runtime (objectui#13450).
7+
8+
`RuntimeWidgetSource` had a `module` member, `{ type: 'module', url, exportName? }`, and `WidgetRegistry.load()` resolved it with a dynamic `import()` built through the Function constructor so that bundlers could not see it. That is the runtime dynamic import AGENTS.md commandment #6 forbids ("No `eval()` / runtime dynamic imports to load components"), and it also needs `unsafe-eval` under a strict Content Security Policy. The member, its `WidgetSourceModule` interface and the loader arm are removed. `RuntimeWidgetSource` is now `WidgetSourceInline | WidgetSourceRegistry`.
9+
10+
An old manifest is refused loudly, not skipped. In TypeScript, `type: 'module'` no longer type-checks, and importing `WidgetSourceModule` is `TS2305`. A manifest that reaches `WidgetRegistry.load()` untyped (plain JavaScript, a cast, or an older `@object-ui/types`) is rejected before anything is fetched. The rejection names the retired `module` source and both replacements, `load()` emits `widget:error`, and `loadAll()` reports the error for that widget. Any other unknown source type is rejected the same way, and the message lists the supported types.
11+
12+
Migration, FROM → TO:
13+
14+
- FROM `source: { type: 'module', url: '/widgets/chart.js', exportName: 'Chart' }`
15+
- TO `import { Chart } from './widgets/chart';` and then `source: { type: 'inline', component: Chart }`. The component is imported at build time, so the bundler sees it.
16+
- OR register the component once with the component registry (`ComponentRegistry.register('chart', Chart)`) and declare `source: { type: 'registry', registryKey: 'chart' }`.
17+
18+
One-line fix: replace each `{ type: 'module', url }` with `{ type: 'inline', component }`, where `component` is imported at build time.
19+
20+
Remote widgets as a product capability would be a new contract (a signed manifest and an allow-listed origin, decided in `@objectstack/spec`). This release does not add one.
21+
22+
Clause-②: no (narrowing)
23+
Metadata-protocol: narrowed

‎packages/core/src/registry/WidgetRegistry.ts‎

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
* WidgetRegistry - Runtime widget management with auto-discovery.
1111
*
1212
* Provides registration, loading, and lookup of runtime widgets
13-
* described by RuntimeWidgetManifest objects. Widgets can be loaded from
14-
* ES module URLs, provided inline, or resolved from the component registry.
13+
* described by RuntimeWidgetManifest objects. A widget's component is either
14+
* provided inline (imported at build time) or resolved from the component
15+
* registry. Nothing is ever loaded from a URL: the former `module` source,
16+
* which did that through a runtime dynamic import, is retired (AGENTS.md
17+
* commandment #6, objectui#13450) and refused by name in `load()`.
1518
*
1619
* @module
1720
*/
@@ -40,14 +43,20 @@ export interface WidgetRegistryOptions {
4043
*
4144
* @example
4245
* ```ts
46+
* // The widget's React component. In an app it is imported at build time
47+
* // (e.g. `import { CustomChart } from './widgets/CustomChart'`).
48+
* function CustomChart() {
49+
* return null;
50+
* }
51+
*
4352
* const widgets = new WidgetRegistry({ componentRegistry: registry });
4453
*
4554
* widgets.register({
4655
* name: 'custom-chart',
4756
* version: '1.0.0',
4857
* type: 'chart',
4958
* label: 'Custom Chart',
50-
* source: { type: 'module', url: '/widgets/chart.js' },
59+
* source: { type: 'inline', component: CustomChart },
5160
* });
5261
*
5362
* const resolved = await widgets.load('custom-chart');
@@ -279,32 +288,32 @@ export class WidgetRegistry {
279288
return component;
280289
}
281290

282-
case 'module': {
283-
// Runtime-only dynamic import for loading widgets from external URLs
284-
// This uses Function constructor to prevent bundlers (Webpack/Turbopack/Vite)
285-
// from attempting static analysis at build time, which would fail since
286-
// source.url is only known at runtime.
287-
//
288-
// Security: Widget URLs must be from trusted sources only. Never pass
289-
// user-supplied URLs directly to RuntimeWidgetManifest. URLs should be validated
290-
// and controlled by the application developer.
291-
//
292-
// CSP Consideration: If your application uses strict Content Security Policy,
293-
// ensure dynamic imports are allowed or use 'inline' or 'registry' source types.
294-
const dynamicImport = new Function('url', 'return import(url)');
295-
const mod = await dynamicImport(source.url);
296-
const exportName = source.exportName ?? 'default';
297-
const component = mod[exportName];
298-
if (!component) {
291+
default: {
292+
// Exhaustive over `RuntimeWidgetSource`: a member added to the union
293+
// without an arm above fails to compile here. What still reaches this
294+
// line at runtime is a manifest that did not come through the type —
295+
// plain JavaScript, a cast, or one written against an older
296+
// `@object-ui/types` — and it is refused loudly, never skipped.
297+
const unhandled: never = source;
298+
const sourceType = (unhandled as { type?: unknown }).type;
299+
if (sourceType === 'module') {
300+
// The retired `module` source (objectui#13450). It loaded code from
301+
// `source.url` through a dynamic `import()` built with the Function
302+
// constructor — the runtime dynamic import AGENTS.md commandment #6
303+
// forbids. Refused by name so an old manifest says what to do.
299304
throw new Error(
300-
`Widget "${manifest.name}" module at "${source.url}" does not export "${exportName}"`,
305+
`Widget "${manifest.name}" uses the retired "module" source: loading widget code ` +
306+
`from a URL at runtime is no longer supported. Import the component at build ` +
307+
`time and declare { type: 'inline', component }, or register it in the ` +
308+
`component registry and declare { type: 'registry', registryKey }.`,
301309
);
302310
}
303-
return component;
311+
const shown = typeof sourceType === 'string' ? `"${sourceType}"` : String(sourceType);
312+
throw new Error(
313+
`Widget "${manifest.name}" has an unknown source type ${shown}; ` +
314+
`the supported source types are "inline" and "registry".`,
315+
);
304316
}
305-
306-
default:
307-
throw new Error(`Unknown widget source type for "${manifest.name}"`);
308317
}
309318
}
310319

‎packages/core/src/registry/__tests__/WidgetRegistry.test.ts‎

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import { describe, it, expect, vi, beforeEach } from 'vitest';
66
import { WidgetRegistry } from '../WidgetRegistry';
77
import { Registry } from '../Registry';
8-
import type { RuntimeWidgetManifest, WidgetRegistryEvent } from '@object-ui/types';
8+
import type {
9+
RuntimeWidgetManifest,
10+
RuntimeWidgetSource,
11+
WidgetRegistryEvent,
12+
} from '@object-ui/types';
913

1014
function createManifest(overrides: Partial<RuntimeWidgetManifest> = {}): RuntimeWidgetManifest {
1115
return {
@@ -318,4 +322,107 @@ describe('WidgetRegistry', () => {
318322
expect(stats.categories).toHaveLength(2);
319323
});
320324
});
325+
326+
describe('the retired `module` source (objectui#13450)', () => {
327+
/**
328+
* Runs `body` with the global Function constructor replaced by a proxy that
329+
* records every construction and call. The retired arm built its loader
330+
* with `new Function('url', 'return import(url)')`, so a refusal that still
331+
* reached that sink shows up here even if the import itself then failed.
332+
*/
333+
async function withFunctionSpy(body: () => Promise<void>): Promise<string[]> {
334+
const original = globalThis.Function;
335+
const calls: string[] = [];
336+
globalThis.Function = new Proxy(original, {
337+
construct(target, args) {
338+
calls.push(`new Function(${args.map(String).join(', ')})`);
339+
return Reflect.construct(target, args);
340+
},
341+
apply(target, thisArg, args) {
342+
calls.push(`Function(${args.map(String).join(', ')})`);
343+
return Reflect.apply(target, thisArg, args);
344+
},
345+
});
346+
try {
347+
await body();
348+
} finally {
349+
globalThis.Function = original;
350+
}
351+
return calls;
352+
}
353+
354+
it('refuses a manifest that still declares a `module` source, by name, without loading code', async () => {
355+
const componentRegistry = new Registry();
356+
const reg = new WidgetRegistry({ componentRegistry });
357+
reg.register(
358+
createManifest({
359+
type: 'retired-module-widget',
360+
// @ts-expect-error `module` is no longer a member of RuntimeWidgetSource.
361+
source: { type: 'module', url: '/widgets/chart.js' },
362+
}),
363+
);
364+
365+
let refusal: unknown;
366+
const functionCalls = await withFunctionSpy(async () => {
367+
refusal = await reg.load('test-widget').then(
368+
() => undefined,
369+
(err: unknown) => err,
370+
);
371+
});
372+
373+
expect(refusal).toBeInstanceOf(Error);
374+
const message = (refusal as Error).message;
375+
// The named subjects, not the wording: the widget, the retired source
376+
// type, and both replacements an author can move to.
377+
expect(message).toContain('"test-widget"');
378+
expect(message).toContain('"module"');
379+
expect(message).toContain("type: 'inline'");
380+
expect(message).toContain("type: 'registry'");
381+
// Nothing was built to fetch the URL, and nothing was registered.
382+
expect(functionCalls).toEqual([]);
383+
expect(reg.isLoaded('test-widget')).toBe(false);
384+
expect(componentRegistry.get('retired-module-widget')).toBeUndefined();
385+
});
386+
387+
it('emits widget:error for the refusal, and loadAll() reports it instead of skipping it', async () => {
388+
widgetRegistry.register(createManifest({ name: 'good', type: 'good' }));
389+
widgetRegistry.register(
390+
createManifest({
391+
name: 'old',
392+
type: 'old',
393+
source: { type: 'module', url: '/widgets/old.js' } as unknown as RuntimeWidgetSource,
394+
}),
395+
);
396+
const events: WidgetRegistryEvent[] = [];
397+
widgetRegistry.on((e) => events.push(e));
398+
399+
const results = await widgetRegistry.loadAll();
400+
401+
const old = results.find((r) => r.name === 'old');
402+
expect(old?.result).toBeInstanceOf(Error);
403+
expect((old?.result as Error).message).toContain('"module"');
404+
expect(results.find((r) => r.name === 'good')?.result).not.toBeInstanceOf(Error);
405+
expect(events.some((e) => e.type === 'widget:error' && e.name === 'old')).toBe(true);
406+
});
407+
408+
it('refuses any other unknown source type loudly, naming the supported ones', async () => {
409+
widgetRegistry.register(
410+
createManifest({
411+
source: { type: 'remote', url: 'https://cdn.example.com/w.mjs' } as unknown as RuntimeWidgetSource,
412+
}),
413+
);
414+
415+
const refusal = await widgetRegistry.load('test-widget').then(
416+
() => undefined,
417+
(err: unknown) => err,
418+
);
419+
420+
expect(refusal).toBeInstanceOf(Error);
421+
const message = (refusal as Error).message;
422+
expect(message).toContain('"remote"');
423+
expect(message).toContain('"inline"');
424+
expect(message).toContain('"registry"');
425+
expect(message).not.toContain('retired');
426+
});
427+
});
321428
});

‎packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ describe('renamed local dialects do not collide with a spec export (objectui#307
789789
['PageNodeRegionSchema', 'zod twin of PageNodeRegion'],
790790
// `MobileResponsiveConfig` retired with its type (objectui#7519).
791791
['RuntimeWidgetManifest', 'SDUI component manifest, not the field-widget plugin'],
792-
['RuntimeWidgetSource', 'objectui module/inline/registry loader union'],
792+
['RuntimeWidgetSource', 'objectui inline/registry loader union'],
793793
['InspectorFlowNode', "app-shell's flow-inspector read type, `id` REQUIRED"],
794794
['ScopeFlowNode', "app-shell's pre-validation scope probe, members `unknown`"],
795795
])('the spec does not own `%s` (%s)', (name) => {

‎packages/types/src/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,6 @@ export type {
15571557
export type {
15581558
RuntimeWidgetManifest,
15591559
RuntimeWidgetSource,
1560-
WidgetSourceModule,
15611560
WidgetSourceInline,
15621561
WidgetSourceRegistry,
15631562
WidgetInput,

‎packages/types/src/widget.ts‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import type { ComponentInputControlType } from './base.js';
2424
* `@object-ui/core`'s `WidgetRegistry` registers, and what the designer palette
2525
* lists.
2626
*
27-
* A manifest provides all metadata needed to discover, load, and render
28-
* a widget without requiring an upfront import of its code.
27+
* A manifest provides the metadata needed to list, register and render a
28+
* widget. Its code is never fetched by the manifest: the component is either
29+
* imported at build time and handed over (`inline`) or already registered in
30+
* the component registry (`registry`). See {@link RuntimeWidgetSource} for why
31+
* there is no URL-loaded source.
2932
*
3033
* Today this is objectui's ONLY widget-registration contract: no published
3134
* `@objectstack/spec` schema models one.
@@ -61,6 +64,12 @@ import type { ComponentInputControlType } from './base.js';
6164
*
6265
* @example
6366
* ```ts
67+
* // The widget's React component. In an app it is imported at build time
68+
* // (e.g. `import { CustomChart } from './widgets/CustomChart'`).
69+
* function CustomChart() {
70+
* return null;
71+
* }
72+
*
6473
* const manifest: RuntimeWidgetManifest = {
6574
* name: 'custom-chart',
6675
* version: '1.0.0',
@@ -69,7 +78,8 @@ import type { ComponentInputControlType } from './base.js';
6978
* description: 'A custom chart powered by D3.',
7079
* category: 'data-visualization',
7180
* icon: 'BarChart',
72-
* source: { type: 'module', url: '/widgets/custom-chart.js' },
81+
* // Imported at build time, so the bundler sees it; never a URL.
82+
* source: { type: 'inline', component: CustomChart },
7383
* };
7484
* ```
7585
*/
@@ -139,11 +149,25 @@ export interface RuntimeWidgetManifest {
139149
* the spec's `inline` carried source **code** to evaluate (`{ type: 'inline',
140150
* code: string }`), objectui's carries an **already-resolved component**
141151
* ({@link WidgetSourceInline}). The other members never overlapped — the spec
142-
* had `npm`/`remote`, objectui has `module`/`registry` — so a value of one
152+
* had `npm`/`remote`, objectui had `module`/`registry` — so a value of one
143153
* union was never a valid value of the other. ⚠️ The spec's union was retired
144154
* with the rest of the widget-registration vocabulary (objectstack#5055): the
145155
* union declared below is objectui's own, and never derived from it.
146156
*
157+
* RETIRED — the `module` member (`{ type: 'module', url, exportName? }`, the
158+
* `WidgetSourceModule` interface) was removed by objectui#13450. It loaded a
159+
* widget's code from a URL at runtime through a dynamic `import()` built with
160+
* the Function constructor, which AGENTS.md commandment #6 forbids ("No
161+
* `eval()` / runtime dynamic imports to load components"). There is no
162+
* tombstone member: a manifest written in TypeScript is refused here by the
163+
* discriminant, and one that reaches `WidgetRegistry.load()` untyped is refused
164+
* there by name, before any code is fetched. Migration: import the component
165+
* at build time and declare `{ type: 'inline', component }`, or register it
166+
* in the component registry and declare `{ type: 'registry', registryKey }`.
167+
* Remote widgets as a product capability would be a new contract (a signed
168+
* manifest and an allow-listed origin, decided in `@objectstack/spec`), not a
169+
* member of this union.
170+
*
147171
* The variant interfaces keep their `WidgetSource…` names: the spec never
148172
* exported those, and renaming them would churn the public surface without
149173
* removing a collision.
@@ -152,29 +176,9 @@ export interface RuntimeWidgetManifest {
152176
* longer owns `WidgetSource`" row pins the vacancy.
153177
*/
154178
export type RuntimeWidgetSource =
155-
| WidgetSourceModule
156179
| WidgetSourceInline
157180
| WidgetSourceRegistry;
158181

159-
/**
160-
* Load from an ES module URL.
161-
*
162-
* ⚠️ SECURITY WARNING: Only use URLs from trusted sources.
163-
* Never pass user-supplied URLs directly. URLs should be validated
164-
* and controlled by your application. This feature uses dynamic imports
165-
* which bypass static analysis and may be restricted by Content Security Policy.
166-
*/
167-
export interface WidgetSourceModule {
168-
type: 'module';
169-
/**
170-
* URL to the ES module (e.g., '/widgets/chart.js' or 'https://cdn.example.com/widget.mjs')
171-
* Must be from a trusted source - never user input.
172-
*/
173-
url: string;
174-
/** Named export to use (default: 'default') */
175-
exportName?: string;
176-
}
177-
178182
/** The component is provided inline (already loaded) */
179183
export interface WidgetSourceInline {
180184
type: 'inline';

0 commit comments

Comments
 (0)