Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions e2e/cases/hmr/basic/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)');
},
);
40 changes: 40 additions & 0 deletions e2e/cases/hmr/esm/index.test.ts
Original file line number Diff line number Diff line change
@@ -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)');
},
);
9 changes: 9 additions & 0 deletions e2e/cases/hmr/esm/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
plugins: [pluginReact()],
output: {
module: true,
},
});
3 changes: 3 additions & 0 deletions e2e/cases/hmr/esm/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#test {
color: rgb(255, 0, 0);
}
4 changes: 4 additions & 0 deletions e2e/cases/hmr/esm/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './App.css';

const App = () => <div id="test">Hello Rsbuild!</div>;
export default App;
17 changes: 17 additions & 0 deletions e2e/cases/hmr/esm/src/index.ts
Original file line number Diff line number Diff line change
@@ -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));
}
15 changes: 12 additions & 3 deletions packages/core/src/plugins/esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
);
}

Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1598,7 +1598,10 @@ export interface HtmlConfig {
>;
/**
* Set the loading mode of the `<script>` tag.
* @default 'defer'
* - `'defer'`: The `<script>` tags are rendered with `defer` attribute.
* - `'module'`: The `<script>` tags are rendered with `type="module"` attribute.
* - `'blocking'`: The `<script>` tags are rendered without `defer` or `async` attribute.
* @default 'defer'. If `output.module` is enabled, the value is always `'module'`.
*/
scriptLoading?: ScriptLoading;
}
Expand Down
Loading