diff --git a/e2e/cases/hmr/basic/index.test.ts b/e2e/cases/hmr/basic/index.test.ts index b93daaae6a..d277e98ca6 100644 --- a/e2e/cases/hmr/basic/index.test.ts +++ b/e2e/cases/hmr/basic/index.test.ts @@ -2,7 +2,7 @@ import { join } from 'node:path'; import { expect, rspackTest } from '@e2e/helper'; rspackTest( - 'HMR should work by default', + 'should perform HMR and preserve state', async ({ page, dev, editFile, copySrcDir }) => { const tempSrc = await copySrcDir(); @@ -28,17 +28,13 @@ rspackTest( ); await expect(locator).toHaveText('Hello Test!'); - // #test-keep should remain unchanged when app.tsx HMR expect(await locatorKeep.innerHTML()).toBe(keepNum); await editFile( join(tempSrc, 'App.css'), - () => `#test { - color: rgb(0, 0, 255); -}`, + () => `#test { color: rgb(0, 0, 255); }`, ); - await expect(locator).toHaveCSS('color', 'rgb(0, 0, 255)'); }, ); diff --git a/e2e/cases/hmr/esm/index.test.ts b/e2e/cases/hmr/esm/index.test.ts new file mode 100644 index 0000000000..cff07cef42 --- /dev/null +++ b/e2e/cases/hmr/esm/index.test.ts @@ -0,0 +1,40 @@ +import { join } from 'node:path'; +import { expect, rspackTest } from '@e2e/helper'; + +rspackTest( + 'should perform HMR and preserve state when `output.module` is enabled', + async ({ page, dev, editFile, copySrcDir }) => { + const tempSrc = await copySrcDir(); + + await dev({ + config: { + source: { + entry: { + index: join(tempSrc, 'index.ts'), + }, + }, + }, + }); + + const locator = page.locator('#test'); + await expect(locator).toHaveText('Hello Rsbuild!'); + await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)'); + + const locatorKeep = page.locator('#test-keep'); + const keepNum = await locatorKeep.innerHTML(); + + await editFile(join(tempSrc, 'App.tsx'), (code) => + code.replace('Hello Rsbuild', 'Hello Test'), + ); + + await expect(locator).toHaveText('Hello Test!'); + // #test-keep should remain unchanged when app.tsx HMR + expect(await locatorKeep.innerHTML()).toBe(keepNum); + + await editFile( + join(tempSrc, 'App.css'), + () => `#test { color: rgb(0, 0, 255); }`, + ); + await expect(locator).toHaveCSS('color', 'rgb(0, 0, 255)'); + }, +); diff --git a/e2e/cases/hmr/esm/rsbuild.config.ts b/e2e/cases/hmr/esm/rsbuild.config.ts new file mode 100644 index 0000000000..a1be526ccb --- /dev/null +++ b/e2e/cases/hmr/esm/rsbuild.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; + +export default defineConfig({ + plugins: [pluginReact()], + output: { + module: true, + }, +}); diff --git a/e2e/cases/hmr/esm/src/App.css b/e2e/cases/hmr/esm/src/App.css new file mode 100644 index 0000000000..f49dc220b6 --- /dev/null +++ b/e2e/cases/hmr/esm/src/App.css @@ -0,0 +1,3 @@ +#test { + color: rgb(255, 0, 0); +} diff --git a/e2e/cases/hmr/esm/src/App.tsx b/e2e/cases/hmr/esm/src/App.tsx new file mode 100644 index 0000000000..44baa4fda4 --- /dev/null +++ b/e2e/cases/hmr/esm/src/App.tsx @@ -0,0 +1,4 @@ +import './App.css'; + +const App = () =>
Hello Rsbuild!
; +export default App; diff --git a/e2e/cases/hmr/esm/src/index.ts b/e2e/cases/hmr/esm/src/index.ts new file mode 100644 index 0000000000..0f9a1befc1 --- /dev/null +++ b/e2e/cases/hmr/esm/src/index.ts @@ -0,0 +1,17 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +const num = Math.ceil(Math.random() * 100); +const testEl = document.createElement('div'); +testEl.id = 'test-keep'; + +testEl.innerHTML = String(num); + +document.body.appendChild(testEl); + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(React.createElement(App)); +} diff --git a/packages/core/src/plugins/esm.ts b/packages/core/src/plugins/esm.ts index d9988ffd6e..7031b37d86 100644 --- a/packages/core/src/plugins/esm.ts +++ b/packages/core/src/plugins/esm.ts @@ -4,16 +4,25 @@ export const pluginEsm = (): RsbuildPlugin => ({ name: 'rsbuild:esm', setup(api) { - api.modifyBundlerChain((chain, { environment, isServer }) => { + api.modifyBundlerChain((chain, { environment, target }) => { const { config } = environment; if (!config.output.module) { return; } - if (!isServer) { + if (target === 'web') { + api.logger.warn( + '[rsbuild:config] `output.module` for web target is experimental and may not work as expected.', + ); + + // Temporary solution to fix the issue of runtime chunk not loaded as expected. + chain.optimization.runtimeChunk(true); + } + + if (target === 'web-worker') { throw new Error( - '[rsbuild:config] `output.module` is only supported for Node.js targets.', + '[rsbuild:config] `output.module` is not supported for web-worker target.', ); } diff --git a/packages/core/src/plugins/html.ts b/packages/core/src/plugins/html.ts index 234f84a462..5f603ccd99 100644 --- a/packages/core/src/plugins/html.ts +++ b/packages/core/src/plugins/html.ts @@ -260,7 +260,9 @@ export const pluginHtml = (context: InternalContext): RsbuildPlugin => ({ filename, entryName, templateParameters, - scriptLoading: config.html.scriptLoading, + scriptLoading: config.output.module + ? 'module' + : config.html.scriptLoading, }; if (templatePath) { diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index ee3d50ded4..75f23d8cf3 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -1598,7 +1598,10 @@ export interface HtmlConfig { >; /** * Set the loading mode of the `