Repository navigation
feat(core,types)!: retire the module widget source and its runtime dynamic import (objectui#13450) - #13473
Conversation
…dynamic import
WidgetRegistry.load() resolved a `{ type: 'module', url }` source with
`new Function('url', 'return import(url)')`, the runtime dynamic import
AGENTS.md commandment #6 forbids. Remove the `module` member of
RuntimeWidgetSource, the WidgetSourceModule interface and the loader
arm. The `default:` arm is now exhaustive over the union and refuses an
untyped old manifest by name (naming both replacements) before anything
is fetched; any other unknown source type is refused with the supported
types listed.
Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ
Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
The retirement rewrote the RuntimeWidgetManifest and WidgetRegistry JSDoc examples to import CustomChart from a relative './widgets/CustomChart' path. The JSDoc example gate (check-doc-example-types.mjs) compiles each block in isolation, where that module does not exist: TS2307 made the types example an undeclared failure and drifted the core example's ledger row (declared TS2304, produced TS2304 + TS2307). Both examples now define a trivial local CustomChart component and name the build-time import in a comment, so the types example compiles and the core example fails exactly as its existing ledger row declares. Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
The retirement removes the module member of a published union and an export, so the clause line must name the narrowing arm; a bare no declares no direction. Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: narrowing, declared ① Derived judgments
② Semver level
③ Boundary flagsReport 6387223880 (implementation round):
Report 6393016217 (patch round):
Head move CI on this head ( Implemented-by: Self-review by session: the implementer was a subagent of this same session; this review was rendered in an isolated context from the card, the PR, the diff and the check-runs alone, not from the dispatch order or the seat's conclusions. VERDICT: PASS Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
席位核对ACCEPT —
Generated by Claude Code |
Fixes #13450
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Retires the
modulewidget source.WidgetRegistry.load()resolved{ type: 'module', url }withnew Function('url', 'return import(url)'). That is the runtime dynamic import AGENTS.md commandment#6forbids ("Noeval()/ runtime dynamic imports to load components"). The claim is the PM's comment6386913269on the card, for branchclaude/issue-13450-retire-widget-module-source, sessionhttps://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ.What changes
@object-ui/types: themodulemember and itsWidgetSourceModuleinterface are removed.RuntimeWidgetSourceis nowWidgetSourceInline | WidgetSourceRegistry, and the barrel no longer exportsWidgetSourceModule. The union's docblock records the retirement and the migration. The manifest's JSDoc@examplenow uses{ type: 'inline', component }with a locally definedCustomChartcomponent (the build-time import is named in a comment, so the block compiles in isolation undercheck-doc-example-types) instead of a URL.@object-ui/core: thecase 'module'arm and its Function-constructor sink are removed fromWidgetRegistry.resolveComponent(). Thedefault:arm is now exhaustive (const unhandled: never = source), so a union member added without an arm fails to compile.load()withtype: '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.load()emitswidget:errorandloadAll()reports the error. The class docblock and its example no longer mention URL loading..changeset/13450-retire-widget-module-source.md:minorfor core and types. objectui never declaresmajor(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 theClause-②:/Metadata-protocol:lines.page-nav-misc-spec-parity.test.ts: the reason string in oneit.eachrow said "module/inline/registry loader union" and now says "inline/registry". It is a test title, not an assertion.Migration (FROM → TO)
source: { type: 'module', url: '/widgets/chart.js', exportName: 'Chart' }import { Chart } from './widgets/chart'andsource: { type: 'inline', component: Chart }(imported at build time, so the bundler sees it)ComponentRegistry.register('chart', Chart)once, andsource: { 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 ata3c0da9. All four are the generatedpackage.json's ESM marker ("type": "module"), not a widget manifest source. Deleting them would make the scaffolded packages CommonJS and break theirimportsyntax. None was changed:buildAppPackageJsoninpackages/cli/src/utils/app-generator.tspackage.json(name,private,type, deps)buildRoutedAppPackageJsonin the same filepackage.jsonbuildInitPackageJsoninpackages/cli/src/commands/init.tsobjectui initprojectpackage.json(scripts.dev: 'vite')buildPackageJsoninpackages/create-plugin/src/templates.tspackage.json(main,module,exports)So
@object-ui/cliand@object-ui/create-plugincarry no change and no changeset, which narrows the route. No widget-manifest producer of amodulesource exists in this tree. Agit grepforWidgetSourceModulebefore this change hit only its declaration and the barrel.apps/console/srcandpackages/app-shell/srcconstruct nomodulesource (triage's reading).objectstackandhotcrmreference neitherWidgetRegistrynorRuntimeWidgetSourcein code (the onlyobjectstackhit is a doc paragraph that does not name themodulesource). Out-of-tree consumers are NOT MEASURED.No user-facing doc names the
modulewidget source.content/docs, the package READMEs anddocs/were checked withgit grep; the controlComponentRegistryhits incontent/docs. The two JSDoc examples were the only documentation of it, and both are rewritten.Tests (head
e15036c)be01ada(patch round):check-doc-example-typesexit 1 → 0 (34 → 35 compile;WidgetRegistry #1back on its TS2304 ledger row). JSDoc text only.New
describe('the retired \module` source (objectui#13450)')inpackages/core/src/registry/tests/WidgetRegistry.test.ts`:source: { type: 'module', url }is refused. The rejection is anErrorthat names"test-widget","module",type: 'inline'andtype: 'registry'; these are the named subjects, not the wording. The globalFunctionconstructor, 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'stype-checkrunstsconfig.test.json, which includessrc/**/*.test.ts.loadAll()reports the refusal for that widget, a sibling still loads, andwidget:errorfires.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: bothDone, exit 0. This ran after both packages were rebuilt, so core's test program read the newdist/*.d.ts.Ablation, runtime (one-shot, through
ablation-replace.mjs, restored to the HEAD blob): acase 'module'arm with the Function-constructor import was re-planted ahead ofdefault:. 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 HEADwas 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 toRuntimeWidgetSourceand@object-ui/typesrebuilt; the dist marker count was 1. Core'stsc -p tsconfig.test.jsonthen reportedTS2578: Unused '@ts-expect-error' directiveon the new test andTS2322 ... is not assignable to type 'never'on the exhaustive arm. After the restore and a rebuild, the dist marker count was 0 and the sametscwas 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-existingno-explicit-any, and none is on a changed line.check:new-line-citations0 ·check:control-bytes0 ·check-changeset-presence0 ·changeset:check(fixed + no-major) 0 ·check:changeset-claims0 ·check:pending-changeset-literals0 ·check:spec-symbols0 ·check:unreferenced-sources0 ·check:doc-example-readers0 ·check:shell-escape-residue0 ·check:test-path-roots0check: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 with0 fabricated, 0 wrong-path. No README namesWidgetSourceModule.check:published-dist: exit 1, NOT MEASURED. Its internal whole-workspaceturbo run buildhit a heap OOM in@object-ui/app-shell, so it judged nothing. That is environmental and not this diff. CI owns this gate.origin/mainwas merged before opening; it had not moved from the branch basea3c0da9, so the merge was a no-op. objectui#13442 is in flight in the same batch on disjoint files (packages/core/src/actions/**and theDataSourcetype). The only shared surface is the@object-ui/typesbarrel, where this PR removes a single line in the widget block.Acceptance notes
register()still accepts a manifest with a retired or unknown source, and the refusal happens atload(). That matches the ruling's "refuse an old manifest loudly indefault:" and how the registry already treats a missingregistryKey.Generated by Claude Code