How to exec javascript code only when a plugin finished the execution and exec another plugin only when this js code finished asynchronous reading
Asked Answered
C

2

5

How do I reference the hashed image (in the html page in the dist folder) after it has been created with copy-webpack-plugin? I tried to resolve this problem in this way.

I have in my index.ejs file (you can consider this the .html file) a classic tag <img> that I'm copying in the dist folder with copy-webpack-plugin

My problem is that in 'production' mode I add an hash to my image instead in the index.ejs file the attribute src of the <img> still will point to the image without the hash. Thus my index.html in the dist folder doesn't display the image.

In order to resolve this problem I used WebpackManifestPlugin to generate a manifest.json that map my images and corresponding webpack output images (with hash) in a object like this:

{
  "assets/img/natura.jpg": "./assets/img/natura.e1b203dd72abf2858773.jpg",
  "assets/img/natale.jpg": "./assets/img/natale.5955e3731fd0538bb5ec.jpg",
  "assets/img/logo-angular.svg": "./assets/img/logo-angular.e7d82ae6d37ff090ba95.svg",
  "assets/img/manifest.json": "./assets/img/manifest.1473edc04cb44efe5ce6.json"
}

Later I have generated the manifest.json I can read this file:

productsJSON = require('./assets/img/manifest.json');

and finally I can pass productsJson to my index.ejs in this way:

new HtmlWebpackPlugin({
        
         inject: false,
         template: "./index.ejs", //Puoi mettere anche un file html
         templateParameters: {
            'myJSON': productsJSON
         },
         minify:true 
      })

and in the index.ejs I can do:

        <img
         src="<%= (process.env.NODE_ENV === 'production') ? myJSON['assets/img/natura.jpg'] : 'assets/img/natura.jpg' %>"
         alt="Natura.jpg"
      />
      <img
         src="<%= (process.env.NODE_ENV === 'production') ? myJSON['assets/img/natale.jpg'] : 'assets/img/natale.jpg' %>"
         alt="Natale.jpg"
      />

These are the whole files above: index.ejs

<!DOCTYPE html>
<html>
   <head>
      <title>Custom insertion example</title>
      <!-- prettier-ignore -->
      <% if (process.env.NODE_ENV === 'production'){%> 
       <%  for(var i=0; i < htmlWebpackPlugin.files.css.length; i++) {%>
      <link
         type="text/css"
         rel="stylesheet"
         href="<%= htmlWebpackPlugin.files.css[i] %>"
      />
      <% } }%>
   </head>
   <body>
      <img
         src="<%= (process.env.NODE_ENV === 'production') ? myJSON['assets/img/natura.jpg'] : 'assets/img/natura.jpg' %>"
         alt="Natura.jpg"
      />
      <img
         src="<%= (process.env.NODE_ENV === 'production') ? myJSON['assets/img/natale.jpg'] : 'assets/img/natale.jpg' %>"
         alt="Natale.jpg"
      />
      <button class="hello-world-button">Ciao</button>
      <img id="asset-resource" />

      <% for(var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
      <script
         type="text/javascript"
         src="<%= htmlWebpackPlugin.files.js[i] %>"
      ></script>
      <% } %>
   </body>
</html>

webpack.production.config.js

const pathM = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
/* const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 */
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
let productsJSON={};
try {
   productsJSON = require('./assets/img/manifest.json');
}
catch (err) {
   console.log(`Alla prima esecuzione il file manifest.json non esiste. Fare il build
                due volte. Sarebbe meglio gestirlo in un altro modo. Ma non so
                temporizzare l'esecuzione dei plugin. Cioè HTMLWebPackPlugin andrebbe
                eseguito solo dopo che ManifestPlugin ha creato Manifest.json `)
}

module.exports = {
   entry: './src/js/index.js',
   output: {
      filename: 'js/bundle.[contenthash].js',
      path: pathM.resolve(__dirname, './dist'),
      assetModuleFilename: '[path][name].[contenthash][ext]', 
      publicPath: './',
      clean: true /* {
         dry: false,
         keep:/\.css$/ 
      } */   //Serve per cancellare la cartella dist dalla precedente esecuzione
   },
   mode: 'production',
   module: {
      rules:[
         {
            test: /\.(png|jpe?g|webp|avif|gif|svg)$/,
            type: 'asset',
            parser: {
               dataUrlCondition: {
                  maxSize: 3 * 1024 //3 Kilobytes QUI CAMBIA LA SOGLIA A 3 KByte
                 /*  Il logo Angular è 6, 5 Kbyte.Cambia la soglia per includere
                  nel bundle js il logo */
               }
            }
         },
         /*rules per quando provi ad importare un file css da javascript. Uso due loaders
         css-loader  legge il contenuto del css e ritorna il contenuto
         style-loader prende il css e lo mette nella pagina, mette il css proprio nel
                      bundle.js Poi vedremo come generarli come file separati
         */
         {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader,'css-loader'] 
         },
         {
            test: /\.scss$/,
            use: [MiniCssExtractPlugin.loader,'css-loader','sass-loader'] 
         },
         {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
               loader: 'babel-loader',
               options: {
                  presets: [['@babel/env', {
                      targets: "> 0.1%, not dead",
                     debug:true, 
                     useBuiltIns: 'usage',
                     //Puoi mettere anche solo version:3
                     //La versione la puoi prelevare da package.json
                     corejs:{version:3.26 , proposals:true}
                  }]],
                  //plugins: ['@babel/plugin-proposal-class-properties']
               }
            }
         }
      ]
   },
   plugins: [
       new CopyWebpackPlugin({
         patterns: [
             {
                from: './assets/img', to: './assets/img/[name].[contenthash][ext]',
                globOptions: {
            ignore: [
              // Ignore all `txt` files
              "**/*.json",
            ],
          }, },
            
          ],
          options: {
        concurrency: 100,
          },
          
       }),
      new WebpackManifestPlugin({
                //Percorso assoluto che serve per dire dove mettere il file manifest.json
                fileName: pathM.resolve(__dirname, 'assets/img/manifest.json'),
         /* publicPath: '/dist/' QUesto metterebbe /dist/ prima del percorso di sopra*/
                filter: (file) => { const regEx = /img/;
                                    return regEx.test(file.name);
      },/*map: (file) => {
                    if ('production' === env.NODE_ENV) {
                        // Remove hash in manifest key
                        file.name = file.name.replace(/(\.[a-f0-9]{32})(\..*)$/, '$2');
                    }
                    return file;
                },*/
            }),
      new MiniCssExtractPlugin({
      filename:"css/main.[contenthash].css"
      }),
      new HtmlWebpackPlugin({
        
         inject: false,
         template: "./index.ejs", //Puoi mettere anche un file html
         templateParameters: {
            'myJSON': productsJSON
         },
         minify:true 
      })
      /*Nella seguente configurazione di questo plugin eliminiamo tutti i file
      e le cartelle e sottocartelle a partire dalla cartella .dist che è quella
      specificata in ouput.path
      asteriscoasterisco/asterisco  vuol dire tutti i file e le sottocartelle
      Inoltre specifico di ripulire anche tutti i file e le sottocartelle 
      dentro la cartella nomeCartella
      Nota che devo fornire un percorso assoluto perchè di default parte
      da ./dist (impostata in output.path)
      */
      //new CleanWebpackPlugin({
        // cleanOnceBeforeBuildPatterns: [
          //  '**/*',
           // path.join(process.cwd(),'nomeCartella/**/*')
      //]
   //})
   ]

}

I have two problems related. I have to exec the instruction productsJSON = require('./assets/img/manifest.json'); only when WebpackManifestPlugin generated manifest.json And I have to synchronize plugin execution, namely exec HtmlWebpackPlugin only when the file manifest.json is already ready

Here the whole project: https://github.com/cuccagna/Webpack28HandleHashWithManifest

How could I do?

I followed the a suggest in an answer. Using the done hook. (but in this way doesn't work because the html-webpack-plugin isn't executed after the hook,so this step is missing)

new WebpackManifestPlugin({
                
                fileName: pathM.resolve(__dirname, 'assets/img/manifest.json'),
         
                filter: (file) => { const regEx = /img/;
                                    return regEx.test(file.name);
      },
                apply(webpackCompiler){
                  
              webpackCompiler.hooks.done.tap('WebpackManifestPlugin', (stats) => {
                     
                     productsJSON = require('./assets/img/manifest.json');
                     
                   })}
            },
            )

Here we are the documentation of manifest-webpack-plugin https://www.npmjs.com/package/webpack-manifest-plugin/v/5.0.0 and in the hook section you talk about to syncronize the order of execution of plugins but I don't understand how I can use it in an effective way

Calvin answered 5/1, 2023 at 9:14 Comment(0)
M
4

Not sure if my answer will appeal to you. Just wanted to suggest an alternative approach. Sometimes, what a plugin does is pretty trivial, yet configuring it in webpack can get a bit messy.

An alternative approach is to use webpack primarily for the really hard job of treeshaking. I have then found this event very useful for writing Node.js code afterwards:

plugins:[

    {
      apply: (compiler) => {
        compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
          doCustomWork();
        });
      }
    }
]

I used this custom code for a demo app of mine to produce some finishing touches and produce HTML like this, with the readability I wanted.


<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>

        <base href='/spa/' />
        <title>OAuth Demo App</title>

        <link rel='stylesheet' href='bootstrap.min.css?t=1673180480701' integrity='sha256-Pxxy6CTJX1fLPROtVJ8Y5mSFIhoXWjOHfzRgUaTTAsI='>
        <link rel='stylesheet' href='app.css?t=1673180480701' integrity='sha256-B7pu+gcFspulW4zXfgczVtPcEuZ81tZRFYeRciEzWro='>
    </head>
    <body>
        <div id='root' class='container'></div>

        <script type='module' src='vendor.bundle.js?t=1673180480701' integrity='sha256-X/U647yXWbmD0b7NMVorHIGHHHW3KN7pohL5PXr4gdw='></script>
        <script type='module' src='app.bundle.js?t=1673180480701' integrity='sha256-amOGxWiYqyf4cHKy7FJ49RWxJSQR7w0yg4lHUKFYf3Y='></script>
    </body>
</html>
Madox answered 12/1, 2023 at 20:38 Comment(0)
W
3

You don't need WebpackManifestPlugin nor CopyWebpackPlugin, HtmlWebpackPlugin with .ejs template is fully capable to complete the task on its own.

You just need to use the correct template syntax:

<img src="<%= require('./assets/img/natura.jpg') %>" alt="Natura.jpg" />
<img src="<%= require('./assets/img/natale.jpg') %>" alt="Natale.jpg" />

Looks like you need some explanation besides a solution. So here we go.

The idea about webpack is that, it's a bundler. It finds assets by following all the require/import hints in your code, starting from the "entry points" that you supply. This process is very akin to how web crawler works.

How the image got included even without CopyWebpackPlugin? It's because <%= require('./assets/img/natura.jpg') %> is a legit require hint, webpack sees that and just includes the image into its compilation process.

Next step normally involves loader, but in webpack v5 they introduce asset-modules, I see you're using it in your webpack config. Basically it's a built-in loader for static assets.

output: {
    assetModuleFilename: '[path][name].[contenthash][ext]', 
// ...
module: {
    rules:[{
        test: /\.(png|jpe?g|webp|avif|gif|svg)$/,
        type: 'asset',

To answer your questions in comment:

I can't figure how is automatically added the hash to images.

Well, it's because you have specified assetModuleFilename to include [contenthash]. Basically asset modules has already done the job of CopyWebpackPlugin for you.

how can I optimize images?

You can chain the ImageMinimizerPlugin.loader with asset modules. Please read the doc for more details. The idea be like:

rules: [
  {
    test: /\.(jpe?g|png|gif|svg)$/i,
    type: "asset",
  },
  {
    test: /\.(jpe?g|png|gif|svg)$/i,
    use: [
      {
        loader: ImageMinimizerPlugin.loader,

And finally, you might wonder when will CopyWebpackPlugin be useful?

Remember the web-crawler-like behavior I mentioned above? It also implies, if some assets is not reachable by following require/import hints, then webpack is completely unaware of them, and they won't be included in /dist. This is where CopyWebpackPlugin comes to rescue by simply copying these forgotten assets over to the other side.

Now you know what's what, feel free to use CopyWebpackPlugin where you see fit.


Update

So I looked into ManifestPlugin's source code. And I found a hack:

  • webpack.production.config.js
// 👇 You don't need to require it.
// productsJSON = require("./assets/img/manifest.json");

// 👇 But you do need an empty object
// we'll populate its content later
const productsJSON = {};

module.exports = {
  // ...
  plugins: [
    new WebpackManifestPlugin({
      // ...
      generate(seed, files, entrypoints) {
        const manifestJson = files.reduce(
          (manifest, file) => Object.assign(manifest, { [file.name]: file.path }),
          seed
        );
        // 👇 here's the magic,
        // I just hijack the result right inside JS
        // so we don't need to wait for it to be written to disk
        // Then just Object.assign it onto productsJson
        Object.assign(productsJSON, manifestJson);
        console.log('tada!', productsJSON);
        return manifestJson;
      },
    }),
    new HtmlWebpackPlugin({
      inject: false,
      template: "./index.ejs",
      templateParameters: {
        myJSON: productsJSON,
      },
      minify: true,
    }),
  ],
};

It's a possible solution. But I'm not gonna test it for you this time. I've already deleted your repo from my laptop. And I really feel reluctant do it again, because you include the whole /node_modules folder into the repo. It takes forever to download.

Theoretically this could work. But still it depends on the execution order of WebpackManifestPlugin and HtmlWebpackPlugin. If WebpackManifestPlugin comes first then we got luck, problem solved. But I'm not completely sure about it.

Give it a try and may the force be with you!

Weinhardt answered 10/1, 2023 at 16:22 Comment(11)
But in this way the images haven't the hash part and my bundles won't have tooCalvin
Have you actually tried it? Cus I've cloned your repo and tested this method. The output do include the hash part, both in image filename and the HTML img src path.Weinhardt
Now I have tested, and it works though I can't figure how is automatically added the hash to images. Furthermore I left HtmlWebpackPlugin because othrwise how can I copy index.ejs file in the dist folder (and insert script e link tag inside it)? Finally with your solution how can I optimize images? With copyWebpackPlugin, togheter with ImageMinimizerPlugin (used as option of copyWebpackPlugin) I was able to minimize my images. Now with yout method I'm not able to do this.Calvin
You can use ImageMinimizerPlugin standalone, without involving CopyWebpackPlugin. It's out of scope of the original question so I'll leave it to you. Webpack is complicated, so many docs to read.Weinhardt
I upvoted your answer, but it don't focus on the specific question namely:1) how to exec js code later a plugin finished 2)how to syncronize the order of execution of 2 plugins Your response is another way to resolve my problem, but I'm looking for a response centered in my way.Calvin
OK. I'll take another look when I have time. But last I remember this kind of thing is impossible to pull together. Don't wrestle with the tool, go with it.Weinhardt
@Calvin I just updated the answer with a possible solution. Check it out.Weinhardt
I didn't imagine that with require in index.ejs the loader-asset/resource comes into play. Thanks to clarify. I understood your edit, I tested it but stil doesn't work because HtmlWebpackPlugin is executed before than WebpackManifestPlugin (the manifest is properly generated). Here the project with the changes you kindly suggested: github.com/cuccagna/webpackProjectStackoverflow without node_modules folder. (There isn't the dist folder. Give npm run build later npm install )Calvin
No luck then. I guess this is a dead end. Think about it, ManifestPlugin is meant to run last, cus its job is to look at all the generated assets and create a manifest. And html file, generated by HtmlPlugin is also an asset. You see the paradox here?Weinhardt
And yes, I didn’t answer the question “ how to syncronize plugins”. I know a bit about all the internal “hooks” of webpack, provided by tapable lib. You need to “tap into” couple of those hooks in order to synchronize plugins, but I’m not familiar enough to write up an answer. Just to mention some leads that you might want to google for. Basically this way involves a lot of doc reading and source code checking, which I tend to avoid. But, if you’re interested to write your own plugin, it’s definitely the way to go.Weinhardt
webpack.js.org/contribute/writing-a-plugin Skim through this docs, you’ll get a brief idea what I mean.Weinhardt

© 2022 - 2024 — McMap. All rights reserved.