angular / angular/angular-cli

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)

未关闭
#34,017 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
area: @angular-devkit/build-angular gemini-triaged
主要语言
TypeScript
星标
27k
派生
11.8k
平均合并
14 小时 23 分钟
30 天内合并 PR
162

描述

### Command

build, serve

### Is this a regression?

- [x] 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**:
```js
_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):

```js
// 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):

```js
_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

```text
NG0201 at bootstrap: no provider for Injector (Injector.ɵprov was removed from the bundle); the app stays on the splash screen.
```

### Your Environment

```text
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).

```diff
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.

贡献指南

打开贡献指南

调研方向

从 @angular-devkit/build-angular/src/tools/babel/presets/application.js 和 JavaScriptOptimizerPlugin 开始,然后使用 chrome 80 设置运行 repro.cjs。当优化后的输出保留 Injector.ɵprov,并且完整的 webpack-builder 复现能够在没有 NG0201 的情况下启动时,即表示完成。

由索引模型根据 Issue 内容生成。

评估

技术栈
angular, babel, typescript
领域
build-system, cli, tooling
Issue 类型
缺陷
难度
3/5
预计耗时
1-2 天
活跃度
活跃
描述清晰度
描述清楚
新手友好度
76/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。