Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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)

Ouverte
#34,017 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
3/5
Temps estimé
1-2 jours
Accessibilité débutants
76/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
angular, babel, typescript

Piste de recherche

Commencez par @angular-devkit/build-angular/src/tools/babel/presets/application.js et JavaScriptOptimizerPlugin, puis exécutez repro.cjs avec les paramètres de chrome 80. Le travail est terminé lorsque la sortie optimisée conserve Injector.ɵprov et que la reproduction complète de webpack-builder démarre sans NG0201.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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.

Langage dominant
TypeScript
Étoiles
27k
Forks
11.8k
Merge moyen
16 h 35 min
PR mergées (30 j)
176

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de angular/angular-cli

Toutes les issues de angular/angular-cli

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.