diff --git a/.changeset/curly-tires-tickle.md b/.changeset/curly-tires-tickle.md new file mode 100644 index 0000000000..eb31ee5989 --- /dev/null +++ b/.changeset/curly-tires-tickle.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/css-extract-webpack-plugin": patch +--- + +feat(css-extra-webpack-plugin): Support css hmr for lazy bundle diff --git a/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.cjs b/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.cjs index b68eb68bff..a061e44843 100644 --- a/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.cjs +++ b/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.cjs @@ -17,29 +17,38 @@ function debounce(fn, time) { } function updateStyle(cssId = 0) { - const filename = __webpack_require__.lynxCssFileName; - if (!filename) { - throw new Error('Css Filename not found'); + const cssHotUpdateList = __webpack_require__.cssHotUpdateList; + if (!cssHotUpdateList) { + throw new Error('cssHotUpdateList is not found'); } - lynx.requireModuleAsync( - // lynx.requireModuleAsync has two level hash and we cannot delete - // the LynxGroup level cache here. - // Temporarily using `Date.now` to avoid being cached. - __webpack_require__.p + filename, - (err, ret) => { - if (err) { - throw new Error('Load update css file `' + filename + '` failed'); - } - - if (ret.content) { - lynx.getCoreContext().dispatchEvent({ - type: 'lynx.hmr.css', - data: { cssId, content: ret.content, deps: ret.deps }, - }); - } - }, - ); + for (const [chunkName, cssHotUpdatePath] of cssHotUpdateList) { + lynx.requireModuleAsync( + // lynx.requireModuleAsync has two level hash and we cannot delete + // the LynxGroup level cache here. + // Temporarily using `Date.now` to avoid being cached. + __webpack_require__.p + cssHotUpdatePath, + (err, ret) => { + if (err) { + throw new Error( + `Failed to load CSS update file: ${cssHotUpdatePath}`, + ); + } + + if (ret.content) { + lynx.getCoreContext().dispatchEvent({ + type: 'lynx.hmr.css', + data: { + cssId, + content: ret.content, + deps: ret.deps, + entry: lynx.__chunk_entries__[chunkName], + }, + }); + } + }, + ); + } } /** diff --git a/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs b/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs index f77dbcb3e6..ea7656b5e8 100644 --- a/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs +++ b/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs @@ -2,13 +2,14 @@ function main() { try { lynx.getJSContext().addEventListener('lynx.hmr.css', (event) => { try { - const { data: { cssId, content, deps } } = event; + const { data: { cssId, content, deps, entry } } = event; // Update the css deps first because the css deps are updated actually. if (Array.isArray(deps[cssId])) { deps[cssId].forEach(depCSSId => { lynx.getDevtool().replaceStyleSheetByIdWithBase64( Number(depCSSId), content, + entry, ); }); } @@ -16,6 +17,7 @@ function main() { lynx.getDevtool().replaceStyleSheetByIdWithBase64( Number(cssId), content, + entry, ); __FlushElementTree(); diff --git a/packages/webpack/css-extract-webpack-plugin/src/CssExtractRspackPlugin.ts b/packages/webpack/css-extract-webpack-plugin/src/CssExtractRspackPlugin.ts index 3c6c0d9a1c..b4797a2bb9 100644 --- a/packages/webpack/css-extract-webpack-plugin/src/CssExtractRspackPlugin.ts +++ b/packages/webpack/css-extract-webpack-plugin/src/CssExtractRspackPlugin.ts @@ -170,6 +170,15 @@ class CssExtractRspackPluginImpl { this.name, (runtimeModule, chunk) => { if (runtimeModule.name === 'require_chunk_loading') { + const asyncChunks = Array.from(chunk.getAllAsyncChunks()) + .map(c => { + const { path } = compilation.getAssetPathWithInfo( + options.chunkFilename ?? '.rspeedy/async/[name]/[name].css', + { chunk: c }, + ); + return [c.name!, path]; + }); + const { path } = compilation.getPathWithInfo( options.filename ?? '[name].css', // Rspack does not pass JsChunk to Rust. @@ -177,13 +186,22 @@ class CssExtractRspackPluginImpl { { filename: chunk.name! }, ); - this.#overrideChunkLoadingRuntimeModule( - compiler, - runtimeModule, - path.replace( + const initialChunk = [chunk.name!, path]; + + const cssHotUpdateList = [...asyncChunks, initialChunk].map(( + [chunkName, cssHotUpdatePath], + ) => [ + chunkName!, + cssHotUpdatePath!.replace( '.css', `${this.hash ? `.${this.hash}` : ''}.css.hot-update.json`, ), + ]); + + this.#overrideChunkLoadingRuntimeModule( + compiler, + runtimeModule, + cssHotUpdateList, ); } }, @@ -253,7 +271,6 @@ class CssExtractRspackPluginImpl { true, ), ); - this.hash = compilation.hash; } catch (error) { if ( error && typeof error === 'object' && 'error_msg' in error @@ -272,6 +289,7 @@ class CssExtractRspackPluginImpl { } } } + this.hash = compilation.hash; }, ); } @@ -281,15 +299,15 @@ class CssExtractRspackPluginImpl { #overrideChunkLoadingRuntimeModule( compiler: Compiler, runtimeModule: RuntimeModule, - filename: string, + cssHotUpdateList: string[][], ) { const { RuntimeGlobals } = compiler.webpack; runtimeModule.source!.source = Buffer.concat([ Buffer.from(runtimeModule.source!.source), - // lynxCssFileName + // cssHotUpdateList Buffer.from(` - ${RuntimeGlobals.require}.lynxCssFileName = ${ - filename ? JSON.stringify(filename) : 'null' + ${RuntimeGlobals.require}.cssHotUpdateList = ${ + cssHotUpdateList ? JSON.stringify(cssHotUpdateList) : 'null' }; `), ]); diff --git a/packages/webpack/css-extract-webpack-plugin/test/helper/stubLynx.js b/packages/webpack/css-extract-webpack-plugin/test/helper/stubLynx.js index f04251638b..d556705c69 100644 --- a/packages/webpack/css-extract-webpack-plugin/test/helper/stubLynx.js +++ b/packages/webpack/css-extract-webpack-plugin/test/helper/stubLynx.js @@ -33,5 +33,6 @@ export function createStubLynx(vi, require, replaceStyleSheetByIdWithBase64) { getDevtool: vi.fn().mockReturnValue({ replaceStyleSheetByIdWithBase64, }), + __chunk_entries__: {}, }; } diff --git a/packages/webpack/css-extract-webpack-plugin/test/runtime.test.ts b/packages/webpack/css-extract-webpack-plugin/test/runtime.test.ts index adcf964a9b..3b5243600a 100644 --- a/packages/webpack/css-extract-webpack-plugin/test/runtime.test.ts +++ b/packages/webpack/css-extract-webpack-plugin/test/runtime.test.ts @@ -17,6 +17,11 @@ describe('HMR Runtime', () => { vi.stubGlobal('lynx', lynx); + lynx.__chunk_entries__ = { + 'chunkName': 'entry', + 'asyncChunkName': 'asyncEntry', + }; + beforeEach(() => { vi.clearAllMocks(); }); @@ -33,14 +38,14 @@ describe('HMR Runtime', () => { expect(vi.getTimerCount()).toBe(1); expect(() => vi.runAllTimers()).toThrowErrorMatchingInlineSnapshot( - `[Error: Css Filename not found]`, + `[Error: cssHotUpdateList is not found]`, ); }); test('cssFileName', () => { vi.stubGlobal('__webpack_require__', { p: '/', - lynxCssFileName: 'foo.css', + cssHotUpdateList: [['chunkName', 'foo.css']], }); vi.useFakeTimers(); @@ -63,7 +68,7 @@ describe('HMR Runtime', () => { await import('../runtime/hotModuleReplacement.lepus.cjs'); vi.stubGlobal('__webpack_require__', { p: '/', - lynxCssFileName: 'foo.css', + cssHotUpdateList: [['chunkName', 'foo.css']], }); const __FlushElementTree = vi.fn(); vi.stubGlobal('__FlushElementTree', __FlushElementTree); @@ -83,6 +88,7 @@ describe('HMR Runtime', () => { expect(replaceStyleSheetByIdWithBase64).toBeCalledWith( 10, expect.stringContaining('/foo.css'), + 'entry', ); expect(__FlushElementTree).toBeCalled(); @@ -92,7 +98,7 @@ describe('HMR Runtime', () => { await import('../runtime/hotModuleReplacement.lepus.cjs'); vi.stubGlobal('__webpack_require__', { p: '/', - lynxCssFileName: 'bar.css', + cssHotUpdateList: [['chunkName', 'bar.css']], }); const __FlushElementTree = vi.fn(); vi.stubGlobal('__FlushElementTree', __FlushElementTree); @@ -112,6 +118,7 @@ describe('HMR Runtime', () => { expect(replaceStyleSheetByIdWithBase64).toBeCalledWith( 0, expect.stringContaining('/bar.css'), + 'entry', ); expect(__FlushElementTree).toBeCalled(); @@ -121,7 +128,7 @@ describe('HMR Runtime', () => { await import('../runtime/hotModuleReplacement.lepus.cjs'); vi.stubGlobal('__webpack_require__', { p: 'https://example.com/', - lynxCssFileName: 'bar.css', + cssHotUpdateList: [['chunkName', 'bar.css']], }); const __FlushElementTree = vi.fn(); vi.stubGlobal('__FlushElementTree', __FlushElementTree); @@ -141,6 +148,49 @@ describe('HMR Runtime', () => { expect(replaceStyleSheetByIdWithBase64).toBeCalledWith( 0, expect.stringContaining('https://example.com/bar.css'), + 'entry', + ); + + expect(__FlushElementTree).toBeCalled(); + }); + + test('update lazy bundle', async () => { + await import('../runtime/hotModuleReplacement.lepus.cjs'); + vi.stubGlobal('__webpack_require__', { + p: '/', + cssHotUpdateList: [['asyncChunkName', 'async.bar.css'], [ + 'chunkName', + 'foo.css', + ]], + }); + const __FlushElementTree = vi.fn(); + vi.stubGlobal('__FlushElementTree', __FlushElementTree); + vi.useFakeTimers(); + + const cssReload = update('', null); + + cssReload(); + + // debounce + vi.runAllTimers(); + + // requireModuleAsync + await vi.runAllTimersAsync(); + + expect(replaceStyleSheetByIdWithBase64).toBeCalledTimes(2); + + expect(replaceStyleSheetByIdWithBase64).toHaveBeenNthCalledWith( + 1, + 0, + expect.stringContaining('async.bar.css'), + 'asyncEntry', + ); + + expect(replaceStyleSheetByIdWithBase64).toHaveBeenNthCalledWith( + 2, + 0, + expect.stringContaining('foo.css'), + 'entry', ); expect(__FlushElementTree).toBeCalled();