Skip to content

Commit c9a4db9

Browse files
RaananWCopilot
andcommitted
TC39 decorators: run Symbol.metadata polyfill before decorated classes in all entry styles
The es6 `pure` entry (and any deep import of a decorated class) crashed at class-definition time with "Cannot convert undefined or null to object" because `context.metadata` was `void 0`: TypeScript's TC39 emit computes `_metadata = Symbol.metadata ? ... : void 0` at the top of the class `static {}` block, so the metadata bag only exists if `Symbol.metadata` was installed BEFORE the class module evaluated. The polyfill was only imported by `index.ts` (the barrel), so the `pure` barrel — which eagerly evaluates every decorated FrameGraph / node block without importing the polyfill — always threw. Fix: anchor the polyfill in the decorator infrastructure that every decorated class already imports. `decorators.functions.ts` now resolves `Symbol.metadata` into an exported module-load `const MetadataSymbol` (the idempotent `GetMetadataSymbol()` already performed the polyfill); ES module evaluation order guarantees this runs before any importing class body. It is a `const` referenced by the retained decorator helpers so bundlers treating the module as side-effect-free cannot drop it, and the tree-shaking side-effect detector skips `const X = ...` initializers so the module stays pure and remains importable from `.pure` modules — no tooling policy or `sideEffects` allowlist change required. `nodeDecorator.ts` and the smart filters `editableInPropertyPage` import that symbol, route their metadata reads through it, and throw an actionable error instead of a cryptic TypeError if the polyfill somehow did not run. Fixes the ES6 visualization "pure" scenes and the resulting Playground snapshot cascade. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 30e155d commit c9a4db9

3 files changed

Lines changed: 54 additions & 9 deletions

File tree

packages/dev/core/src/Decorators/nodeDecorator.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type Nullable } from "../types";
22
import { type Scene } from "../scene";
3+
import { MetadataSymbol } from "../Misc/decorators.functions";
34

45
/**
56
* Enum defining the type of properties that can be edited in the property pages in the node editor
@@ -108,7 +109,16 @@ export function editableInPropertyPage(
108109
options?: IEditablePropertyOption
109110
) {
110111
return (_value: unknown, context: { name: string | symbol; metadata: DecoratorMetadataObject }) => {
111-
const meta = context.metadata;
112+
const meta = context.metadata as DecoratorMetadataObject | undefined;
113+
if (!meta) {
114+
// `context.metadata` is `void 0` when `Symbol.metadata` was not installed before this class
115+
// was evaluated. Importing `MetadataSymbol` from decorators.functions runs the polyfill at
116+
// module load (before this class body), so this should never happen; referencing it here also
117+
// keeps that module-load polyfill anchored against bundler tree-shaking.
118+
throw new Error(
119+
`editableInPropertyPage: decorator metadata is unavailable; the Symbol.metadata (${String(MetadataSymbol)}) polyfill must run before decorated classes are evaluated.`
120+
);
121+
}
112122
let propStore: IPropertyDescriptionForEdition[];
113123
if (Object.prototype.hasOwnProperty.call(meta, __bjsPropStoreKey)) {
114124
propStore = meta[__bjsPropStoreKey] as IPropertyDescriptionForEdition[];
@@ -139,7 +149,7 @@ export const __bjsPropStoreKey = "__bjs_prop_store__";
139149
*/
140150
export function GetEditableProperties(target: any): IPropertyDescriptionForEdition[] {
141151
const ctor = typeof target === "function" ? target : target?.constructor;
142-
const metadata: DecoratorMetadataObject | undefined = ctor?.[Symbol.metadata];
152+
const metadata: DecoratorMetadataObject | undefined = ctor?.[MetadataSymbol];
143153
if (!metadata) {
144154
return [];
145155
}

packages/dev/core/src/Misc/decorators.functions.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ function GetMetadataSymbol(): symbol {
3535
return metadataSymbol;
3636
}
3737

38+
/**
39+
* The well-known `Symbol.metadata` symbol, resolved once at module-evaluation time.
40+
*
41+
* Reading (and thereby resolving) this symbol at module load guarantees that `Symbol.metadata`
42+
* exists BEFORE any decorated class which imports the decorator infrastructure evaluates its class
43+
* body. TypeScript's TC39 decorator emit captures `context.metadata` from `Symbol.metadata` at the
44+
* top of the class `static {}` block (before the decorator factory runs), so if the symbol is not
45+
* yet installed the metadata object is `void 0` and every metadata-based decorator throws.
46+
*
47+
* It is intentionally an exported `const` initialized from a function call: bundlers that treat this
48+
* module as side-effect-free may drop bare top-level calls, but they cannot drop a `const` whose
49+
* value is referenced by the retained decorator helpers below. The tree-shaking side-effect detector
50+
* likewise skips `const X = ...` initializers, so this module stays classified as pure and remains
51+
* importable from `.pure` modules.
52+
* @internal
53+
*/
54+
export const MetadataSymbol: symbol = GetMetadataSymbol();
55+
3856
// Returns the constructor for the provided decorator/serialization target.
3957
// Experimental decorators pass a prototype; serialization passes an instance or a constructor.
4058
function GetConstructor(target: any): any {
@@ -47,17 +65,17 @@ function GetOwnMetadata(ctor: any): any {
4765
if (!ctor) {
4866
return undefined;
4967
}
50-
if (!HasOwn(ctor, GetMetadataSymbol())) {
68+
if (!HasOwn(ctor, MetadataSymbol)) {
5169
const parent = Object.getPrototypeOf(ctor);
52-
const parentMetadata = parent ? parent[GetMetadataSymbol()] : null;
53-
Object.defineProperty(ctor, GetMetadataSymbol(), {
70+
const parentMetadata = parent ? parent[MetadataSymbol] : null;
71+
Object.defineProperty(ctor, MetadataSymbol, {
5472
value: Object.create(parentMetadata ?? null),
5573
configurable: true,
5674
writable: true,
5775
enumerable: false,
5876
});
5977
}
60-
return ctor[GetMetadataSymbol()];
78+
return ctor[MetadataSymbol];
6179
}
6280

6381
/**
@@ -66,6 +84,13 @@ function GetOwnMetadata(ctor: any): any {
6684
* @internal
6785
*/
6886
export function GetDirectStoreFromMetadata(metadata: DecoratorMetadataObject): Record<string, any> {
87+
if (!metadata) {
88+
// `metadata` is `context.metadata`, which is `void 0` when `Symbol.metadata` was not installed
89+
// before the class was evaluated. Referencing `MetadataSymbol` here (a) produces an actionable
90+
// error instead of a cryptic "Cannot convert undefined to object" and (b) keeps the module-load
91+
// polyfill anchored so bundlers cannot tree-shake it away on the decorate-time serialize path.
92+
throw new Error(`Decorator metadata is unavailable; the Symbol.metadata (${String(MetadataSymbol)}) polyfill must run before decorated classes are evaluated.`);
93+
}
6994
if (!HasOwn(metadata, __bjsSerializableKey)) {
7095
(metadata as any)[__bjsSerializableKey] = {};
7196
}
@@ -90,7 +115,7 @@ export function GetDirectStore(target: any): any {
90115
*/
91116
export function GetMergedStore(target: any): any {
92117
const ctor = GetConstructor(target);
93-
const metadata = ctor ? ctor[GetMetadataSymbol()] : undefined;
118+
const metadata = ctor ? ctor[MetadataSymbol] : undefined;
94119
if (!metadata) {
95120
return {};
96121
}

packages/dev/smartFilters/src/editorUtils/editableInPropertyPage.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type Observable } from "core/Misc/observable.js";
2+
import { MetadataSymbol } from "core/Misc/decorators.functions.js";
23

34
/**
45
* Enum defining the type of properties that can be edited in the property pages in the node editor
@@ -97,7 +98,16 @@ export function EditableInPropertyPage(
9798
options?: IEditablePropertyOption
9899
) {
99100
return (_value: unknown, context: { name: string | symbol; metadata: DecoratorMetadataObject }) => {
100-
const meta = context.metadata;
101+
const meta = context.metadata as DecoratorMetadataObject | undefined;
102+
if (!meta) {
103+
// `context.metadata` is `void 0` when `Symbol.metadata` was not installed before this class
104+
// was evaluated. Importing `MetadataSymbol` from core runs the polyfill at module load (before
105+
// this class body), so this should never happen; referencing it here also keeps that
106+
// module-load polyfill anchored against bundler tree-shaking.
107+
throw new Error(
108+
`EditableInPropertyPage: decorator metadata is unavailable; the Symbol.metadata (${String(MetadataSymbol)}) polyfill must run before decorated classes are evaluated.`
109+
);
110+
}
101111
let propStore: IPropertyDescriptionForEdition[];
102112
if (Object.prototype.hasOwnProperty.call(meta, __bjsSmartFilterPropStoreKey)) {
103113
propStore = meta[__bjsSmartFilterPropStoreKey] as IPropertyDescriptionForEdition[];
@@ -136,7 +146,7 @@ export function EditableInPropertyPage(
136146
*/
137147
export function GetSmartFilterEditableProperties(target: any): IPropertyDescriptionForEdition[] {
138148
const ctor = typeof target === "function" ? target : target?.constructor;
139-
const metadata: DecoratorMetadataObject | undefined = ctor?.[Symbol.metadata];
149+
const metadata: DecoratorMetadataObject | undefined = ctor?.[MetadataSymbol];
140150
if (!metadata) {
141151
return [];
142152
}

0 commit comments

Comments
 (0)