Skip to content

[RFC/Discussion] Migrate from Experimental Decorators to TC39 Stage 3 Decorators#18142

Draft
RaananW wants to merge 4 commits intoBabylonJS:masterfrom
RaananW:feature/tc39-decorators
Draft

[RFC/Discussion] Migrate from Experimental Decorators to TC39 Stage 3 Decorators#18142
RaananW wants to merge 4 commits intoBabylonJS:masterfrom
RaananW:feature/tc39-decorators

Conversation

@RaananW
Copy link
Copy Markdown
Member

@RaananW RaananW commented Mar 20, 2026

[RFC/Discussion] Migrate from Experimental Decorators to TC39 Stage 3 Decorators

This PR is a suggestion for open discussion. It is not intended to be merged as-is, but rather to serve as a proof-of-concept and conversation starter about moving Babylon.js to standard TC39 decorators.

Motivation

Babylon.js currently uses TypeScript's experimentalDecorators — a legacy implementation based on an early (now-abandoned) TC39 proposal. TypeScript 5.0+ ships native support for the TC39 Stage 3 decorator proposal, which has reached consensus and is being implemented by browsers and runtimes.

Benefits of Moving to TC39 Standard Decorators

  1. Future-proof: The experimentalDecorators flag is a legacy feature. The TC39 proposal is the standard path forward and will eventually be natively supported by all JavaScript engines.
  2. No compiler flag needed: TC39 decorators work out of the box in TypeScript 5.0+ without any tsconfig.json flag. This simplifies configuration for both the library and consumers.
  3. Better metadata model: Symbol.metadata provides a clean, per-class metadata storage mechanism that naturally follows the prototype chain — no more manual class-name-based maps.
  4. Improved encapsulation: The accessor keyword and ClassAccessorDecoratorResult provide a first-class way to intercept property access, replacing fragile Object.defineProperty hacks on prototypes.
  5. Ecosystem alignment: Lit 3.x, Stencil, and other major frameworks already support or prefer TC39 decorators. Aligning with the standard reduces friction for the broader ecosystem.
  6. Better tree-shaking potential: Standard decorators have clearer static semantics that bundlers can reason about more effectively.
  7. TypeScript transpilation: TypeScript fully transpiles TC39 decorators when targeting below ES2024. No browser native support is needed — the compiler emits __esDecorate/__runInitializers helpers and a Symbol.metadata polyfill automatically.

Breaking Changes

These are the changes that would affect external consumers of Babylon.js.

1. accessor keyword on serialized/expandable properties

All properties decorated with @expandToProperty (and related serialization decorators that use expandToProperty) now require the accessor keyword. This is a TC39 requirement for decorators that need to intercept get/set.

Before:

@expandToProperty("_dirtyLight", "_direction")
direction: Vector3;

After:

@expandToProperty("_dirtyLight", "_direction")
accessor direction: Vector3;

Impact: Any subclass that overrides these properties must also use accessor. The accessor keyword creates an auto-getter/setter pair, so direct field access patterns still work identically at runtime.

2. Symbol.metadata replaces class-name-based metadata maps

The old system stored decorator metadata in a global map keyed by getClassName(). The new system uses Symbol.metadata on each class constructor, which is the TC39 standard mechanism.

Impact: Code that directly accessed DecoratorInitialStore or MergedStore from decorators.functions.ts must migrate to the new GetDirectStore/GetMergedStore functions that read from Symbol.metadata. In practice, this is internal machinery — most consumers never touch these directly.

3. _propStore removed — use getEditableProperties() helper

The editableInPropertyPage decorator previously stored property metadata on a static _propStore property on each class. This has been replaced with Symbol.metadata-based storage.

Before:

const props = (block as any)._propStore;

After:

import { getEditableProperties } from "core/Decorators/nodeDecorator";
const props = getEditableProperties(block);

Impact: Any custom editor or tooling that reads _propStore must migrate to the new getEditableProperties() function. Similarly, Smart Filters consumers should use getSmartFilterEditableProperties().

4. UMD build target bumped from ES5 to ES2015

The accessor keyword requires at minimum ES2015. All 8 UMD tsconfig.build.json files were updated from "target": "ES5" to "target": "es2015".

Impact: The UMD bundles will no longer run in environments that only support ES5 (e.g., IE11). Given that Babylon.js already requires WebGL2/WebGPU, this is not expected to be a practical concern.

5. addAccessorsForMaterialProperty pattern change

The addAccessorsForMaterialProperty decorator is now an accessor decorator applied directly to the public property, rather than being called on a private backing property.

Before:

@addAccessorsForMaterialProperty("_albedoColor")
private _albedoColor: Color3;

public albedoColor: Color3; // auto-generated via Object.defineProperty

After:

@addAccessorsForMaterialProperty("_albedoColor")
accessor albedoColor: Color3;

private _albedoColor: Color3; // backing field declared explicitly

What Was Done

Core Decorator Rewrites

  • packages/dev/core/src/Misc/decorators.ts: Complete TC39 rewrite of all decorator functions (serialize*, expandToProperty, nativeOverride, addAccessorsForMaterialProperty). All accessor decorators are now generic (<This, V>) and return ClassAccessorDecoratorResult.
  • packages/dev/core/src/Misc/decorators.functions.ts: Rewritten to use Symbol.metadata with a __bjs_serializable__ key. GetMergedStore walks the metadata prototype chain with WeakMap caching.
  • packages/dev/core/src/Decorators/nodeDecorator.ts: Rewritten with __bjs_prop_store__ metadata key. New exported getEditableProperties(target) helper.
  • packages/dev/smartFilters/src/editorUtils/editableInPropertyPage.ts: Rewritten with __bjs_sf_prop_store__ metadata key. New getSmartFilterEditableProperties(target) helper.

Call-Site Updates (74 files, ~300 decorator usages)

  • 36 files with @expandToProperty: Added accessor keyword to all 294 usages.
  • openpbrMaterial.ts: All 66 @addAccessorsForMaterialProperty usages flipped to the new pattern.
  • 6 editor files: Updated from _propStore access to getEditableProperties() / getSmartFilterEditableProperties().

Framework-Specific Fixes

  • Lit (viewer): Added accessor keyword to all @property() / @state() / @query() decorated fields. Fixed @query types to use | null. Fixed environment property: moved @property() from getter to setter.
  • 3MF XML decorators: Rewrote XmlName, XmlAttr, XmlElem, XmlIgnore to use context.metadata with AddXmlMeta / GetXmlFieldMeta helpers.

TypeScript Configuration

  • tsconfig.build.json: Removed experimentalDecorators: true, added "esnext.decorators" and "es2022.object" to lib.
  • tsconfig.json: Removed experimentalDecorators: true.
  • tsconfig.test.json: Added "esnext.decorators" and "es2022.object" to lib.
  • 8 UMD tsconfig.build.json files: Changed target from ES5 to es2015.
  • Playground & Snippet Loader: Removed experimentalDecorators from Monaco/TypeScript compiler options.

Edge Cases Resolved

  • Class self-references in decorator arguments (TS2449): Three Node Material blocks (LightBlock, ReflectionTextureBaseBlock, PBRMetallicRoughnessBlock) referenced their own class in onValidation callbacks passed to decorators. Wrapped in arrow functions for deferred evaluation.
  • WebCamInputBlock: Class self-reference in options getter — similarly deferred.
  • imageProcessing.ts: Had a manual decorator call that bypassed the decorator API — replaced with direct metadata store access.
  • backgroundMaterial.ts / pbrSubSurfaceConfiguration.ts: Had @expandToProperty on explicit getters (not valid for accessor decorators) — restructured.
  • String.at() in GUI editor: Not available with current lib settings — replaced with bracket index access.
  • convertShaders.ts: Fixed pre-existing Dirent type issue unrelated to decorators.

Testing

Build Verification

  • build:es6 — Full NX build passes: 10 projects + 29 dependencies compiled successfully.
  • build:umd — Full NX build passes: 7 projects + 25 dependencies compiled successfully.
  • Core package — 0 TypeScript errors.

Unit Tests

  • New decorator test suite (packages/dev/core/test/unit/Decorators/decorators.test.ts):

    • @serialize* decorators correctly store metadata via Symbol.metadata
    • GetMergedStore properly walks the prototype chain (parent + child metadata merged)
    • @expandToProperty correctly intercepts get/set on accessor properties
    • Metadata isolation between classes verified
  • Runtime integration test (packages/dev/core/test/unit/Decorators/decorators.inline-test.ts):

    • Verifies full decorator lifecycle in a TSX-compiled context
    • Confirms expandToProperty auto-syncs backing properties
    • Confirms serialization metadata is accessible at runtime

What Still Needs Testing

  • Full visualization test suite (Playwright)
  • Runtime testing of all material types (PBR, Standard, Node Materials)
  • Serialization/deserialization round-trip tests
  • Inspector property grid with editable properties
  • Playground — verify user code compilation works without experimentalDecorators
  • Node Material Editor, GUI Editor, Node Geometry Editor functionality
  • Viewer web component lifecycle
  • 3MF serializer XML decorator round-trip
  • Performance benchmarks (decorator overhead comparison)
  • Test Babylon Native with native optimizations

Files Changed (74 files)

Click to expand full file list

Core decorator infrastructure:

  • packages/dev/core/src/Misc/decorators.ts
  • packages/dev/core/src/Misc/decorators.functions.ts
  • packages/dev/core/src/Decorators/nodeDecorator.ts

TypeScript configuration:

  • tsconfig.build.json
  • tsconfig.json
  • tsconfig.test.json
  • 8x packages/public/umd/*/tsconfig.build.json

Materials with @expandToProperty (36 files):

  • packages/dev/core/src/BakedVertexAnimation/bakedVertexAnimationManager.ts
  • packages/dev/core/src/Lights/light.ts
  • packages/dev/core/src/Materials/Background/backgroundMaterial.ts
  • packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingSolidColorMaterialPlugin.ts
  • packages/dev/core/src/Materials/PBR/openpbrMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrAnisotropicConfiguration.ts
  • packages/dev/core/src/Materials/PBR/pbrBRDFConfiguration.ts
  • packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrBaseSimpleMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrClearCoatConfiguration.ts
  • packages/dev/core/src/Materials/PBR/pbrIridescenceConfiguration.ts
  • packages/dev/core/src/Materials/PBR/pbrMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrMetallicRoughnessMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrSheenConfiguration.ts
  • packages/dev/core/src/Materials/PBR/pbrSpecularGlossinessMaterial.ts
  • packages/dev/core/src/Materials/PBR/pbrSubSurfaceConfiguration.ts
  • packages/dev/core/src/Materials/imageProcessing.ts
  • packages/dev/core/src/Materials/material.decalMapConfiguration.ts
  • packages/dev/core/src/Materials/material.detailMapConfiguration.ts
  • packages/dev/core/src/Materials/meshDebugPluginMaterial.ts
  • packages/dev/core/src/Materials/standardMaterial.ts
  • packages/dev/core/src/Rendering/GlobalIllumination/giRSMManager.ts
  • packages/dev/core/src/Rendering/IBLShadows/iblShadowsPluginMaterial.ts
  • packages/dev/core/src/Rendering/reflectiveShadowMap.ts
  • packages/dev/gui/src/3D/materials/fluent/fluentMaterial.ts
  • packages/dev/materials/src/cell/cellMaterial.ts
  • packages/dev/materials/src/fire/fireMaterial.ts
  • packages/dev/materials/src/fur/furMaterial.ts
  • packages/dev/materials/src/gradient/gradientMaterial.ts
  • packages/dev/materials/src/grid/gridMaterial.ts
  • packages/dev/materials/src/lava/lavaMaterial.ts
  • packages/dev/materials/src/mix/mixMaterial.ts
  • packages/dev/materials/src/normal/normalMaterial.ts
  • packages/dev/materials/src/simple/simpleMaterial.ts
  • packages/dev/materials/src/terrain/terrainMaterial.ts
  • packages/dev/materials/src/triPlanar/triPlanarMaterial.ts
  • packages/dev/materials/src/water/waterMaterial.ts

Node Material blocks:

  • packages/dev/core/src/Materials/Node/Blocks/Dual/lightBlock.ts
  • packages/dev/core/src/Materials/Node/Blocks/Dual/reflectionTextureBaseBlock.ts
  • packages/dev/core/src/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.ts

Editor/tooling consumers:

  • packages/dev/sharedUiComponents/src/nodeGraphSystem/graphNode.ts
  • 5x */genericNodePropertyComponent.tsx (node editor, geometry editor, render graph editor, particle editor, smart filters editor)
  • packages/tools/smartFiltersEditorControl/src/properties/inputNodePropertyComponent.tsx

3MF serializer:

  • packages/dev/serializers/src/3MF/core/xml/xml.interfaces.ts

Smart Filters:

  • packages/dev/smartFilters/src/editorUtils/editableInPropertyPage.ts
  • packages/dev/smartFilters/src/blockFoundation/customShaderBlock.ts
  • packages/dev/smartFilters/src/utils/buildTools/convertShaders.ts
  • packages/dev/smartFilters/test/unit/customShaderBlock.test.ts

Viewer (Lit web components):

  • packages/tools/viewer/src/viewerElement.ts
  • packages/tools/viewer/src/viewerAnnotationElement.ts

Playground / Snippet Loader:

  • packages/tools/playground/src/tools/monaco/ts/tsPipeline.ts
  • packages/tools/snippetLoader/src/snippetLoader.ts

GUI Editor:

  • packages/tools/guiEditor/src/components/propertyTab/propertyGrids/gui/gridPropertyGridComponent.tsx

Tests (new):

  • packages/dev/core/test/unit/Decorators/decorators.test.ts
  • packages/dev/core/test/unit/Decorators/decorators.inline-test.ts

Discussion Points

  1. Is now the right time? TC39 decorators are stable in TypeScript 5.0+ and browsers are beginning to ship native support. But the migration is invasive.
  2. Breaking change tolerance: The accessor keyword change affects any code subclassing Babylon.js materials. How should we communicate/version this?
  3. UMD ES5 to ES2015: Is dropping ES5 for UMD builds acceptable? (The main es6 build is already es2021.)
  4. Performance: Are there any measurable performance differences? The transpiled decorator helpers are slightly different in shape.
  5. Phased approach? Could we migrate incrementally (e.g., new code only) or is it all-or-nothing due to the metadata system change?

This PR was created as a proof-of-concept to facilitate discussion. All feedback welcome.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@RaananW RaananW force-pushed the feature/tc39-decorators branch from 814ab01 to 59b6c5f Compare March 20, 2026 12:55
@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Snapshot stored with reference name:
refs/pull/18142/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18142/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18142/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/18142/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18142/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18142/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18142/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/18142/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Smart Filters Editor is available to test at:
https://sfe.babylonjs.com/?snapshot=refs/pull/18142/merge

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Building or testing the playground has failed.

If the tests failed, results can be found here:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/PLAYGROUND/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Building or testing the sandbox has failed.

If the tests failed, results can be found here:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/SANDBOX/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Graph tools CI has failed you can find the test results at:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/TOOLS/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Smart Filters Editor is available to test at:
https://sfe.babylonjs.com/?snapshot=refs/pull/18142/merge

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Building or testing the playground has failed.

If the tests failed, results can be found here:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/PLAYGROUND/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Building or testing the sandbox has failed.

If the tests failed, results can be found here:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/SANDBOX/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

Graph tools CI has failed you can find the test results at:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/TOOLS/refs/pull/18142/merge/testResults/

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@bjsplat
Copy link
Copy Markdown
Collaborator

bjsplat commented Mar 20, 2026

@sebavan
Copy link
Copy Markdown
Member

sebavan commented Mar 27, 2026

Love the idea !!! cc @bghgary and @ryantrem for the babylon native part.

RaananW added 4 commits March 30, 2026 11:29
- Rewrite all decorator definitions for TC39 (decorators.ts, decorators.functions.ts, nodeDecorator.ts)
- Use Symbol.metadata for per-class metadata storage
- Add 'accessor' keyword to all @expandToProperty and @addAccessorsForMaterialProperty properties
- Flip addAccessorsForMaterialProperty pattern: decorator on public accessor
- Rewrite 3MF XML decorators and Smart Filters decorators for TC39
- Update Lit viewer components with accessor keyword for @property/@state/@query
- Remove experimentalDecorators from all tsconfig files
- Add esnext.decorators and es2022.object to lib
- Bump UMD build targets from ES5 to ES2015 (accessor requires it)
- Replace _propStore access with getEditableProperties() helper
- Add unit tests for decorator metadata and expandToProperty
- Fix class self-references in decorator arguments (TS2449)
- Remove experimentalDecorators from Playground/Snippet Loader compiler options
- Resolve merge conflicts in convertShaders.ts and tsconfig.build.json
- Add useDefineForClassFields: false to prevent class field override issues
- Add 'declare' to type-narrowing _effectWrapper overrides in 18 PostProcess
  subclasses to prevent TC39 decorator lowering from emitting void 0 assignments
  that overwrite parent constructor values
- Rename getEditableProperties/getSmartFilterEditableProperties to PascalCase
  per repo naming convention
- Add eslint-disable for internal double-underscore metadata keys
- Fix test assertion in decorators.test.ts (property stored under key name, not
  sourceName)
- Add Symbol.metadata polyfill to vitest.setup.ts for Node.js compatibility
- Fix missing curly braces in xml.interfaces.ts
- Update customShaderBlock.test.ts: className is empty string with TC39
  decorators (no target available in decorator context)
…ide for type-narrowing fields, fix barrel import, bump dictionary mode threshold
@RaananW RaananW force-pushed the feature/tc39-decorators branch from da742fe to a999fe7 Compare March 30, 2026 10:52
@bghgary
Copy link
Copy Markdown
Contributor

bghgary commented Mar 31, 2026

  1. UMD build target bumped from ES5 to ES2015
    The accessor keyword requires at minimum ES2015. All 8 UMD tsconfig.build.json files were updated from "target": "ES5" to "target": "es2015".

Impact: The UMD bundles will no longer run in environments that only support ES5 (e.g., IE11). Given that Babylon.js already requires WebGL2/WebGPU, this is not expected to be a practical concern.

I'm assuming babel can still take the results to translate to ES5 if necessary?

@RaananW
Copy link
Copy Markdown
Member Author

RaananW commented Mar 31, 2026

  1. UMD build target bumped from ES5 to ES2015
    The accessor keyword requires at minimum ES2015. All 8 UMD tsconfig.build.json files were updated from "target": "ES5" to "target": "es2015".

Impact: The UMD bundles will no longer run in environments that only support ES5 (e.g., IE11). Given that Babylon.js already requires WebGL2/WebGPU, this is not expected to be a practical concern.

I'm assuming babel can still take the results to translate to ES5 if necessary?

It should be possible. I can try finding the babel rule for that.

@bghgary
Copy link
Copy Markdown
Contributor

bghgary commented Mar 31, 2026

  1. UMD build target bumped from ES5 to ES2015
    The accessor keyword requires at minimum ES2015. All 8 UMD tsconfig.build.json files were updated from "target": "ES5" to "target": "es2015".

Impact: The UMD bundles will no longer run in environments that only support ES5 (e.g., IE11). Given that Babylon.js already requires WebGL2/WebGPU, this is not expected to be a practical concern.

I'm assuming babel can still take the results to translate to ES5 if necessary?

It should be possible. I can try finding the babel rule for that.

We already do this in a number of places when building a bundle, so now we will have to do this for the UMD files also.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants