just-jeb / angular-builders

Angular build facade extensions (Jest and custom webpack configuration)
MIT License
1.14k stars 198 forks source link

Compiling workbox webworker with angular #1332

Closed yelhouti closed 1 year ago

yelhouti commented 1 year ago

Describe the Bug

I am trying to compile a Workbox service worker written in typescript, I am using the @angular-builders/custom-webpack:browser project to do customize other parts of the build, and would like to run the workbox-webpack-plugin (with InjectManifest).

When I try to run the plugin inside the same config it fails because the angular build is not yet finished/ready. If I try to return two configs, using code that looks like this, it fails, because an array is not expected in the config.

const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
const { hashElement } = require('folder-hash');
const MergeJsonWebpackPlugin = require('merge-jsons-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const WebpackNotifierPlugin = require('webpack-notifier');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');

const environment = require('./environment');
const proxyConfig = require('./proxy.conf');

module.exports = async (config, options, targetOptions) => {
  const languagesHash = await hashElement(path.resolve(__dirname, '../src/main/webapp/i18n'), {
    algo: 'md5',
    encoding: 'hex',
    files: { include: ['*.json'] },
  });

  // PLUGINS
  if (config.mode === 'development') {
    config.plugins.push(
      new ESLintPlugin({
        baseConfig: {
          parserOptions: {
            project: ['../tsconfig.app.json'],
          },
        },
      }),
      new WebpackNotifierPlugin({
        title: 'MyApp',
        contentImage: path.join(__dirname, 'logo-jhipster.png'),
      })
    );
  }

  // configuring proxy for back end service
  const tls = Boolean(config.devServer && config.devServer.https);
  if (config.devServer) {
    config.devServer.proxy = proxyConfig({ tls });
  }

  if (targetOptions.target === 'serve' || config.watch) {
    config.plugins.push(
      new BrowserSyncPlugin(
        {
          host: 'localhost',
          port: 9000,
          https: tls,
          proxy: {
            target: `http${tls ? 's' : ''}://localhost:${targetOptions.target === 'serve' ? '4200' : '8080'}`,
            ws: true,
            proxyOptions: {
              changeOrigin: false, //pass the Host header to the backend unchanged  https://github.com/Browsersync/browser-sync/issues/430
            },
          },
          socket: {
            clients: {
              heartbeatTimeout: 60000,
            },
          },
          ghostMode: {
            // uncomment this part to disable BrowserSync ghostMode; https://github.com/jhipster/generator-jhipster/issues/11116
            clicks: false,
            location: false,
            forms: false,
            scroll: false,
          },
        },
        {
          // reload: targetOptions.target === 'build', // enabled for build --watch
          reload: false, // we angular hmr
        }
      )
    );
  }

  if (config.mode === 'production') {
    config.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false,
        // Webpack statistics in target folder
        reportFilename: '../stats.html',
      })
    );
  }

  const patterns = [
    {
      // https://github.com/swagger-api/swagger-ui/blob/v4.6.1/swagger-ui-dist-package/README.md
      context: require('swagger-ui-dist').getAbsoluteFSPath(),
      from: '*.{js,css,html,png}',
      to: 'swagger-ui/',
      globOptions: { ignore: ['**/index.html'] },
    },
    {
      from: require.resolve('axios/dist/axios.min.js'),
      to: 'swagger-ui/',
    },
    { from: './src/main/webapp/swagger-ui/', to: 'swagger-ui/' },
    // jhipster-needle-add-assets-to-webpack - JHipster will add/remove third-party resources in this array
  ];

  if (patterns.length > 0) {
    config.plugins.push(new CopyWebpackPlugin({ patterns }));
  }

  config.resolve.fallback = {
    stream: require.resolve('stream-browserify/index.js'),
    buffer: require.resolve('buffer/'),
    util: require.resolve('util/'),
    process: require.resolve('process/'),
    path: require.resolve('path-browserify/'),
    zlib: false,
  };

  config.plugins.push(
    new webpack.DefinePlugin({
      I18N_HASH: JSON.stringify(languagesHash.hash),
      // APP_VERSION is passed as an environment variable from the Gradle / Maven build tasks.
      __VERSION__: JSON.stringify(environment.__VERSION__),
      __DEBUG_INFO_ENABLED__: environment.__DEBUG_INFO_ENABLED__ || config.mode === 'development',
      // The root URL for API calls, ending with a '/' - for example: `"https://www.jhipster.tech:8081/myservice/"`.
      // If this URL is left empty (""), then it will be relative to the current context.
      // If you use an API server, in `prod` mode, you will need to enable CORS
      // (see the `jhipster.cors` common JHipster property in the `application-*.yml` configurations)
      SERVER_API_URL: JSON.stringify(environment.SERVER_API_URL),
    }),
    new MergeJsonWebpackPlugin({
      output: {
        groupBy: [
          { pattern: './src/main/webapp/i18n/en/*.json', fileName: './i18n/en.json' },
          { pattern: './src/main/webapp/i18n/fr/*.json', fileName: './i18n/fr.json' },
          // jhipster-needle-i18n-language-webpack - JHipster will add/remove languages in this array
        ],
      },
    }),
    // needed by util, needed by escpos
    new webpack.ProvidePlugin({
      process: 'process/browser',
    }),
    new webpack.ProvidePlugin({
      path: 'path-browserify',
    }),
  );

  // THIS DOESNT WORK BECAUSE IVY/ANGULAR IS TRYING TO COMPILE UNRELATED CODE AND FAILS FOR SOME REASON
  // config.entry['serviceworker'] = {
  //   import: './src/main/serviceworker/serviceworker.ts',
  //   filename: 'serviceworker.js', // using filename to avoid output on build looks like: serviceworker.961c9e356e2871e6.js and have issues when there is an issue with the worker.
  //   // more info here: https://developer.chrome.com/docs/workbox/remove-buggy-service-workers/#deploy-a-no-op-service-worker
  // }

  config = merge(
    config
    // jhipster-needle-add-webpack-config - JHipster will add custom config
  );

  return [
    config,
    // THIS DOESN'T WORK EITHER BECAUSE RETURNING ARRAY IS NOT EXPECTED
    // {
    //   name: 'serviceworker',
    //   plugins: [
    //     // Normally, we wouldn't use workbox-webpack-plugin since precaching in our case should be handled by angular's service worker ngsw
    //     // and we use workbox only to handle requests going to /api
    //     // however ngsw doesn't allow customizations: https://github.com/angular/angular/issues/21197
    //     new WorkboxPlugin.InjectManifest({
    //       swSrc: './src/main/serviceworker/serviceworker.ts',
    //       swDest: 'serviceworker.js',
    //     })
    //   ]
    // }
  ];
};

Minimal Reproduction

https://github.com/yelhouti/angular-workbox

Expected Behavior

Angular should compile then workbox should get executed

Environment


Libs
- @angular/core version: 14.2.3
- @angular-devkit/build-angular version: 14.2.3
- @angular-builders/custom-webpack version: 14.0.1

For Tooling issues:
- Node version: v16.13.0
- Platform:  Linux

Additional context

I would prefer to have the same config do everything so that when I run ng serve --hmr, the service worker is compiled and can be debugged ...

just-jeb commented 1 year ago

Ok, so I did a little investigation and I have good news and bad news 😅 . The good news (mostly for me I guess) is that Custom Webpack builder works as expected and the configuration array goes all the way down to Angular Webpack abstraction.

The bad news is that Angular treats the configuration as an object, hence the issue. The error you get is:

An unhandled exception occurred: Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
 - configuration has an unknown property '0'.

And here is the root cause. See that little ...config? That means they spread the object and add watch: false to it. Which works well when it's indeed an object. But what happens when it's an array? Well, arrays are objects so when you spread it like object you'll get an object with properties 0, 1 etc.
So after the spread the "config" would look like that:

{
 '0': yourFirstConfig,
 '1': yourSecondConfig,
 watch: false
}

Which by no means is a Webpack config object. And that's why you get the error (which btw comes from Webpack schema validation).

Unfortunately there is not much you can do about it, except for opening an issue on Angular repo.
And even this would be a long shot IMO, because they treat Webpack as implementation details and they'll probably not support it.

From my side there is nothing I can do at all, but thanks for sharing an interesting use case.

yelhouti commented 1 year ago

@just-jeb thanks for taking the time, I guess I will add a postBuild step just for that, maybe its for the best.

just-jeb commented 1 year ago

Yup, good call 👍.