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,