Home > Enterprise >  How to exec javascript code only when a plugin finished the execution and exec another plugin only w
How to exec javascript code only when a plugin finished the execution and exec another plugin only w

Time:01-13

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 >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

CodePudding user response:

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
//            
  • Related