pure-toplevel-functions marks the IIFE Babel emits for self-referencing static fields as PURE → Injector.ɵprov dropped, NG0201 (webpack builder, targets below Chrome 94)

Đang mở
#34,017 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
76/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
angular, babel, typescript
Lĩnh vực
build-system, cli, tooling

Hướng nghiên cứu

Bắt đầu với @angular-devkit/build-angular/src/tools/babel/presets/application.js và JavaScriptOptimizerPlugin, sau đó chạy repro.cjs với các thiết lập chrome 80. Hoàn thành khi đầu ra đã tối ưu vẫn giữ lại Injector.ɵprov và bản tái hiện đầy đủ của webpack-builder khởi động mà không có NG0201.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

area: @angular-devkit/build-angular gemini-triaged
Command

build, serve

Is this a regression?
  • Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was

Same family as #29145 (Angular 19). #29250 fixed the _defineProperty(...) call shape, but the IIFE wrapper that Babel emits for self-referencing static fields is still annotated as pure.

Description

With the webpack builder (@angular-devkit/build-angular:browser), optimization: true / buildOptimizer: true and a browserslist that requires lowering class static blocks (any target below Chrome 94, e.g. chrome >= 80, Smart TV engines), the application fails at bootstrap with NG0201 because Injector.ɵprov (and __NG_ELEMENT_ID__) no longer exist on Injector from @angular/core.

Chain:

  1. @babel/preset-env for such targets lowers class fields (class-static-block lowering pulls in transform-class-properties). Static fields that reference their own class (static ɵprov = ɵɵdefineInjectable({ token: Injector, ... })) are emitted as a zero-argument IIFE:
    _Injector = Injector;
    (() => {
      _defineProperty(_Injector, "ɵprov", ɵɵdefineInjectable({ token: _Injector, ... }));
      _defineProperty(_Injector, "__NG_ELEMENT_ID__", -1);
    })();
    
    For targets that only need private methods lowered (Chrome 84–93) the same code appears as a _staticBlock() helper call.
  2. pure-toplevel-functions (running in the non-safe mode used for @angular/* packages) skips IIFEs only when they have arguments (path.node.arguments.length !== 0) and does not know the _staticBlock helper name, so the wrapper gets /*#__PURE__*/.
  3. esbuild in JavaScriptOptimizerPlugin drops the annotated, unused call. Injector.ɵprov is gone → NG0201 at bootstrap.

#29250 added an exemption for direct _defineProperty(...) calls, which covers the non-self-referencing fields (THROW_IF_NOT_FOUND, NULL) but not the IIFE that Babel wraps around self-referencing ones.

Minimal Reproduction

Standalone script (run inside a project with @angular-devkit/build-angular installed; it uses the real application Babel preset with the options the webpack loader passes for @angular/* packages):

// repro.cjs
const { transformSync } = require('@babel/core');
const esbuild = require('esbuild');
const preset = require('@angular-devkit/build-angular/src/tools/babel/presets/application.js').default;
const src = `
export class Injector {
  static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
  static NULL = new NullInjector();
  static create(options, parent) { return createInjector(options, parent); }
  static ɵprov = /* @__PURE__ */ ɵɵdefineInjectable({ token: Injector, providedIn: 'any', factory: () => ɵɵinject(INJECTOR) });
  static __NG_ELEMENT_ID__ = -1;
}`;
const lowered = transformSync(src, {
  configFile: false, babelrc: false, sourceType: 'module', compact: false,
  filename: '/x/node_modules/@angular/core/fesm2022/core.mjs',
  presets: [[preset, { supportedBrowsers: ['chrome 80'], forceAsyncTransformation: false, optimize: { topLevelSafeMode: false, wrapDecorators: true } }]],
}).code;
console.log(lowered);
const out = esbuild.transformSync(lowered, { treeShaking: true, format: 'esm', target: 'chrome80', charset: 'utf8' });
console.log(out.code);
console.log(/ɵprov/.test(out.code) ? 'OK: Injector.ɵprov kept' : 'BUG: Injector.ɵprov dropped');

Output after the preset (note the /*#__PURE__*/ on the IIFE):

_Injector = Injector;
_defineProperty(Injector, "THROW_IF_NOT_FOUND", THROW_IF_NOT_FOUND);
_defineProperty(Injector, "NULL", /*#__PURE__*/new NullInjector());
/*#__PURE__*/(() => {
  _defineProperty(_Injector, "ɵprov", /* @__PURE__ */ɵɵdefineInjectable({ token: _Injector, providedIn: 'any', factory: () => ɵɵinject(INJECTOR) }));
  _defineProperty(_Injector, "__NG_ELEMENT_ID__", -1);
})();

After esbuild the IIFE is gone: BUG: Injector.ɵprov dropped.

Full-app steps: ng new, switch to @angular-devkit/build-angular:browser, .browserslistrc = chrome >= 80, optimization: true, buildOptimizer: true, ng build, open the app → NG0201 at bootstrap.

Exception or Error
NG0201 at bootstrap: no provider for Injector (Injector.ɵprov was removed from the bundle); the app stays on the splash screen.
Your Environment
Angular CLI: 22.0.9 (@angular/build 22.0.9 nested under @angular-devkit/build-angular); the plugin source is unchanged in @angular/build 22.1.7
Angular: 22.1.0
@babel/core / @babel/preset-env: 7.29.7
esbuild: 0.19.9
Node: 22.23.1
OS: darwin
Anything else relevant?

A minimal fix that works for us (applied via patch-package): never annotate IIFEs regardless of argument count, and treat _staticBlock/_staticBlock2… as Babel helper names. Class wrappers that are safe to drop are already annotated by adjust-static-class-members, so tree-shaking is unaffected in practice (main bundle +0.06% in our legacy build).

 function isBabelHelperName(name) {
-    return babelHelpers.has(name);
+    return babelHelpers.has(name) || /^_staticBlock\d*$/.test(name);
 }
 ...
                 const callee = path.get('callee');
-                if ((callee.isFunctionExpression() || callee.isArrowFunctionExpression()) &&
-                    path.node.arguments.length !== 0) {
+                if (callee.isFunctionExpression() || callee.isArrowFunctionExpression()) {
                     return;
                 }

Since #33751 moved these plugins into @angular-devkit/build-angular, the fix would land there for 22.2. Happy to send a PR.

Ngôn ngữ chính
TypeScript
Star
27k
Fork
11.8k
Merge trung bình
16 giờ 35 phút
Pull request đã merge (30 ngày)
176

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của angular/angular-cli

Tất cả issue của angular/angular-cli

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.