angular / angular-cli

CLI tool for Angular

Home Page:https://cli.angular.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Issues with independently using @ngtools/webpack to load loader.

grapehunter opened this issue · comments

Command

build

Description

Issues with @ngtools/webpack

I found some drawbacks when using additional loaders independently with @ngtools/webpack, detached from angular-cli. Please see the minimal example using ifdef-loader (https://stackblitz.com/~/github.com/grapehunter/ngtool_webpack_show):

webpack.config.json

module.exports = (env) => {
    ...
    module: {
      rules: [
        {
          test: /\.[cm]?[tj]sx?$/,
          resolve: { fullySpecified: false },
          exclude:
            /[\\/]node_modules[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill|whatwg-url)[/\\]/,
          use: [
            { loader: "ifdef-loader", options: { DEBUG: !isProduction } }, 
            { loader: path.resolve(__dirname, "./scripts/trace-loader.cjs") }, // log source code in pipeline
            {
              loader: "@ngtools/webpack",
            },
          ],
        },
      ],
    },
    ...
}

index.ts

...
/// #if DEBUG
alert("Running in development mode!");
/// #else
alert("Running in production mode!");
/// #endif

@Component({
  selector: "app-root",
  standalone: true,
  /// #if DEBUG
  template: ` <h1>Hello from development!</h1> `,
  /// #else
  // @ts-ignore
  template: ` <h1>Hello from production!</h1> `,
  /// #endif
})
export class App {}

bootstrapApplication(App, {
  providers: [provideExperimentalZonelessChangeDetection()],
});
  1. If the example code is executed first by @ngtools/webpack (AngularWebpackPlugin), then the code processed by @ngtools/webpack is printed out as follows through trace-loader.cjs:
/// #if DEBUG
alert("Running in development mode!");
/// #else
alert("Running in production mode!");
/// #endif
export class App {
}
App.ɵfac = function App_Factory(t) { return new (t || App)(); };
App.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: App, selectors: [["app-root"]], standalone: true, features: [i0.ɵɵStandaloneFeature], decls: 2, vars: 0, template: function App_Template(rf, ctx) { if (rf & 1) {
        i0.ɵɵelementStart(0, "h1");
        i0.ɵɵtext(1, "Hello from production!");
        i0.ɵɵelementEnd();
    } }, encapsulation: 2 });
bootstrapApplication(App, {
    providers: [provideExperimentalZonelessChangeDetection()],
});

As you can see, after @ngtools/webpack compilation, the commented code within the @component scope disappears. Thus, when ifdef-loader receives the code, it cannot process the second /// #if DEBUG in main.ts, leading to incorrect page output (Hello from production! instead of the correct Hello from development!).

  1. If { loader: "ifdef-loader", options: { DEBUG: !isProduction } } is placed below @ngtools/webpack, i.e., ifdef-loader is executed first and then @ngtools/webpack. Since @ngtools/webpack internally loads source files from the path, not through the loader pipeline, the final compiled result will ignore any changes made by ifdef-loader to the code.
    Here is an example of an incorrectly executed project: https://stackblitz.com/~/github.com/grapehunter/ngtool_webpack_show

Describe the solution you'd like

The modification method I am currently using

I currently preprocess the code by patching the AngularWebpackPlugin configuration parameters of @ngtools/webpack to add a sourceModifier.
The patch file is as follows:

diff --git a/node_modules/@ngtools/webpack/src/ivy/plugin.js b/node_modules/@ngtools/webpack/src/ivy/plugin.js
index 9c970d0..8c00f1f 100644
--- a/node_modules/@ngtools/webpack/src/ivy/plugin.js
+++ b/node_modules/@ngtools/webpack/src/ivy/plugin.js
@@ -139,7 +139,7 @@ class AngularWebpackPlugin {
         // Webpack lacks an InputFileSytem type definition with sync functions
-        compiler.inputFileSystem, (0, paths_1.normalizePath)(compiler.context));
+        compiler.inputFileSystem, (0, paths_1.normalizePath)(compiler.context), this.pluginOptions.sourceModifier ?? []);
         const host = ts.createIncrementalCompilerHost(compilerOptions, system);
         // Setup source file caching and reuse cache from previous compilation if present
         let cache = this.sourceFileCache;
diff --git a/node_modules/@ngtools/webpack/src/ivy/system.js b/node_modules/@ngtools/webpack/src/ivy/system.js
index d86681b..4ba03b6 100644
--- a/node_modules/@ngtools/webpack/src/ivy/system.js
+++ b/node_modules/@ngtools/webpack/src/ivy/system.js
@@ -36,7 +36,7 @@ const paths_1 = require("./paths");
 function shouldNotWrite() {
     throw new Error('Webpack TypeScript System should not write.');
 }
-function createWebpackSystem(input, currentDirectory) {
+function createWebpackSystem(input, currentDirectory, sourceModifiers) {
     // Webpack's CachedInputFileSystem uses the default directory separator in the paths it uses
     // for keys to its cache. If the keys do not match then the file watcher will not purge outdated
     // files and cause stale data to be used in the next rebuild. TypeScript always uses a `/` (POSIX)
@@ -58,7 +58,14 @@ function createWebpackSystem(input, currentDirectory) {
             if (data.length > 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) {
                 start = 3;
             }
-            return data.toString('utf8', start);
+            // return data.toString('utf8', start);
+            let source = data.toString('utf8', start);
+            for (const sourceModifier of sourceModifiers) {
+                if (typeof sourceModifier.filter === 'function' && sourceModifier.filter(source, path)) {
+                    source = sourceModifier.modifier(source, path);
+                }
+            }
+            return source;
         },

Modifications to webpack.config.js are required for use.

module: {
      rules: [
        {
          test: /\.[cm]?[tj]sx?$/,
          resolve: { fullySpecified: false },
          exclude:
            /[\\/]node_modules[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill|whatwg-url)[/\\]/,
          use: [
            // { loader: "ifdef-loader", options: { DEBUG: !isProduction } },  // comment ifdef-loader here
            { loader: path.resolve(__dirname, "./scripts/trace-loader.cjs") }, // log source code in pipeline
            {
              loader: "@ngtools/webpack",
            },
          ],
        },
      ],
plugins: [
new AngularWebpackPlugin({
        tsConfigPath: "tsconfig.json",
        jitMode: false,
        ....
        sourceModifier: [
          {
            filter: (source, path) => !/node_modules/.test(path),
            modifier: (source, path) => {
              const { parse } = require("ifdef-loader/preprocessor");
              return parse(source, {
                DEBUG: !isProduction,
              });
            },
          },
        ],
      }),
]
...

In this way, the code given to the TypeScript compiler internally by @ngtools/webpack has already been processed by ifdef-loader, so the final page output is correct.
Here is the modified project example: https://stackblitz.com/~/github.com/grapehunter/ngtool_webpack_show_fix

Describe alternatives you've considered

Questions I would like to ask

  1. The modification method I am currently using is quite troublesome. Each time I upgrade the @ngtools/webpack version, I have to re-edit the patch file. So I would like to know if there are other ways to achieve the same goal.
  2. If not, does this repo accept such modifications? If so, I can submit a pull request.