Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a66dd0a
refactor: use post transfer module ids
stormslowly Jan 8, 2026
8149c31
test: skip lazy active method
stormslowly Jan 8, 2026
f8e3322
Merge remote-tracking branch 'origin/main' into refactor/lazy_active_…
stormslowly Jan 8, 2026
cef1dc7
test: add lazy post request help
stormslowly Jan 8, 2026
093bf25
test: add large module id container
stormslowly Jan 8, 2026
cf5349f
test: add case introduction
stormslowly Jan 9, 2026
b253637
refactor: remove about controller
stormslowly Jan 9, 2026
78d904c
Merge remote-tracking branch 'origin/main' into refactor/lazy_active_…
stormslowly Jan 9, 2026
0c136db
test: we are using post method now
stormslowly Jan 9, 2026
890aede
refactor: rename
stormslowly Jan 9, 2026
5ace8c0
Merge remote-tracking branch 'origin/main' into refactor/lazy_active_…
stormslowly Jan 12, 2026
f9f8d98
fix body parser (vibe-kanban 09797829)
stormslowly Jan 12, 2026
332146a
refactor read module ids from body (vibe-kanban a7874ac1)
stormslowly Jan 12, 2026
a0619ae
refactor: downgrading to lower web api
stormslowly Jan 13, 2026
f4f10d6
test: ✅ add lazy compilation active cors cases
stormslowly Jan 13, 2026
2ba3cd6
chore: update test case doc
stormslowly Jan 13, 2026
5d8eeb5
fix: we all need cors header no matter is simple request or not
stormslowly Jan 13, 2026
401d886
refactor:delete cors header setting
stormslowly Jan 14, 2026
363dfa5
refactor: set cors header should set by user
stormslowly Jan 14, 2026
3be7a91
Merge branch 'main' into refactor/lazy_active_module_id_goes_to_post_…
stormslowly Jan 14, 2026
bd739dd
Update packages/rspack/hot/lazy-compilation-web.js
stormslowly Jan 14, 2026
bfd543e
test: fix case name
stormslowly Jan 14, 2026
0487659
Merge branch 'refactor/lazy_active_module_id_goes_to_post_body' of gi…
stormslowly Jan 14, 2026
2064a1b
chore: api-extract update
stormslowly Jan 14, 2026
0c66e27
fix: memory leak of requst listeners
stormslowly Jan 14, 2026
5b2ff93
refactor: remove event source handle logic
stormslowly Jan 14, 2026
bf0fc1f
fix: jsdom XMLHTTPRequets need strict cors header
stormslowly Jan 14, 2026
c0078ad
refactor: node lazy compilation client use post too
stormslowly Jan 14, 2026
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
7 changes: 6 additions & 1 deletion packages/rspack-test-tools/src/runner/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ export class WebRunner extends NodeRunner {
protected createBaseModuleScope() {
const moduleScope = super.createBaseModuleScope();
moduleScope.EventSource = EventSource;
moduleScope.fetch = async (url: string) => {
moduleScope.fetch = async (url: string, options: any) => {
// For Lazy Compilation Proxy the POST request to the real dev server.
if (options?.method === 'POST') {
return fetch(url, options as any);
}

try {
const filePath = this.urlToPath(url);
this.log(`fetch: ${url} -> ${filePath}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/rspack/etc/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4289,7 +4289,7 @@ interface LabeledStatement extends Node_4, HasSpan {
// @public
export type Layer = string | null;

// @public
// @public (undocumented)
export const lazyCompilationMiddleware: (compiler: Compiler | MultiCompiler) => MiddlewareHandler;

// @public
Expand Down
94 changes: 60 additions & 34 deletions packages/rspack/hot/lazy-compilation-web.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,70 @@
if (typeof EventSource !== 'function') {
if (typeof XMLHttpRequest === 'undefined') {
throw new Error(
"Environment doesn't support lazy compilation (requires EventSource)",
"Environment doesn't support lazy compilation (requires XMLHttpRequest)",
);
}

var urlBase = decodeURIComponent(__resourceQuery.slice(1));
/** @type {EventSource | undefined} */
var activeEventSource;
var compiling = new Set();
var errorHandlers = new Set();

var updateEventSource = function updateEventSource() {
if (activeEventSource) activeEventSource.close();
if (compiling.size) {
activeEventSource = new EventSource(
urlBase +
Array.from(compiling, function (module) {
return encodeURIComponent(module);
}).join('@'),
);
/**
* @this {EventSource}
* @param {Event & { message?: string, filename?: string, lineno?: number, colno?: number, error?: Error }} event event
*/
activeEventSource.onerror = function (event) {
errorHandlers.forEach(function (onError) {
onError(
new Error(
'Problem communicating active modules to the server' +
(event.message ? `: ${event.message} ` : '') +
(event.filename ? `: ${event.filename} ` : '') +
(event.lineno ? `: ${event.lineno} ` : '') +
(event.colno ? `: ${event.colno} ` : '') +
(event.error ? `: ${event.error}` : ''),
),
/** @type {XMLHttpRequest | undefined} */
var pendingXhr;
/** @type {boolean} */
var hasPendingUpdate = false;

var sendRequest = function sendRequest() {
if (compiling.size === 0) {
hasPendingUpdate = false;
return;
}

var modules = Array.from(compiling);
var data = modules.join('\n');

var xhr = new XMLHttpRequest();
pendingXhr = xhr;
xhr.open('POST', urlBase, true);
// text/plain Content-Type is simple request header
xhr.setRequestHeader('Content-Type', 'text/plain');

xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
pendingXhr = undefined;
if (xhr.status < 200 || xhr.status >= 300) {
var error = new Error(
'Problem communicating active modules to the server: HTTP ' +
xhr.status,
);
});
};
} else {
activeEventSource = undefined;
errorHandlers.forEach(function (onError) {
onError(error);
});
}
if (hasPendingUpdate) {
hasPendingUpdate = false;
sendRequest();
}
}
};

xhr.onerror = function () {
pendingXhr = undefined;
var error = new Error('Problem communicating active modules to the server');
errorHandlers.forEach(function (onError) {
onError(error);
});
Comment thread
stormslowly marked this conversation as resolved.
};

xhr.send(data);
};

var sendActiveRequest = function sendActiveRequest() {
Comment thread
stormslowly marked this conversation as resolved.
Outdated
hasPendingUpdate = true;

// If no request is pending, start one
if (!pendingXhr) {
hasPendingUpdate = false;
sendRequest();
}
};

Expand All @@ -55,7 +81,7 @@ exports.activate = function (options) {

if (!compiling.has(data)) {
compiling.add(data);
updateEventSource();
sendActiveRequest();
}

if (!active && !module.hot) {
Expand All @@ -67,6 +93,6 @@ exports.activate = function (options) {
return function () {
errorHandlers.delete(onError);
compiling.delete(data);
updateEventSource();
sendActiveRequest();
};
};
70 changes: 53 additions & 17 deletions packages/rspack/src/builtin-plugin/lazy-compilation/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ const DEPRECATED_LAZY_COMPILATION_OPTIONS_WARN =
const REPEAT_LAZY_COMPILATION_OPTIONS_WARN =
'Both top-level `lazyCompilation` and `experiments.lazyCompilation` options are set. The top-level `lazyCompilation` configuration will take precedence.';

/**
* Create a middleware that handles lazy compilation requests from the client.
* This function returns an Express-style middleware that listens for
* requests triggered by lazy compilation in the dev server client,
* then invokes the Rspack compiler to compile modules on demand.
* Use this middleware when integrating lazy compilation into a
* custom development server instead of relying on the built-in server.
*/
export const lazyCompilationMiddleware = (
compiler: Compiler | MultiCompiler,
): MiddlewareHandler => {
Expand Down Expand Up @@ -88,7 +80,6 @@ export const lazyCompilationMiddleware = (
}

const options = {
// TODO: remove this when experiments.lazyCompilation is removed
...c.options.experiments.lazyCompilation,
...c.options.lazyCompilation,
};
Expand Down Expand Up @@ -139,7 +130,6 @@ export const lazyCompilationMiddleware = (
const activeModules: Set<string> = new Set();

const options = {
// TODO: remove this when experiments.lazyCompilation is removed
Comment thread
stormslowly marked this conversation as resolved.
...compiler.options.experiments.lazyCompilation,
...compiler.options.lazyCompilation,
};
Expand Down Expand Up @@ -173,24 +163,67 @@ function applyPlugin(
plugin.apply(compiler);
}

// used for reuse code, do not export this
function readModuleIdsFromBody(
req: IncomingMessage & { body?: unknown },
): Promise<string[]> {
// If body is already parsed by another middleware, use it directly
if (req.body !== undefined) {
if (Array.isArray(req.body)) {
return Promise.resolve(req.body);
}
if (typeof req.body === 'string') {
return Promise.resolve(req.body.split('\n').filter(Boolean));
}
throw new Error('Invalid body type');
Comment thread
stormslowly marked this conversation as resolved.
}

return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
req.on('end', () => {
// Concatenate all chunks and decode as UTF-8 to handle multi-byte characters correctly
const body = Buffer.concat(chunks).toString('utf8');
resolve(body.split('\n').filter(Boolean));
Comment thread
stormslowly marked this conversation as resolved.
Outdated
});
req.on('error', reject);
Comment thread
stormslowly marked this conversation as resolved.
Outdated
});
Comment thread
stormslowly marked this conversation as resolved.
}

const lazyCompilationMiddlewareInternal = (
compiler: Compiler | MultiCompiler,
activeModules: Set<string>,
lazyCompilationPrefix: string,
): MiddlewareHandler => {
const logger = compiler.getInfrastructureLogger('LazyCompilation');

return (req: IncomingMessage, res: ServerResponse, next?: () => void) => {
return async (
req: IncomingMessage,
res: ServerResponse,
next?: () => void,
) => {
if (!req.url?.startsWith(lazyCompilationPrefix)) {
// only handle requests that are come from lazyCompilation
return next?.();
}

const modules = req.url
.slice(lazyCompilationPrefix.length)
.split('@')
.map(decodeURIComponent);
let modules: string[] = [];
if (req.method === 'POST') {
try {
modules = await readModuleIdsFromBody(req);
} catch (err) {
logger.error('Failed to parse request body: ' + err);
res.writeHead(400);
res.end('Bad Request');
return;
}
Comment thread
stormslowly marked this conversation as resolved.
Outdated
} else {
Comment thread
stormslowly marked this conversation as resolved.
Outdated
modules = req.url
.slice(lazyCompilationPrefix.length)
.split('@')
.map(decodeURIComponent);
}

req.socket.setNoDelay(true);

res.setHeader('content-type', 'text/event-stream');
Expand All @@ -210,5 +243,8 @@ const lazyCompilationMiddlewareInternal = (
if (moduleActivated.length && compiler.watching) {
compiler.watching.invalidate();
}
if (req.method === 'POST') {
res.end();
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, test } from '@/fixtures';

test('should update style', async ({ page }) => {
Comment thread
stormslowly marked this conversation as resolved.
Outdated
const body = await page.locator('body');
await expect(body).toContainText('All Modules Loaded');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const { rspack } = require('@rspack/core');

/*
Construct a project with lots of virtual files with very long file names
And `virtual_index.js` dynamically imports all the long file name files.
src/
├── virtual_index.js
├── virtual_with_a_very_long_file_name_number_....._0.js
...
└── virtual_with_a_very_long_file_name_number_....._19.js
*/
let lotsLongFileNameVirtualFiles = {};
let longStr = new Array(1024).fill('a').join('');
for (let i = 0; i < 20; i++) {
lotsLongFileNameVirtualFiles[
`src/virtual_with_a_very_long_file_name_number_${longStr}_${i}.js`
] = `"dynamic_imported"`;
}
let allFiles = Object.keys(lotsLongFileNameVirtualFiles);
lotsLongFileNameVirtualFiles['src/virtual_index.js'] = `
Promise.all([
${allFiles.map((file) => `import('./${file.slice(3)}')\n`).join(',')}
]).then(()=> document.body.innerHTML = 'All Modules Loaded');
Comment thread
stormslowly marked this conversation as resolved.
`;

/** @type { import('@rspack/core').RspackOptions } */
module.exports = {
context: __dirname,
entry: './src/virtual_index.js',
mode: 'development',
lazyCompilation: true,
devServer: {
hot: true,
port: 5678,
},
plugins: [
new rspack.HtmlRspackPlugin(),
new rspack.experiments.VirtualModulesPlugin(lotsLongFileNameVirtualFiles),
],
experiments: {
useInputFileSystem: [/virtual/],
},
};
46 changes: 46 additions & 0 deletions tests/e2e/cases/lazy-compilation/cross-origin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Cross-Origin Lazy Compilation Test

This test verifies that lazy compilation works correctly
when the lazy compilation server runs on a different origin (port) than the frontend dev server.

## Architecture

```
+----------------------+ +----------------------+ +-----------------------------+
| | | | | |
| Browser | | Dev Server | | Lazy Compilation Server |
| (Frontend) | | (Port: 8500) | | (Port: 8600) |
| | | | | |
+----------+-----------+ +----------+-----------+ +--------------+--------------+
| | |
| 1. Load page | |
| GET http://localhost:8500 |
+-------------------------> |
| | |
| 2. Click button -> dynamic import() |
| | |
| 3. Cross-origin POST request |
| POST http://127.0.0.1:8600/lazy-... |
| Content-Type: text/plain |
| Body: "module-id-1\nmodule-id-2" |
+---------------------------------------------------------->
| | |
| 4. Response (SSE or empty for POST) |
<----------------------------------------------------------+
| | |
| 5. Webpack invalidate & rebuild |
| | |
| 6. Load compiled chunk |
v | |
+----------------------+ | |
| Component | | |
| Rendered | | |
+----------------------+ | |
```

## Key Points

1. **Two Separate Servers**: Frontend runs on port 8500, lazy compilation on port 8600
2. **Cross-Origin Request**: Browser sends POST request to a different origin
3. **Simple Request**: Uses `Content-Type: text/plain` to avoid CORS preflight
4. **XMLHttpRequest**: Uses XHR instead of fetch for better browser compatibility
Comment thread
stormslowly marked this conversation as resolved.
Loading
Loading