diff --git a/.changeset/brave-news-sin.md b/.changeset/brave-news-sin.md new file mode 100644 index 0000000000..6084e844d9 --- /dev/null +++ b/.changeset/brave-news-sin.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/template-webpack-plugin": patch +--- + +Fix "Failed to load CSS update file" for lazy bundle diff --git a/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.css b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.css new file mode 100644 index 0000000000..a7aafb7083 --- /dev/null +++ b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.css @@ -0,0 +1,4 @@ +.LazyComponent { + font-weight: 700; + color: yellow; +} diff --git a/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.tsx b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.tsx new file mode 100644 index 0000000000..687db8a070 --- /dev/null +++ b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/LazyComponent.tsx @@ -0,0 +1,9 @@ +import './LazyComponent.css' + +export default function LazyComponent() { + return ( + + LazyComponent + + ) +} diff --git a/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.css b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.css new file mode 100644 index 0000000000..9929ab0322 --- /dev/null +++ b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.css @@ -0,0 +1,3 @@ +.Suspense { + background-color: red; +} diff --git a/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.tsx b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.tsx new file mode 100644 index 0000000000..cba1ea2cc6 --- /dev/null +++ b/packages/rspeedy/plugin-react/test/fixtures/lazy-bundle/index.tsx @@ -0,0 +1,14 @@ +import { Suspense, lazy } from '@lynx-js/react' +import './index.css' + +const LazyComponent = lazy(() => import('./LazyComponent.js')) + +export function App() { + return ( + + Loading...}> + + + + ) +} diff --git a/packages/rspeedy/plugin-react/test/lazy.test.ts b/packages/rspeedy/plugin-react/test/lazy.test.ts index 32494ebe26..5d56733205 100644 --- a/packages/rspeedy/plugin-react/test/lazy.test.ts +++ b/packages/rspeedy/plugin-react/test/lazy.test.ts @@ -1,11 +1,14 @@ // Copyright 2024 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. +import fs from 'node:fs/promises' import path from 'node:path' -import type { Rspack } from '@rsbuild/core' +import type { RsbuildPlugin, Rspack } from '@rsbuild/core' import { describe, expect, test, vi } from 'vitest' +import { LynxTemplatePlugin } from '@lynx-js/template-webpack-plugin' + import { createStubRspeedy as createRspeedy } from './createRspeedy.js' import { pluginStubRspeedyAPI } from './stub-rspeedy-api.plugin.js' @@ -170,4 +173,254 @@ describe('Lazy', () => { vi.unstubAllEnvs() }) }) + + test('lazy bundle beforeEncode entryNames', async () => { + vi.stubEnv('NODE_ENV', 'development') + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + + const entryNamesOfBeforeEncode: string[][] = [] + let backgroundJSContent = '' + + const rsbuild = await createRspeedy({ + rspeedyConfig: { + source: { + entry: { + main: new URL( + './fixtures/lazy-bundle/index.tsx', + import.meta.url, + ).pathname, + }, + }, + output: { + distPath: { + root: './dist/lazy-bundle', + }, + }, + plugins: [ + pluginReactLynx(), + { + name: 'test', + pre: ['lynx:react'], + setup(api) { + api.modifyBundlerChain((chain, { CHAIN_ID }) => { + const rule = chain.module + .rules.get('css:react:main-thread') + .uses.get(CHAIN_ID.USE.IGNORE_CSS) + rule.loader( + // add .ts suffix to ignore-css-loader + // this workaround is needed because vitest + // runs on our ts files. + rule.get('loader') as string + '.ts', + ) + }) + }, + } as RsbuildPlugin, + ], + tools: { + rspack: { + plugins: [ + { + name: 'extractBackgroundJSContent', + apply(compiler) { + compiler.hooks.compilation.tap( + 'extractBackgroundJSContent', + (compilation) => { + compilation.hooks.processAssets.tap( + 'extractBackgroundJSContent', + (assets) => { + for (const key in assets) { + if (/[\\/]background.js$/.test(key)) { + backgroundJSContent = assets[key]!.source() + .toString()! + } + } + }, + ) + }, + ) + }, + } as Rspack.RspackPluginInstance, + { + name: 'beforeEncode-test', + apply(compiler) { + compiler.hooks.compilation.tap( + 'beforeEncode-test', + (compilation) => { + const hooks = LynxTemplatePlugin + .getLynxTemplatePluginHooks( + compilation as unknown as Parameters< + typeof LynxTemplatePlugin.getLynxTemplatePluginHooks + >[0], + ) + hooks.beforeEncode.tap( + 'beforeEncode-test', + (args) => { + entryNamesOfBeforeEncode.push(args.entryNames) + + return args + }, + ) + }, + ) + }, + } as Rspack.RspackPluginInstance, + ], + }, + }, + }, + }) + + try { + await rsbuild.build() + + expect(entryNamesOfBeforeEncode).toMatchInlineSnapshot(` + [ + [ + "main__main-thread", + "main", + ], + [ + "./LazyComponent.js-react__main-thread", + "./LazyComponent.js-react__background", + ], + ] + `) + const cssHotUpdateList = + /\.cssHotUpdateList\s*=\s*(\[\[[\s\S]*?\]\])/.exec( + backgroundJSContent, + )![1] + expect(cssHotUpdateList).toMatchInlineSnapshot( + `"[["./LazyComponent.js-react__background",".rspeedy/async/./LazyComponent.js-react__background/./LazyComponent.js-react__background.css.hot-update.json"],["main",".rspeedy/main/main.css.hot-update.json"]]"`, + ) + } finally { + vi.unstubAllEnvs() + } + }) + + test('lazy bundle app-service.js should not load hot-update.js', async () => { + vi.stubEnv('NODE_ENV', 'development') + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + + let appServiceJSContent = '' + let done = false + const waitCompilationDone = () => + new Promise(resolve => { + const interval = setInterval(() => { + if (done) { + clearInterval(interval) + done = false + resolve(null) + } + }, 100) + }) + + const rsbuild = await createRspeedy({ + rspeedyConfig: { + source: { + entry: { + main: new URL( + './fixtures/lazy-bundle/index.tsx', + import.meta.url, + ).pathname, + }, + }, + output: { + distPath: { + root: './dist/lazy-bundle', + }, + }, + plugins: [ + pluginReactLynx(), + { + name: 'test', + pre: ['lynx:react'], + setup(api) { + api.modifyBundlerChain((chain, { CHAIN_ID }) => { + const rule = chain.module + .rules.get('css:react:main-thread') + .uses.get(CHAIN_ID.USE.IGNORE_CSS) + rule.loader( + // add .ts suffix to ignore-css-loader + // this workaround is needed because vitest + // runs on our ts files. + rule.get('loader') as string + '.ts', + ) + }) + }, + } as RsbuildPlugin, + ], + tools: { + rspack: { + plugins: [ + { + name: 'beforeEncode-test', + apply(compiler) { + compiler.hooks.compilation.tap( + 'beforeEncode-test', + (compilation) => { + const hooks = LynxTemplatePlugin + .getLynxTemplatePluginHooks( + compilation as unknown as Parameters< + typeof LynxTemplatePlugin.getLynxTemplatePluginHooks + >[0], + ) + hooks.beforeEmit.tap( + 'beforeEmit-test', + (args) => { + if ( + args.entryNames.some((name) => + name.includes('LazyComponent') + ) + ) { + appServiceJSContent = args.finalEncodeOptions + .manifest['/app-service.js']! + } + return args + }, + ) + }, + ) + compiler.hooks.done.tap('beforeEncode-test', () => { + done = true + }) + }, + } as Rspack.RspackPluginInstance, + ], + }, + }, + }, + }) + + const lazyComponentUrl = new URL( + './fixtures/lazy-bundle/LazyComponent.tsx', + import.meta.url, + ) + let tmpContent: string | undefined + + try { + await rsbuild.createDevServer() + await waitCompilationDone() + expect(appServiceJSContent).toMatchInlineSnapshot( + `"(function(){'use strict';function n({tt}){tt.define('/app-service.js',function(e,module,_,i,l,u,a,c,s,f,p,d,h,v,g,y,lynx){module.exports=lynx.requireModule("/static/js/async/./LazyComponent.js-react__background.js",globDynamicComponentEntry?globDynamicComponentEntry:'__Card__');});return tt.require('/app-service.js');}return{init:n}})()"`, + ) + + // Modify the fixtures/lazy-bundle/LazyComponent.tsx file + // to trigger HMR + tmpContent = await fs.readFile(lazyComponentUrl, 'utf-8') + await fs.writeFile( + lazyComponentUrl, + 'export default function LazyComponent() { return null }', + ) + await waitCompilationDone() + + expect(appServiceJSContent).toMatchInlineSnapshot( + `"(function(){'use strict';function n({tt}){tt.define('/app-service.js',function(e,module,_,i,l,u,a,c,s,f,p,d,h,v,g,y,lynx){module.exports=lynx.requireModule("/static/js/async/./LazyComponent.js-react__background.js",globDynamicComponentEntry?globDynamicComponentEntry:'__Card__');});return tt.require('/app-service.js');}return{init:n}})()"`, + ) + } finally { + if (tmpContent !== undefined) { + await fs.writeFile(lazyComponentUrl, tmpContent) + } + vi.unstubAllEnvs() + } + }) }) diff --git a/packages/webpack/template-webpack-plugin/src/LynxTemplatePlugin.ts b/packages/webpack/template-webpack-plugin/src/LynxTemplatePlugin.ts index 36a2067f47..5f16bda3ea 100644 --- a/packages/webpack/template-webpack-plugin/src/LynxTemplatePlugin.ts +++ b/packages/webpack/template-webpack-plugin/src/LynxTemplatePlugin.ts @@ -671,12 +671,16 @@ class LynxTemplatePluginImpl { await Promise.all( Object.entries(asyncChunkGroups).map( - ([entryName, chunkGroups]): Promise => { - const chunkNames = - // We use the chunk name(provided by `webpackChunkName`) as filename + ([_entryName, chunkGroups]): Promise => { + const entryNames = // We use the chunk name(provided by `webpackChunkName`) as filename chunkGroups - .filter(cg => cg.name !== null && cg.name !== undefined) - .map(cg => hooks.asyncChunkName.call(cg.name!)); + .filter(cg => cg.name !== null && cg.name !== undefined).map(cg => + cg.name! + ); + + const chunkNames = entryNames.map(name => + hooks.asyncChunkName.call(name) + ); const filename = Array.from(new Set(chunkNames)).join('_'); @@ -704,7 +708,7 @@ class LynxTemplatePluginImpl { return this.#encodeByAssetsInformation( compilation, asyncAssetsInfoByGroups, - [entryName], + entryNames, filenameTemplate, path.join(intermediateRoot, 'async', filename), /** isAsync */ true,