From bb55e8632ad5a5b3fe388d154b06a7926ba862cf Mon Sep 17 00:00:00 2001 From: Ole Kristian Losvik Date: Sat, 1 Aug 2026 22:57:04 +0100 Subject: [PATCH] fix(vite-config): banner-aware 'use client' directive detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preserve-use-client plugin checked only code.trimStart() for an existing directive, so chunks that begin with a banner comment (e.g. Rollup's output.banner, SPDX license headers) always failed the check and got a duplicate 'use client' prepended above the banner. Strip leading comments via a single shared helper that handles any interleaved mix of line and block comments — sequential per-style passes miss mixed orders. The same helper also replaces the weaker stripping in the source-module scan (hasClientDirective), which missed block-after-line orders and consecutive block comments, silently dropping the directive from output. Ported from ratio-ui, which carries the banner fix in its forked copy of the config. Co-Authored-By: Claude Fable 5 --- .changeset/banner-aware-use-client.md | 5 +++++ config/vite-config/src/react-lib.ts | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .changeset/banner-aware-use-client.md diff --git a/.changeset/banner-aware-use-client.md b/.changeset/banner-aware-use-client.md new file mode 100644 index 0000000..461184b --- /dev/null +++ b/.changeset/banner-aware-use-client.md @@ -0,0 +1,5 @@ +--- +'@eventuras/vite-config': patch +--- + +Skip leading banner/license comments (e.g. Rollup's `output.banner`) when checking whether a chunk already starts with a `'use client'` directive, so banner-carrying chunks no longer get a duplicate directive prepended. diff --git a/config/vite-config/src/react-lib.ts b/config/vite-config/src/react-lib.ts index 08edb83..cb60900 100644 --- a/config/vite-config/src/react-lib.ts +++ b/config/vite-config/src/react-lib.ts @@ -16,6 +16,15 @@ import { getRuntimeDependencyExternals, NODE_BUILTINS_EXTERNAL } from './externa */ const requireFromHere = createRequire(import.meta.url); +/** + * Strip leading whitespace and any interleaved mix of line and block comments, + * so a directive check sees the first real token. Sequential per-style passes + * miss mixed orders (a line comment, then a block comment, then the directive). + */ +function stripLeadingComments(code: string): string { + return code.replace(/^(?:\s+|\/\*[\s\S]*?\*\/\s*|\/\/.*(?:\r?\n|$)\s*)+/, ''); +} + /** * Plugin to preserve 'use client' directives in React Server Components. * This ensures client-side code is properly marked when building for Next.js. @@ -33,12 +42,10 @@ function preserveUseClient() { try { const content = fs.readFileSync(id, 'utf-8'); // Remove leading comments to check for 'use client' - const withoutComments = content - .replace(/^(\s*\/\/.*\n)+/, '') - .replace(/^(\s*\/\*[\s\S]*?\*\/\s*)/, ''); + const withoutComments = stripLeadingComments(content); return ( - withoutComments.trimStart().startsWith("'use client'") || - withoutComments.trimStart().startsWith('"use client"') + withoutComments.startsWith("'use client'") || + withoutComments.startsWith('"use client"') ); } catch { return false; @@ -46,7 +53,10 @@ function preserveUseClient() { }); if (hasClientDirective) { - const codeStart = chunkData.code.trimStart(); + // Ignore leading banner/license comments (e.g. Rollup's + // output.banner) when checking for the directive, so we don't + // prepend a duplicate above a directive that's already there. + const codeStart = stripLeadingComments(chunkData.code); if (!codeStart.startsWith("'use client'") && !codeStart.startsWith('"use client"')) { chunkData.code = `'use client';\n${chunkData.code}`; }