web-infra-dev / rspack

The fast Rust-based web bundler with webpack-compatible API 🦀️
https://rspack.dev
MIT License
9.8k stars 564 forks source link

[Bug]: Chunks hashes calculated with errors #7937

Closed RabbitShare closed 1 month ago

RabbitShare commented 1 month ago

System Info

System: OS: macOS 15.0 CPU: (10) arm64 Apple M1 Pro Memory: 139.61 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.16.0 - ~/.nvm/versions/node/v20.16.0/bin/node Yarn: 1.22.19 - /opt/homebrew/bin/yarn npm: 10.8.1 - ~/.nvm/versions/node/v20.16.0/bin/npm Browsers: Chrome: 128.0.6613.138 Firefox: 128.0.2 Safari: 18.0

Details

After build some files dosen't change his hashes, but links to builded files changed. image Sorry for bad quality. It's our user's screenshot

Rspack trying to request chunk /js/5488.undefined.js. But this chunk broken. I think it's because rspack not change file hash if change only link to another chunk (i don't know how it works, perceive link as abstraction) image (comparison of two chunks with same names between different builds) image

webpack build normally.

rspack.config.ts

import { defineConfig } from '@rspack/cli';
import { rspack } from '@rspack/core';
import RefreshPlugin from '@rspack/plugin-react-refresh';
import path from 'path';
import dotenv from 'dotenv';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
import HtmlRspackPlugin from 'html-rspack-plugin';

dotenv.config({ path: './.env.local' });
const isDev = process.env.NODE_ENV === 'development';

const APP_NAME = 'Secret';
const COOKIE_BACKEND_HOST = process.env.COOKIE_BACKEND_HOST;
const DEBUG_HOST = 'localhost';
const DEBUG_PORT = process.env.PORT || '8086';
const BASE_URL = process.env.BASE_URL || '/api';
const DEV_PROXY = process.env.REACT_APP_API_BASE_URL;
const JSON_ENV_URL = process.env.JSON_ENV_URL || '/config.json';
const FAVICON_PATH = path.resolve(__dirname, 'src', 'assets', 'favicon.ico');
const ALLOW_ANALYTICS = Boolean(process.env.ALLOW_ANALYTICS);

// Target browsers, see: https://github.com/browserslist/browserslist
const targets = ['chrome >= 87', 'edge >= 88', 'firefox >= 78', 'safari >= 14'];

export default defineConfig({
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    filename: `js/[name].[${isDev ? 'fullhash' : 'contenthash'}].bundle.js`,
    chunkFilename: 'js/[name].[chunkhash:8].js',
    clean: true,
  },
  devtool: isDev ? 'source-map' : false,
  experiments: {
    css: true,
  },
  context: __dirname,
  entry: {
    main: './src/app/index.tsx',
  },
  resolve: {
    extensions: ['...', '.ts', '.tsx', '.jsx'],
    tsConfig: {
      configFile: path.resolve(__dirname, 'tsconfig.json'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(jsx?|tsx?)$/,
        use: [
          {
            loader: 'builtin:swc-loader',
            options: {
              jsc: {
                experimental: {
                  plugins: [
                    [
                      '@swc/plugin-styled-components',
                      {
                        displayName: true,
                        ssr: false,
                      },
                    ],
                  ],
                },
                parser: {
                  syntax: 'typescript',
                  tsx: true,
                },
                transform: {
                  react: {
                    runtime: 'automatic',
                    development: isDev,
                    refresh: isDev,
                  },
                },
              },
              env: { targets },
            },
          },
        ],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/,
        type: 'asset/resource',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new rspack.DefinePlugin({
      env: { JSON_ENV_URL: JSON.stringify(JSON_ENV_URL) },
      webpack: {
        isProduction: !isDev,
      },
    }),
    new HtmlRspackPlugin({
      template: './src/index.html',
      inject: 'body',
      templateParameters: {
        title:
          APP_NAME ||
          'You should use variable APP_NAME in rspack.config.ts for your project',
        appConfigFilePath: JSON_ENV_URL,
      },
      favicon: (FAVICON_PATH && path.resolve(__dirname, FAVICON_PATH)) || '',
    }),
    new rspack.CopyRspackPlugin({
      patterns: [{ from: 'public', to: '', noErrorOnMissing: true }],
    }),
    isDev && new RefreshPlugin(),
    ALLOW_ANALYTICS &&
      new RsdoctorRspackPlugin({
        // plugin options
      }),
  ].filter(Boolean),
  optimization: {
    minimizer: [
      new rspack.SwcJsMinimizerRspackPlugin(),
      new rspack.LightningCssMinimizerRspackPlugin({
        minimizerOptions: { targets },
      }),
    ],
    runtimeChunk: isDev ? 'single' : 'multiple',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          idHint: 'vendors',
          priority: -1,
          reuseExistingChunk: true,
        },
      },
    },
  },
  devServer: {
    setupMiddlewares: (middlewares, devServer) => {
      if (!devServer) {
        throw new Error('webpack-dev-server is not defined');
      }

      const { SMART_CAPTCHA_SITE_KEY, FEATURE_FLAG_V2_THEME, BASE_URL } =
        process.env;

      devServer?.app?.get(JSON_ENV_URL, (_, response) => {
        response.send(
          JSON.stringify({
            SMART_CAPTCHA_SITE_KEY,
            FEATURE_FLAG_V2_THEME,
            BASE_URL,
          }),
        );
      });

      return middlewares;
    },
    host: DEBUG_HOST,
    port: DEBUG_PORT,
    static: path.resolve(__dirname, 'dist'),
    historyApiFallback: true,
    proxy: [
      {
        context: [BASE_URL],
        target: DEV_PROXY,
        changeOrigin: true,
        secure: false,
        onProxyReq: (proxyReq) => {
          // CSRF bypass
          proxyReq.setHeader('referer', DEV_PROXY as string);
        },
        cookieDomainRewrite: {
          [COOKIE_BACKEND_HOST as string]: DEBUG_HOST,
        },
      },
    ],
  },
});

Reproduce link

No response

Reproduce Steps

  1. Run build
  2. Modify code in connected chunk
  3. Run build again
  4. ????
  5. One chunk has the same name
github-actions[bot] commented 1 month ago

Hello @RabbitShare, sorry we can't investigate the problem further without reproduction demo, please provide a repro demo by forking rspack-repro, or provide a minimal GitHub repository by yourself. Issues labeled by need reproduction will be closed if no activities in 14 days.

LingyuCoder commented 1 month ago

You can try disabling splitChunks and see if the problem still exists, and also remove hashes in your output filenames. When an entry chunk is splitted to mutiple and rendered into html script tags by html plugin, the generated html will not be updated without reloading the page even if the chunk is splitted again.

RabbitShare commented 1 month ago

Thanks for variants, but it's not works for me. Here is repro demo https://github.com/RabbitShare/rspack-test. Repo has dist1 and dist2. dist1 build with

export const Test = () => {
  return (
    <div>
      <h1>Test</h1>
    </div>
  );
};

output is:

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
  ["568"],
  {
    58: function (e, s, t) {
      t.r(s),
        t.d(s, {
          Test: function () {
            return n;
          },
        });
      var c = t(250);
      let n = () =>
        (0, c.jsx)("div", { children: (0, c.jsx)("h1", { children: "Test" }) });
    },
  },
]);
//# sourceMappingURL=568.js.map

dist2 build with

export const Test = () => {
  return (
    <div>
      <h1>Test1</h1>
    </div>
  );
};

output:

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
  ["568"],
  {
    58: function (e, s, t) {
      t.r(s),
        t.d(s, {
          Test: function () {
            return n;
          },
        });
      var c = t(250);
      let n = () =>
        (0, c.jsx)("div", {
          children: (0, c.jsx)("h1", { children: "Test1" }),
        });
    },
  },
]);
//# sourceMappingURL=568.js.map

As you see 568 from dist1 not equal to 568 from dist2.

For reproduction:

  1. pull repo
  2. npm ci
  3. git checkout v1.0.0
  4. npm run build
  5. save dist artifact with another name (dist1 in repo, but you can use another). Optional npx prettier --write [new dist name]
  6. git checkout v1.0.1
  7. npm run build
  8. save dist artifact with another name (dist2 in repo, but you can use another). Optional npx prettier --write [new dist name]

Expected behavior: Chunk from dist2 has another name

LingyuCoder commented 1 month ago

Thanks for variants, but it's not works for me. Here is repro demo RabbitShare/rspack-test. Repo has dist1 and dist2. dist1 build with

export const Test = () => {
  return (
    <div>
      <h1>Test</h1>
    </div>
  );
};

output is:

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
  ["568"],
  {
    58: function (e, s, t) {
      t.r(s),
        t.d(s, {
          Test: function () {
            return n;
          },
        });
      var c = t(250);
      let n = () =>
        (0, c.jsx)("div", { children: (0, c.jsx)("h1", { children: "Test" }) });
    },
  },
]);
//# sourceMappingURL=568.js.map

dist2 build with

export const Test = () => {
  return (
    <div>
      <h1>Test1</h1>
    </div>
  );
};

output:

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
  ["568"],
  {
    58: function (e, s, t) {
      t.r(s),
        t.d(s, {
          Test: function () {
            return n;
          },
        });
      var c = t(250);
      let n = () =>
        (0, c.jsx)("div", {
          children: (0, c.jsx)("h1", { children: "Test1" }),
        });
    },
  },
]);
//# sourceMappingURL=568.js.map

As you see 568 from dist1 not equal to 568 from dist2.

For reproduction:

  1. pull repo
  2. npm ci
  3. git checkout v1.0.0
  4. npm run build
  5. save dist artifact with another name (dist1 in repo, but you can use another). Optional npx prettier --write [new dist name]
  6. git checkout v1.0.1
  7. npm run build
  8. save dist artifact with another name (dist2 in repo, but you can use another). Optional npx prettier --write [new dist name]

Expected behavior: Chunk from dist2 has another name

Can not find v1.0.0 and v1.0.1 in tags or branches, have you pushed them to github?

The chunk id will not change if the module relationships are not changed. Changing code of a module can only affect the content hash of the chunks which contains the changed module.

If you set chunkFilename to [id].js, then the runtime will generate url from chunk id directly. If you set chunkFilename to [id].[contenthash].js, then the runtime will generate a map from chunk id to chunk file name.

According to the image you reported before, it seems that the runtime used the old chunk map and try to load the new chunk which content hash had changed. But the old chunk map is in the main chunk which loaded by html plugin. And if the main chunk also has content hash, the html need to be refreshed after every change.

In your repro, there is no content hash, so the chunk filename will only contains the chunk id which will not always change like the content hash.

AndreyGladkov commented 1 month ago

Hi, added hashing https://github.com/AndreyGladkov/rspack-test/blob/main/rspack.config.ts#L16(checked for the following types: chunkhash, fullhash. for the value contenthash the hash changes as expected) I'm added SubComponent

export const SubComponent = () => {
  return (
    <div>
      <h1>Test1</h1>
    </div>
  );
};

and building to dest1:

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
    ["300"],
    {
        88: function (s, e, t) {
            t.r(e), t.d(e, { Test: () => r });
            var c = t("250");
            let h = () =>
                    (0, c.jsx)("div", {
                        children: (0, c.jsx)("h1", { children: "Test1" }),
                    }),
                r = () =>
                    (0, c.jsxs)("div", {
                        children: [
                            (0, c.jsx)("h1", { children: "Test2" }),
                            (0, c.jsx)(h, {}),
                        ],
                    });
        },
    },
]);
//# sourceMappingURL=300.5ba29289df5157a4ca31.js.map

I'm changed SubComponent:

export const SubComponent = () => {
  return (
    <div>
      <h1>Test2</h1>
    </div>
  );
};

and building to dest2

"use strict";
(self.webpackChunkrspack_test = self.webpackChunkrspack_test || []).push([
    ["300"],
    {
        88: function (s, e, t) {
            t.r(e), t.d(e, { Test: () => r });
            var c = t("250");
            let h = () =>
                    (0, c.jsx)("div", {
                        children: (0, c.jsx)("h1", { children: "Test2" }),
                    }),
                r = () =>
                    (0, c.jsxs)("div", {
                        children: [
                            (0, c.jsx)("h1", { children: "Test2" }),
                            (0, c.jsx)(h, {}),
                        ],
                    });
        },
    },
]);
//# sourceMappingURL=300.5ba29289df5157a4ca31.js.map

The content changes, but the chunk hashes do not:

dist1/300.5ba29289df5157a4ca31.js is equals to dist2/300.5ba29289df5157a4ca31.js

AndreyGladkov commented 1 month ago

@LingyuCoder hi, please look at the message above

LingyuCoder commented 1 month ago

There is a bug while using [chunkhash] in output.chunkFilename with optimization.concatenateModules which is enabled by default in production mode. I will try to fix it