Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

fix(proposal-decorators): only compile decorated fields with legacy decorators#18125

Open
Develop-KIM wants to merge 3 commits into
babel:mainbabel/babel:mainfrom
Develop-KIM:legacy-decorators-only-compile-decorated-fieldsDevelop-KIM/babel:legacy-decorators-only-compile-decorated-fieldsCopy head branch name to clipboard
Open

fix(proposal-decorators): only compile decorated fields with legacy decorators#18125
Develop-KIM wants to merge 3 commits into
babel:mainbabel/babel:mainfrom
Develop-KIM:legacy-decorators-only-compile-decorated-fieldsDevelop-KIM/babel:legacy-decorators-only-compile-decorated-fieldsCopy head branch name to clipboard

Conversation

@Develop-KIM

@Develop-KIM Develop-KIM commented Jul 9, 2026

Copy link
Copy Markdown
Q A
Fixed Issues? Fixes #14917
Patch: Bug Fix? Yes
Major: Breaking Change? No
Minor: New Feature? No
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes? No
License MIT

Legacy decorators depend on the class-properties transform to lower decorated
fields: a decorated field is emitted as an _initializerWarningHelper(...)
marker that class-properties turns into an _initializerDefineProperty call in
the constructor. When class-properties is enabled it lowers every field,
private element and method of the class. When it is not enabled (e.g.
targeting an engine with native class fields), the marker survives and throws at
runtime.

This makes legacy decorators work without the class-properties transform, along
the lines @nicolo-ribaudo suggested in the issue:

  • A decorated instance field is lowered into the constructor (via
    initializerDefineProperty), so a decorator that replaces the field with a
    prototype accessor keeps working — a native field initializer would create an
    own data property that shadows the prototype getter. This is covered by the
    new getter-decorator fixture.
  • Native field initializers run before the constructor body, so an undecorated
    field that follows a decorated one would otherwise initialize too early. To
    preserve the observable initialization order, every instance field after the
    first decorated one (public and private) is relocated into the constructor in
    source order. Fields before the first decorated field, methods and static
    members stay as native class syntax. When a computed key would need to move
    (its evaluation time would change), we keep the class-properties handshake.
  • When the class-properties transform is enabled, the existing output and
    field-initialization order are preserved unchanged (the marker/handshake path
    is kept). Detection uses the class-features fields feature flag, so with
    @babel/preset-env the field lowering still happens automatically when the
    targets need it.

Before / after

class Example {
  before = 1;
  @dec decorated = 2;
  after = 3;
  #trailingPrivate = 4;
  method() {}
}

With @babel/plugin-proposal-decorators (legacy) and no class-properties, this
previously emitted the throwing _initializerWarningHelper. It now produces
working output that keeps the members that can stay native (fields before the
decorator, the method, the private field declaration) as class syntax, while
initializing the decorated field and the fields that follow it from the
constructor in source order:

let Example = (_class = class Example {
  constructor() {
    babelHelpers.initializerDefineProperty(this, "decorated", _descriptor, this);
    babelHelpers.defineProperty(this, "after", 3);
    this.#trailingPrivate = 4;
  }
  before = 1;
  #trailingPrivate;
  method() {}
}, _descriptor = babelHelpers.applyDecoratedDescriptor(/* ... */), _class);

An existing fixture in @babel/plugin-transform-typescript
(class/abstract-class-decorated-parameter, a real mobx @observable field)
went from emitting the throwing _initializerWarningHelper to working
constructor initialization.


Disclosure: this change was written with AI assistance (see the Co-Authored-By
trailer on the commits). I've reviewed and can walk through every line.

…ecorators

Legacy decorators depend on the class-properties transform to lower decorated
fields, which lowers every field, private element and method of the class even
when only one field is decorated. Without class-properties the decorated field
is emitted as a `_initializerWarningHelper` call that throws at runtime.

When class-properties is not enabled, lower only the decorated instance fields
into the constructor and leave undecorated members as native class syntax. When
it is enabled, the existing output and field initialization order are preserved.

Fixes babel#14917

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

❌ Automation signals

Activity patterns show signs of automation.

View full analysis →

This is an automated analysis by AgentScan

@babel-bot

babel-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/61889

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 955101f8e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

| NodePath<t.ClassMethod>
| undefined;

injectInitialization(path, constructor, instanceFieldInitializers);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve initializer order for mixed fields

When proposal-decorators runs without the class-properties transform, this injects every decorated field initializer into the constructor while leaving the other fields as native class fields. In a class such as a = log('a'); @dec b = log('b'); c = log('c'), the remaining native fields run before the constructor body, so the transformed order becomes a, c, b instead of a, b, c (and private fields after a decorated field are likewise initialized too early). This silently changes observable side effects and initialization errors whenever decorated and undecorated instance fields are mixed.

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

commit: 8b2a7cd

…ators

A native field initializer runs before the constructor body, so lowering only
the decorated fields into the constructor reordered initialization when an
undecorated field followed a decorated one. Relocate every instance field after
the first decorated one into the constructor too, in source order; fields before
it, methods and static members stay native. Fall back to the marker handshake
when a computed key would need to move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Develop-KIM

Copy link
Copy Markdown
Author

The previous version reordered initializers when an undecorated field followed a decorated one, since native fields run before the constructor body. Fixed by relocating every instance field after the first decorated one (public and private) into the constructor in source order. Fields before the first decorator, methods and statics stay native; a computed key that would need to move falls back to the marker handshake. Added a field-initialization-order exec fixture for the a; @dec b; c case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa168fa386

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

),
),
);
node.value = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep private brands from being installed early

When a private field follows the first decorated field in the no-class-properties path, this clears the value but leaves a native #x; element in the class. Native private elements are installed before the constructor body, so a relocated earlier initializer such as @dec a = (#x in this); #x = 1; observes true instead of the source-order false (or reads undefined instead of throwing through a method) before #x's position is reached. Fresh evidence after the update is that this path leaves the declaration in place while moving only its value into the constructor.

Useful? React with 👍 / 👎.

… a decorator

A native private field installs its brand with the class element, before the
constructor body, so relocating an earlier initializer that reaches the private
name (for example through a method) made it observe the brand too early: it read
undefined instead of throwing. The brand cannot be deferred without the full
private-field lowering that only the class-properties transform performs.

Fall back to the class-properties handshake when a private instance field
follows the first decorated field, the same way a computed key already does, so
only undecorated public fields are relocated into the constructor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b2a7cdb72

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

t.callExpression(state.addHelper("defineProperty"), [
t.thisExpression(),
computed || t.isLiteral(key) ? key : t.stringLiteral(key.name),
node.value || t.buildUndefinedNode(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve initializer scope when relocating fields

When a trailing undecorated field initializer references a name that is also bound by the constructor, moving the raw node.value into the constructor makes it resolve against constructor parameters/var bindings. Native field initializers do not have access to constructor scope, so class C { @dec x = 0; y = value; constructor(value) {} } should read an outer value or throw, but this transform makes y use the constructor argument. The class-features path passes a renamer to injectInitialization for this reason; this relocation path needs equivalent renaming/wrapping before injecting these initializers.

Useful? React with 👍 / 👎.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Excessive class properties transformation when other plugins are present

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.