From 9a1c61458789d2c76772bca2d10a7ca6b7b29e16 Mon Sep 17 00:00:00 2001 From: Qingyu Wang Date: Sun, 29 Mar 2026 17:34:26 +0800 Subject: [PATCH 1/3] fix: preserve 'use client' directive when injecting utilities --- src/index.ts | 18 ++++++++++++++--- test/rsc-test-react/src/index.js | 2 ++ test/rsc-test/index.test.ts | 33 ++++++++++++++++++++++++++++++++ test/rsc-test/src/index.js | 2 ++ 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 test/rsc-test-react/src/index.js create mode 100644 test/rsc-test/index.test.ts create mode 100644 test/rsc-test/src/index.js diff --git a/src/index.ts b/src/index.ts index 7bbba99..0e4bfbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,9 +178,21 @@ export const pluginTailwindCSS = ( path: resourcePath, }); - return `\ -import "${pathToFileURL(VIRTUAL_UTILITIES_ID)}?${params.toString()}"; -${code}`; + const importStr = `import "${pathToFileURL(VIRTUAL_UTILITIES_ID)}?${params.toString()}";\n`; + + const match = code.match( + /^(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*\n))*(?:(?:"[^"]*"|'[^']*')[ \t]*;?[\s]*)+/, + ); + + if (match) { + return ( + code.slice(0, match[0].length) + + importStr + + code.slice(match[0].length) + ); + } + + return importStr + code; }, ); diff --git a/test/rsc-test-react/src/index.js b/test/rsc-test-react/src/index.js new file mode 100644 index 0000000..c1e5e9f --- /dev/null +++ b/test/rsc-test-react/src/index.js @@ -0,0 +1,2 @@ +'use client'; +console.log('hello'); diff --git a/test/rsc-test/index.test.ts b/test/rsc-test/index.test.ts new file mode 100644 index 0000000..db0f139 --- /dev/null +++ b/test/rsc-test/index.test.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect, test } from '@playwright/test'; +import { createRsbuild } from '@rsbuild/core'; +import { pluginTailwindCSS } from '../../src'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +test('should preserve "use client" directive at the top of the file', async () => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + rsbuildConfig: { + plugins: [pluginTailwindCSS()], + output: { + minify: false, + }, + }, + }); + + await rsbuild.build(); + + const distPath = resolve(__dirname, './dist/static/js'); + const files = fs.readdirSync(distPath); + const mainFile = files.find((f) => f.endsWith('.js')); + if (!mainFile) { + throw new Error('No JS file found'); + } + const content = fs.readFileSync(resolve(distPath, mainFile), 'utf-8'); + + // "use client" should be present in the output + expect(content).toMatch(/['"]use client['"]/); +}); diff --git a/test/rsc-test/src/index.js b/test/rsc-test/src/index.js new file mode 100644 index 0000000..c1e5e9f --- /dev/null +++ b/test/rsc-test/src/index.js @@ -0,0 +1,2 @@ +'use client'; +console.log('hello'); From 2947f81467a30c70c2ff404c6321a22871c6c4fe Mon Sep 17 00:00:00 2001 From: Qingyu Wang Date: Sun, 29 Mar 2026 17:47:43 +0800 Subject: [PATCH 2/3] fix: refine regex to match inline comments after directives --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 0e4bfbc..012d647 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,7 +181,7 @@ export const pluginTailwindCSS = ( const importStr = `import "${pathToFileURL(VIRTUAL_UTILITIES_ID)}?${params.toString()}";\n`; const match = code.match( - /^(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*\n))*(?:(?:"[^"]*"|'[^']*')[ \t]*;?[\s]*)+/, + /^(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*\n?))*(?:(?:(?:"[^"]*"|'[^']*')[ \t]*;?)(?:[ \t]*\/\/[^\n]*\n?|[ \t]*\/\*[\s\S]*?\*\/|[\s]*))+/, ); if (match) { From 2cc19f7d85dff1d33feef01e62fbcbdabceea4c9 Mon Sep 17 00:00:00 2001 From: Qingyu Wang Date: Sun, 29 Mar 2026 20:56:54 +0800 Subject: [PATCH 3/3] fix(rsc): prevent catastrophic backtracking in directive regex The regular expression used to preserve "use client" and "use server" directives was flawed and could backtrack into single-line comments that contained string literal characters. This resulted in the virtual CSS import being injected inside comments or invalid positions, causing syntax errors in files with URL queries like `?url` or `?raw` (e.g. static-assets tests). This fix changes the comment and string literal matching to prevent backtracking and ensures we only match true top-level string directives. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 012d647..4c07437 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,7 +181,7 @@ export const pluginTailwindCSS = ( const importStr = `import "${pathToFileURL(VIRTUAL_UTILITIES_ID)}?${params.toString()}";\n`; const match = code.match( - /^(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*\n?))*(?:(?:(?:"[^"]*"|'[^']*')[ \t]*;?)(?:[ \t]*\/\/[^\n]*\n?|[ \t]*\/\*[\s\S]*?\*\/|[\s]*))+/, + /^(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*(?:\n|$)))*(?:(?:(?:"[^"\n\r]*"|'[^'\n\r]*')[ \t]*;?)(?:[\s]*|(?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\n]*(?:\n|$)))*)+/, ); if (match) {