Search code examples
webpackwebpack-4

Webpack: Set the path for my assets according to the entry name


Using webpack, I have a multiple page application which works with multiple entries. I have a project tree like this:

-- webpack.config.js
-- src/
    -- first/
        -- main.ts
        -- helper.ts
        -- main.css
        -- index.html
    -- second/
        -- main.ts
        -- main.css
        -- index.html
    -- assets/
        -- myAsset1.png
        -- myAsset2.png
        -- myAsset3.png

I wanted something tidy and organised by folder. To do so, I add [name]/... each time for the name or the filename like this:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');

module.exports = {
  entry: {
    first: './src/first/main.ts',
    second: './src/second/main.ts',
  },
  output: {
    path: path.resolve(__dirname, 'dist'), // output directory
    filename: '[name]/script.js', // name of the generated bundle
    publicPath: '/',
  },
  module: {
    rules: [
      // TypeScript
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: {
          onlyCompileBundledFiles: true,
        },
      },
      // Style
      {
        test: /\.s?[ac]ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
          },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'resolve-url-loader',
          },
          {
            loader: 'sass-loader',
          },
        ],
      },
      // Fonts & Images
      {
        test: /\.(woff(2)?|ttf|eot|jpg|png|svg|md)(\?v=\d+\.\d+\.\d+)?$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              limit: 8192,
              name: '[name].[ext]',
            },
          },
        ],
      },
    ],
  },
  resolve: {
    modules: ['node_modules', path.resolve(process.cwd(), 'src')],
    extensions: ['.ts', '.js'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(process.cwd(), 'src/first/index.html'),
      inject: true,
      chunks: ['first'],
      filename: 'first/index.html',
    }),
    new HtmlWebpackPlugin({
      template: './src/second/index.html',
      inject: true,
      chunks: ['second'],
      filename: 'second/index.html',
    }),
    new MiniCssExtractPlugin({
      filename: '[name]/style.css',
      chunkFilename: '[name]/style.css',
    }),
  ],
};

The result will be:

-- dist/
    -- first/
        -- script.js
        -- style.css
        -- index.html
    -- second/
        -- script.js
        -- style.css
        -- index.html
    -- myAsset1.png
    -- myAsset2.png
    -- myAsset3.png

However, I would like the assets in the folder name according to the entry from where it is imported. Indeed, myAssets1.png and myAssets2.png are imported from main.ts of first folder and myAssets3.png is imported from main.ts of second folder

-- dist/
    -- first/
        -- script.js
        -- style.css
        -- index.html
        -- myAsset1.png
        -- myAsset2.png
    -- second/
        -- script.js
        -- style.css
        -- index.html
        -- myAsset3.png

How could I set the path for my assets according to the entry name?


Solution

  • In order to specify that the assets that file-loader is handling will be saved in your required place few things should happend:

    1. you need to determine which entry point is the issuer of this file
    2. inject it as a resourceQuery
    3. use the injected resourceQuery in the name function of the file-loader to specify the path.

    In order to achieve 1,2 you can create a custom loader, which will extract the issuer & attach the query.

    // entry-dir-injector-loader.js
    const path = require('path');
    
    function recursiveIssuer(m) {
      if (m.issuer && m.issuer.context) {
        return recursiveIssuer(m.issuer);
      } else if (m.context) {
        return m.context;
      } else {
        return false;
      }
    }
    
    module.exports = function (content) {
      const entry = recursiveIssuer(this._module);
      const entryRelativePath = path.relative(this.rootContext, entry).replace('src/', '');
      this.resourceQuery = `?entryDir=${entryRelativePath}`;
      return content;
    };
    

    Then specify the name as a function, extract the query.

    module.exports = {
      module: {
        rules: [
          {
            test: /\.(woff(2)?|ttf|eot|jpg|png|svg|md)(\?v=\d+\.\d+\.\d+)?$/,
            use: [
              {
                loader: 'file-loader',
                options: {
                  limit: 8192,
                  name(resourcePath, resourceQuery) {
                    const entryDir = resourceQuery
                      .substring(1)
                      .split('&')
                      .filter((key) => key.startsWith('entryDir='))
                      .map((key) => key.split('=').pop())
                      .join('');
    
                    return path.join(entryDir, `[name].[ext]`);
                  },
                },
              },
              {
                loader: './entry-dir-injector-loader.js',
              },
            ],
          },
        ],
      },
    };