From c16787a921b0d71ca1554b6fc11dae1a023c3ac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 02:37:02 +0000 Subject: [PATCH 1/2] chore(packages): declare `sideEffects` on every published package The 29 published manifests that did not declare the field now do, each from a read of its own modules: - `false` for react, auth, collaboration, data-objectstack, mobile, permissions and providers: no module registers, imports CSS or writes a global at load time. - An array, derived by `scripts/check-side-effects-array.mjs`, for the plugins whose registrations the gate can enumerate exactly. - `true` for plugin-ai, plugin-chatbot, plugin-grid, cli, create-plugin and runner. The consistency test learns the array gate's module-local write carve-out (`X.displayName = 'X'` on a binding the module declares), with pins in both directions, and its census grows to the new population. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Q4K9dtZAX6vw67etsxBeuP --- .changeset/13464-declare-side-effects.md | 36 +++++ packages/auth/package.json | 1 + packages/cli/package.json | 1 + packages/collaboration/package.json | 1 + packages/create-plugin/package.json | 1 + packages/data-objectstack/package.json | 1 + packages/mobile/package.json | 1 + packages/permissions/package.json | 1 + packages/plugin-ai/package.json | 1 + packages/plugin-calendar/package.json | 7 + packages/plugin-charts/package.json | 7 + packages/plugin-chatbot/package.json | 1 + packages/plugin-dashboard/package.json | 5 + packages/plugin-designer/package.json | 5 + packages/plugin-detail/package.json | 7 + packages/plugin-editor/package.json | 5 + packages/plugin-form/package.json | 5 + packages/plugin-gantt/package.json | 5 + packages/plugin-grid/package.json | 1 + packages/plugin-kanban/package.json | 6 + packages/plugin-list/package.json | 7 + packages/plugin-map/package.json | 5 + packages/plugin-markdown/package.json | 5 + packages/plugin-report/package.json | 5 + packages/plugin-timeline/package.json | 7 + packages/plugin-tree/package.json | 5 + packages/plugin-view/package.json | 5 + packages/providers/package.json | 1 + packages/react/package.json | 1 + packages/runner/package.json | 1 + ...de-effects-declaration-consistency.test.ts | 124 ++++++++++++++++-- 31 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 .changeset/13464-declare-side-effects.md diff --git a/.changeset/13464-declare-side-effects.md b/.changeset/13464-declare-side-effects.md new file mode 100644 index 0000000000..91d419f29b --- /dev/null +++ b/.changeset/13464-declare-side-effects.md @@ -0,0 +1,36 @@ +--- +"@object-ui/auth": patch +"@object-ui/cli": patch +"@object-ui/collaboration": patch +"@object-ui/create-plugin": patch +"@object-ui/data-objectstack": patch +"@object-ui/mobile": patch +"@object-ui/permissions": patch +"@object-ui/plugin-ai": patch +"@object-ui/plugin-calendar": patch +"@object-ui/plugin-charts": patch +"@object-ui/plugin-chatbot": patch +"@object-ui/plugin-dashboard": patch +"@object-ui/plugin-designer": patch +"@object-ui/plugin-detail": patch +"@object-ui/plugin-editor": patch +"@object-ui/plugin-form": patch +"@object-ui/plugin-gantt": patch +"@object-ui/plugin-grid": patch +"@object-ui/plugin-kanban": patch +"@object-ui/plugin-list": patch +"@object-ui/plugin-map": patch +"@object-ui/plugin-markdown": patch +"@object-ui/plugin-report": patch +"@object-ui/plugin-timeline": patch +"@object-ui/plugin-tree": patch +"@object-ui/plugin-view": patch +"@object-ui/providers": patch +"@object-ui/react": patch +"@object-ui/runner": patch +--- + +Clause-②: no +Metadata-protocol: none + +Every published package now declares `sideEffects`, so a host that bundles `@object-ui/*` can tree-shake the ones that do nothing on import. `@object-ui/react`, `auth`, `collaboration`, `data-objectstack`, `mobile`, `permissions` and `providers` declare `false`: no module in them registers, imports CSS or writes a global when it loads. Every `plugin-*` package except `plugin-ai`, `plugin-chatbot` and `plugin-grid` declares an array naming exactly its entry forms and the modules that register components at load time, so `import '@object-ui/plugin-x'` still registers everything it did before. `plugin-ai`, `plugin-chatbot`, `plugin-grid`, `cli`, `create-plugin` and `runner` declare `true` (objectui#13464). diff --git a/packages/auth/package.json b/packages/auth/package.json index 4043435e39..c17b1053e4 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -13,6 +13,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 3f46a6d494..4be59f52e5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -15,6 +15,7 @@ "bin": { "objectui": "./dist/cli.js" }, + "sideEffects": true, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index 1f5a2369fd..8ff344fa05 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -13,6 +13,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/create-plugin/package.json b/packages/create-plugin/package.json index dabb4d118a..03db814dc3 100644 --- a/packages/create-plugin/package.json +++ b/packages/create-plugin/package.json @@ -7,6 +7,7 @@ "bin": { "create-plugin": "./dist/index.js" }, + "sideEffects": true, "files": [ "dist", "README.md", diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json index 3f5bacaea4..c54aad8756 100644 --- a/packages/data-objectstack/package.json +++ b/packages/data-objectstack/package.json @@ -7,6 +7,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 25137d9c8d..9b0e7663c4 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -13,6 +13,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/permissions/package.json b/packages/permissions/package.json index 5ff7661c04..e41325dd23 100644 --- a/packages/permissions/package.json +++ b/packages/permissions/package.json @@ -13,6 +13,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-ai/package.json b/packages/plugin-ai/package.json index c8b9a8c6c3..ede40b408c 100644 --- a/packages/plugin-ai/package.json +++ b/packages/plugin-ai/package.json @@ -5,6 +5,7 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": true, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-calendar/package.json b/packages/plugin-calendar/package.json index fe70f996b9..b5678fb3d6 100644 --- a/packages/plugin-calendar/package.json +++ b/packages/plugin-calendar/package.json @@ -16,6 +16,13 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/calendar-view-renderer.js", + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/calendar-view-renderer.tsx", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-charts/package.json b/packages/plugin-charts/package.json index 8d0862eb68..371fc680ed 100644 --- a/packages/plugin-charts/package.json +++ b/packages/plugin-charts/package.json @@ -16,6 +16,13 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/ObjectChart.js", + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/ObjectChart.tsx", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-chatbot/package.json b/packages/plugin-chatbot/package.json index 64c1a082cd..6ef48f9028 100644 --- a/packages/plugin-chatbot/package.json +++ b/packages/plugin-chatbot/package.json @@ -16,6 +16,7 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": true, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index 24d28777e7..db32a1bb9a 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -7,6 +7,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index 0a65a65e4d..0af4fe57aa 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -7,6 +7,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index bf66a04bd6..2faa8a2b20 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -16,6 +16,13 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./dist/useRecordEditable.js", + "./src/index.tsx", + "./src/useRecordEditable.ts" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-editor/package.json b/packages/plugin-editor/package.json index 7c34144197..64998e6fac 100644 --- a/packages/plugin-editor/package.json +++ b/packages/plugin-editor/package.json @@ -16,6 +16,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index 5b4f4d3962..fe76c1a083 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -7,6 +7,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index 05e65da954..089122de11 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -16,6 +16,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-grid/package.json b/packages/plugin-grid/package.json index cea16f6175..db2ec3cdcd 100644 --- a/packages/plugin-grid/package.json +++ b/packages/plugin-grid/package.json @@ -7,6 +7,7 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": true, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json index 6c77da1216..9cc71260a5 100644 --- a/packages/plugin-kanban/package.json +++ b/packages/plugin-kanban/package.json @@ -16,6 +16,12 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.css", + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-list/package.json b/packages/plugin-list/package.json index 620e5d9c04..ac44e41590 100644 --- a/packages/plugin-list/package.json +++ b/packages/plugin-list/package.json @@ -16,6 +16,13 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/ObjectGallery.js", + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/ObjectGallery.tsx", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index 24edfc4b50..4f19cc13e4 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -16,6 +16,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-markdown/package.json b/packages/plugin-markdown/package.json index a5ab1ecfae..3f7c192644 100644 --- a/packages/plugin-markdown/package.json +++ b/packages/plugin-markdown/package.json @@ -16,6 +16,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-report/package.json b/packages/plugin-report/package.json index 8819eceb27..a8ae7905d4 100644 --- a/packages/plugin-report/package.json +++ b/packages/plugin-report/package.json @@ -5,6 +5,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index 7f98e9ba4a..0107d34c46 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -16,6 +16,13 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./dist/renderer.js", + "./src/index.tsx", + "./src/renderer.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index 25edf75363..9c778b84e5 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -16,6 +16,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index cb4e140ca9..bb01a8b928 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -7,6 +7,11 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/index.js", + "./dist/index.umd.cjs", + "./src/index.tsx" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/providers/package.json b/packages/providers/package.json index 4f704b1f2b..1e013d4cdc 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -16,6 +16,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/react/package.json b/packages/react/package.json index bd0c0cc5f7..5c54ffead7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -16,6 +16,7 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/runner/package.json b/packages/runner/package.json index 65e3c1c82c..883287a01a 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -63,6 +63,7 @@ "publishConfig": { "access": "public" }, + "sideEffects": true, "files": [ "dist", "README.md", diff --git a/scripts/__tests__/side-effects-declaration-consistency.test.ts b/scripts/__tests__/side-effects-declaration-consistency.test.ts index 619bb2b833..c62310618b 100644 --- a/scripts/__tests__/side-effects-declaration-consistency.test.ts +++ b/scripts/__tests__/side-effects-declaration-consistency.test.ts @@ -708,6 +708,44 @@ function impureNode(node: ts.Node): ts.Node | undefined { return hit; } +/** Every identifier declared at the top level of this source file (imports excluded). */ +function moduleScopeBindings(source: ts.SourceFile): Set { + const names = new Set(); + for (const stmt of source.statements) { + if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations) { + if (ts.isIdentifier(d.name)) names.add(d.name.text); + } + } else if ( + (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && + stmt.name && + ts.isIdentifier(stmt.name) + ) { + names.add(stmt.name.text); + } + } + return names; +} + +/** + * `X.displayName = 'X'` where `X` is declared in THIS module and the right-hand + * side calls nothing (objectui#13464). The write is provably module-local: a + * bundler that drops the module drops its target with it, so nothing outside can + * observe the difference. This is the same carve-out + * `scripts/check-side-effects-array.mjs` makes as `local-binding-write`, and + * without it a pure React library could never declare `false` here. A write to + * an IMPORTED binding or a global is still a finding, and so is any right-hand + * side that calls or constructs something. + */ +function isModuleLocalBindingWrite(stmt: ts.ExpressionStatement, localBindings: Set): boolean { + const expr = stmt.expression; + if (!ts.isBinaryExpression(expr) || expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return false; + if (!ts.isPropertyAccessExpression(expr.left)) return false; + const target = expr.left.expression; + if (!ts.isIdentifier(target) || !localBindings.has(target.text)) return false; + return impureNode(expr.right) === undefined; +} + /** Statement kinds that RUN when the module is evaluated (as opposed to declaring). */ function isExecutedStatement(stmt: ts.Statement): boolean { return ( @@ -746,6 +784,7 @@ function scanModule(absFile: string): ModuleScan { const effects: SideEffectStatement[] = []; const relativeSpecifiers: string[] = []; + const localBindings = moduleScopeBindings(source); for (const stmt of source.statements) { if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) { @@ -766,6 +805,7 @@ function scanModule(absFile: string): ModuleScan { // A bare string is a directive prologue (`'use client'`), not a statement // anything can observe. if (ts.isStringLiteral(stmt.expression)) continue; + if (isModuleLocalBindingWrite(stmt, localBindings)) continue; const hit = impureNode(stmt); if (hit) { effects.push({ @@ -1032,22 +1072,45 @@ describe('`sideEffects` declarations match load-time behaviour (objectui#3943)', it('discovers the declaring packages (guard cannot pass by finding nothing)', () => { // The hazard here is a SILENT one, so a guard that inspected an empty list // would reproduce it exactly: green output, nothing checked. These floors - // sit at the census re-measured for objectui#6683 — 9 packages declare the - // field, 5 of them `false` and 2 an array (`components`/`fields` declare - // `true`) — so a lost workspace root, a broken glob parser or a renamed + // sit at the census re-measured for objectui#13464, when every published + // package came to declare the field: 12 `false` and 18 arrays, the rest + // `true` — so a lost workspace root, a broken glob parser or a renamed // field is a failure and not a quiet pass. - expect(declaringPackages.length).toBeGreaterThanOrEqual(7); - expect(falsePackages.length).toBeGreaterThanOrEqual(5); - expect(arrayPackages.length).toBeGreaterThanOrEqual(2); + expect(declaringPackages.length).toBeGreaterThanOrEqual(30); + expect(falsePackages.length).toBeGreaterThanOrEqual(12); + expect(arrayPackages.length).toBeGreaterThanOrEqual(18); - // The census by name. `components`/`fields` declare `true` and are excluded - // on purpose: `true` is the conservative claim and can never lose a + // The census by name. `true` packages (`components`, `fields`, three + // plugins and the executables) are excluded on purpose: `true` is the conservative claim and can never lose a // registration, so it is not falsifiable in the direction that hurts. expect(declaringPackages.map((p) => p.name)).toEqual([ '@object-ui/app-shell', + '@object-ui/auth', + '@object-ui/collaboration', '@object-ui/core', + '@object-ui/data-objectstack', '@object-ui/i18n', '@object-ui/layout', + '@object-ui/mobile', + '@object-ui/permissions', + '@object-ui/plugin-calendar', + '@object-ui/plugin-charts', + '@object-ui/plugin-dashboard', + '@object-ui/plugin-designer', + '@object-ui/plugin-detail', + '@object-ui/plugin-editor', + '@object-ui/plugin-form', + '@object-ui/plugin-gantt', + '@object-ui/plugin-kanban', + '@object-ui/plugin-list', + '@object-ui/plugin-map', + '@object-ui/plugin-markdown', + '@object-ui/plugin-report', + '@object-ui/plugin-timeline', + '@object-ui/plugin-tree', + '@object-ui/plugin-view', + '@object-ui/providers', + '@object-ui/react', '@object-ui/react-runtime', '@object-ui/sdui-parser', '@object-ui/types', @@ -1384,6 +1447,51 @@ describe('`sideEffects` declarations match load-time behaviour (objectui#3943)', }); }); +/** + * The module-local write carve-out, pinned in both directions (objectui#13464). + * The carve-out exists so a pure React library can declare `false`; each + * neighbouring shape below must still be a finding, or the carve-out would have + * opened the objectui#3899 hole one statement wide. + */ +describe('a write to a binding this module declares is not a load-time side effect (objectui#13464)', () => { + const effectsOf = (code: string): string[] => { + const dir = mkdtempSync(path.join(tmpdir(), 'objectui-13464-scan-')); + try { + const file = path.join(dir, 'probe.tsx'); + writeFileSync(file, code); + return scanModule(file).effects.map((e) => e.kind); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }; + + it('`X.displayName = "X"` on a function, class or const this module declares is pure', () => { + expect(effectsOf("const X = () => null;\nX.displayName = 'X';")).toEqual([]); + expect(effectsOf("export function Y() { return null; }\nY.displayName = 'Y';")).toEqual([]); + expect(effectsOf("class Z {}\nZ.label = 'z';")).toEqual([]); + expect(effectsOf("export const W = {};\nW.label = `w`;")).toEqual([]); + }); + + it('the same write to an IMPORTED binding is still a top-level write', () => { + expect(effectsOf("import { X } from './x';\nX.displayName = 'X';")).toEqual(['top-level write']); + expect(effectsOf("import * as R from './r';\nR.flag = true;")).toEqual(['top-level write']); + }); + + it('a write to a global is still a top-level write', () => { + expect(effectsOf('window.installed = true;')).toEqual(['top-level write']); + expect(effectsOf('globalThis.flag = 1;')).toEqual(['top-level write']); + }); + + it('a local write whose right-hand side calls or constructs something is still a finding', () => { + expect(effectsOf("const X = {};\nX.label = make();")).toEqual(['top-level write']); + expect(effectsOf("const X = {};\nX.store = new Map();")).toEqual(['top-level write']); + }); + + it('only a property write is carved out: an element write on a local binding is still a finding', () => { + expect(effectsOf("const X = {};\nX['label'] = 'x';")).toEqual(['top-level write']); + }); +}); + /** * The probes. Everything above is static: it reads manifests and parses source. * None of it can answer the question the whole hazard turns on — does the From fcfc372400f201d568c92d6139e8a4c7ab06db3a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 02:50:16 +0000 Subject: [PATCH 2/2] chore(plugin-dashboard): declare `sideEffects: true` plugin-dashboard registers its retired node types in a loop, so the key set is not statically readable and check-sdui-registration-pins cannot pin it. `true` is the conservative claim for it. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01Q4K9dtZAX6vw67etsxBeuP --- .changeset/13464-declare-side-effects.md | 2 +- packages/plugin-dashboard/package.json | 6 +----- .../side-effects-declaration-consistency.test.ts | 9 ++++----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.changeset/13464-declare-side-effects.md b/.changeset/13464-declare-side-effects.md index 91d419f29b..e49f4c95d6 100644 --- a/.changeset/13464-declare-side-effects.md +++ b/.changeset/13464-declare-side-effects.md @@ -33,4 +33,4 @@ Clause-②: no Metadata-protocol: none -Every published package now declares `sideEffects`, so a host that bundles `@object-ui/*` can tree-shake the ones that do nothing on import. `@object-ui/react`, `auth`, `collaboration`, `data-objectstack`, `mobile`, `permissions` and `providers` declare `false`: no module in them registers, imports CSS or writes a global when it loads. Every `plugin-*` package except `plugin-ai`, `plugin-chatbot` and `plugin-grid` declares an array naming exactly its entry forms and the modules that register components at load time, so `import '@object-ui/plugin-x'` still registers everything it did before. `plugin-ai`, `plugin-chatbot`, `plugin-grid`, `cli`, `create-plugin` and `runner` declare `true` (objectui#13464). +Every published package now declares `sideEffects`, so a host that bundles `@object-ui/*` can tree-shake the ones that do nothing on import. `@object-ui/react`, `auth`, `collaboration`, `data-objectstack`, `mobile`, `permissions` and `providers` declare `false`: no module in them registers, imports CSS or writes a global when it loads. Every `plugin-*` package except `plugin-ai`, `plugin-chatbot`, `plugin-dashboard` and `plugin-grid` declares an array naming exactly its entry forms and the modules that register components at load time, so `import '@object-ui/plugin-x'` still registers everything it did before. `plugin-ai`, `plugin-chatbot`, `plugin-dashboard`, `plugin-grid`, `cli`, `create-plugin` and `runner` declare `true` (objectui#13464). diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index db32a1bb9a..17c4d7192e 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -7,11 +7,7 @@ "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", - "sideEffects": [ - "./dist/index.js", - "./dist/index.umd.cjs", - "./src/index.tsx" - ], + "sideEffects": true, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/scripts/__tests__/side-effects-declaration-consistency.test.ts b/scripts/__tests__/side-effects-declaration-consistency.test.ts index c62310618b..42c6ce1e74 100644 --- a/scripts/__tests__/side-effects-declaration-consistency.test.ts +++ b/scripts/__tests__/side-effects-declaration-consistency.test.ts @@ -1073,14 +1073,14 @@ describe('`sideEffects` declarations match load-time behaviour (objectui#3943)', // The hazard here is a SILENT one, so a guard that inspected an empty list // would reproduce it exactly: green output, nothing checked. These floors // sit at the census re-measured for objectui#13464, when every published - // package came to declare the field: 12 `false` and 18 arrays, the rest + // package came to declare the field: 12 `false` and 17 arrays, the rest // `true` — so a lost workspace root, a broken glob parser or a renamed // field is a failure and not a quiet pass. - expect(declaringPackages.length).toBeGreaterThanOrEqual(30); + expect(declaringPackages.length).toBeGreaterThanOrEqual(29); expect(falsePackages.length).toBeGreaterThanOrEqual(12); - expect(arrayPackages.length).toBeGreaterThanOrEqual(18); + expect(arrayPackages.length).toBeGreaterThanOrEqual(17); - // The census by name. `true` packages (`components`, `fields`, three + // The census by name. `true` packages (`components`, `fields`, four // plugins and the executables) are excluded on purpose: `true` is the conservative claim and can never lose a // registration, so it is not falsifiable in the direction that hurts. expect(declaringPackages.map((p) => p.name)).toEqual([ @@ -1095,7 +1095,6 @@ describe('`sideEffects` declarations match load-time behaviour (objectui#3943)', '@object-ui/permissions', '@object-ui/plugin-calendar', '@object-ui/plugin-charts', - '@object-ui/plugin-dashboard', '@object-ui/plugin-designer', '@object-ui/plugin-detail', '@object-ui/plugin-editor',