From a648434ecf3ea424885083a7217541b9b91169bb Mon Sep 17 00:00:00 2001 From: igz0 <37741728+igz0@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:08:15 +0900 Subject: [PATCH 1/5] fix(nextjs): preserve directive prologues in turbopack loaders --- .../loaders/moduleMetadataInjectionLoader.ts | 7 +- .../config/loaders/valueInjectionLoader.ts | 154 ++++++++++++++++-- .../moduleMetadataInjectionLoader.test.ts | 26 +++ .../test/config/valueInjectionLoader.test.ts | 73 ++++++++- 4 files changed, 243 insertions(+), 17 deletions(-) diff --git a/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts index 96c00569e06f..b26eb452e13b 100644 --- a/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts @@ -1,5 +1,5 @@ import type { LoaderThis } from './types'; -import { SKIP_COMMENT_AND_DIRECTIVE_REGEX } from './valueInjectionLoader'; +import { findInjectionIndexAfterDirectives } from './valueInjectionLoader'; export type ModuleMetadataInjectionLoaderOptions = { applicationKey: string; @@ -39,7 +39,6 @@ export default function moduleMetadataInjectionLoader( `e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],${metadata});` + '}catch(e){}}();'; - return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => { - return match + injectedCode; - }); + const injectionIndex = findInjectionIndexAfterDirectives(userCode); + return `${userCode.slice(0, injectionIndex)}${injectedCode}${userCode.slice(injectionIndex)}`; } diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 62cabcf818b8..e7c19c30fc68 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -1,18 +1,149 @@ -// Rollup doesn't like if we put the directive regex as a literal (?). No idea why. -/* oxlint-disable sdk/no-regexp-constructor */ - import type { LoaderThis } from './types'; export type ValueInjectionLoaderOptions = { values: Record; }; -// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other directive. -// As an additional complication directives may come after any number of comments. -// This regex is shamelessly stolen from: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/7f984482c73e4284e8b12a08dfedf23b5a82f0af/packages/bundler-plugin-core/src/index.ts#L535-L539 -export const SKIP_COMMENT_AND_DIRECTIVE_REGEX = - // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. - new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?'); +// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other +// directives. A small scanner is easier to reason about than the previous regex and avoids regex backtracking concerns. +export function findInjectionIndexAfterDirectives(userCode: string): number { + let index = 0; + let lastDirectiveEndIndex: number | undefined; + + while (true) { + const statementStartIndex = skipWhitespaceAndComments(userCode, index); + + const nextDirectiveIndex = skipDirective(userCode, statementStartIndex); + if (nextDirectiveIndex === undefined) { + return lastDirectiveEndIndex ?? statementStartIndex; + } + + const statementEndIndex = skipDirectiveTerminator(userCode, nextDirectiveIndex); + if (statementEndIndex === undefined) { + return lastDirectiveEndIndex ?? statementStartIndex; + } + + index = statementEndIndex; + lastDirectiveEndIndex = statementEndIndex; + } +} + +function skipWhitespaceAndComments(userCode: string, startIndex: number): number { + let index = startIndex; + + while (index < userCode.length) { + const char = userCode[index]; + const nextChar = userCode[index + 1]; + + if (char && /\s/.test(char)) { + index += 1; + continue; + } + + if (char === '/' && nextChar === '/') { + index += 2; + while (index < userCode.length && userCode[index] !== '\n' && userCode[index] !== '\r') { + index += 1; + } + continue; + } + + if (char === '/' && nextChar === '*') { + const commentEndIndex = userCode.indexOf('*/', index + 2); + if (commentEndIndex === -1) { + return startIndex; + } + + index = commentEndIndex + 2; + continue; + } + + return index; + } + + return index; +} + +function skipDirective(userCode: string, startIndex: number): number | undefined { + const quote = userCode[startIndex]; + + if (quote !== '"' && quote !== "'") { + return undefined; + } + + let index = startIndex + 1; + + while (index < userCode.length) { + const char = userCode[index]; + + if (char === '\\') { + index += 2; + continue; + } + + if (char === quote) { + index += 1; + break; + } + + if (char === '\n' || char === '\r') { + return undefined; + } + + index += 1; + } + + if (index > userCode.length || userCode[index - 1] !== quote) { + return undefined; + } + + return index; +} + +function skipDirectiveTerminator(userCode: string, startIndex: number): number | undefined { + let index = startIndex; + + while (index < userCode.length) { + const char = userCode[index]; + const nextChar = userCode[index + 1]; + + if (char === ';') { + return index + 1; + } + + if (char === '\n' || char === '\r' || char === '}') { + return index; + } + + if (char && /\s/.test(char)) { + index += 1; + continue; + } + + if (char === '/' && nextChar === '/') { + return index; + } + + if (char === '/' && nextChar === '*') { + const commentEndIndex = userCode.indexOf('*/', index + 2); + if (commentEndIndex === -1) { + return undefined; + } + + const comment = userCode.slice(index + 2, commentEndIndex); + if (comment.includes('\n') || comment.includes('\r')) { + return index; + } + + index = commentEndIndex + 2; + continue; + } + + return undefined; + } + + return index; +} /** * Set values on the global/window object at the start of a module. @@ -36,7 +167,6 @@ export default function valueInjectionLoader(this: LoaderThis `globalThis["${key}"] = ${JSON.stringify(value)};`) .join(''); - return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => { - return match + injectedCode; - }); + const injectionIndex = findInjectionIndexAfterDirectives(userCode); + return `${userCode.slice(0, injectionIndex)}${injectedCode}${userCode.slice(injectionIndex)}`; } diff --git a/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts index 1a6a2cd14b71..f6c1c613bd00 100644 --- a/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts +++ b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts @@ -131,4 +131,30 @@ describe('moduleMetadataInjectionLoader', () => { expect(result).toContain('"_sentryBundlerPluginAppKey:test-key-123":true'); }); + + it('should inject after multiple directives', () => { + const loaderThis = createLoaderThis('my-app'); + const userCode = '"use strict";\n"use client";\nimport React from \'react\';'; + + const result = moduleMetadataInjectionLoader.call(loaderThis, userCode); + + const metadataIndex = result.indexOf('_sentryModuleMetadata'); + const clientDirectiveIndex = result.indexOf('"use client"'); + const importIndex = result.indexOf("import React from 'react';"); + + expect(metadataIndex).toBeGreaterThan(clientDirectiveIndex); + expect(metadataIndex).toBeLessThan(importIndex); + }); + + it('should inject after comments between multiple directives', () => { + const loaderThis = createLoaderThis('my-app'); + const userCode = '"use strict";\n/* keep */\n"use client";\nimport React from \'react\';'; + + const result = moduleMetadataInjectionLoader.call(loaderThis, userCode); + + const metadataIndex = result.indexOf('_sentryModuleMetadata'); + const clientDirectiveIndex = result.indexOf('"use client"'); + + expect(metadataIndex).toBeGreaterThan(clientDirectiveIndex); + }); }); diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts index 57b40b006baa..cd6c93e31690 100644 --- a/packages/nextjs/test/config/valueInjectionLoader.test.ts +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { LoaderThis } from '../../src/config/loaders/types'; import type { ValueInjectionLoaderOptions } from '../../src/config/loaders/valueInjectionLoader'; -import valueInjectionLoader from '../../src/config/loaders/valueInjectionLoader'; +import valueInjectionLoader, { findInjectionIndexAfterDirectives } from '../../src/config/loaders/valueInjectionLoader'; const defaultLoaderThis = { addDependency: () => undefined, @@ -149,4 +149,75 @@ describe.each([[clientConfigLoaderThis], [instrumentationLoaderThis]])('valueInj expect(result).toMatchSnapshot(); expect(result).toMatch(';globalThis["foo"] = "bar";'); }); + + it('should correctly insert values after multiple directives', () => { + const userCode = ` + "use strict"; + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + const injectionIndex = result.indexOf(';globalThis["foo"] = "bar";'); + const clientDirectiveIndex = result.indexOf('"use client"'); + const importIndex = result.indexOf("import * as Sentry from '@sentry/nextjs';"); + + expect(injectionIndex).toBeGreaterThan(clientDirectiveIndex); + expect(injectionIndex).toBeLessThan(importIndex); + }); + + it('should correctly insert values after comments between multiple directives', () => { + const userCode = ` + "use strict"; + /* keep */ + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + const injectionIndex = result.indexOf(';globalThis["foo"] = "bar";'); + const clientDirectiveIndex = result.indexOf('"use client"'); + + expect(injectionIndex).toBeGreaterThan(clientDirectiveIndex); + }); + + it('should correctly insert values after semicolon-free directives', () => { + const userCode = ` + "use strict" + "use client" + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + const injectionIndex = result.indexOf(';globalThis["foo"] = "bar";'); + const clientDirectiveIndex = result.indexOf('"use client"'); + + expect(injectionIndex).toBeGreaterThan(clientDirectiveIndex); + }); +}); + +describe('findInjectionIndexAfterDirectives', () => { + it('returns the position immediately after the last directive', () => { + const userCode = '"use strict";\n"use client";\nimport React from \'react\';'; + + expect(userCode.slice(findInjectionIndexAfterDirectives(userCode))).toBe("\nimport React from 'react';"); + }); + + it('does not skip a string literal that is not a directive', () => { + const userCode = '"use client" + suffix;'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + }); + + it('treats a block comment without a line break as part of the same statement', () => { + const userCode = '"use client" /* comment */ + suffix;'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + }); }); From dd6204fab4e924320d262778f8f614dde2ae48b1 Mon Sep 17 00:00:00 2001 From: igz0 <37741728+igz0@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:54:38 +0900 Subject: [PATCH 2/5] refactor(nextjs): make directive scan exit explicit --- packages/nextjs/src/config/loaders/valueInjectionLoader.ts | 4 +++- packages/nextjs/test/config/valueInjectionLoader.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index e7c19c30fc68..8251a7815842 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -10,7 +10,7 @@ export function findInjectionIndexAfterDirectives(userCode: string): number { let index = 0; let lastDirectiveEndIndex: number | undefined; - while (true) { + while (index < userCode.length) { const statementStartIndex = skipWhitespaceAndComments(userCode, index); const nextDirectiveIndex = skipDirective(userCode, statementStartIndex); @@ -26,6 +26,8 @@ export function findInjectionIndexAfterDirectives(userCode: string): number { index = statementEndIndex; lastDirectiveEndIndex = statementEndIndex; } + + return lastDirectiveEndIndex ?? index; } function skipWhitespaceAndComments(userCode: string, startIndex: number): number { diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts index cd6c93e31690..38b0a4ac780a 100644 --- a/packages/nextjs/test/config/valueInjectionLoader.test.ts +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -209,6 +209,12 @@ describe('findInjectionIndexAfterDirectives', () => { expect(userCode.slice(findInjectionIndexAfterDirectives(userCode))).toBe("\nimport React from 'react';"); }); + it('returns the end of the input when the last directive reaches EOF', () => { + const userCode = '"use strict";\n"use client";'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe(userCode.length); + }); + it('does not skip a string literal that is not a directive', () => { const userCode = '"use client" + suffix;'; From 81516b3a34de4586da24c6fa734adfcbc23800f7 Mon Sep 17 00:00:00 2001 From: igz0 <37741728+igz0@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:07:53 +0900 Subject: [PATCH 3/5] fix(nextjs): handle unterminated escaped directives --- packages/nextjs/src/config/loaders/valueInjectionLoader.ts | 4 +++- packages/nextjs/test/config/valueInjectionLoader.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 8251a7815842..0fbec19e7117 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -74,6 +74,7 @@ function skipDirective(userCode: string, startIndex: number): number | undefined } let index = startIndex + 1; + let foundClosingQuote = false; while (index < userCode.length) { const char = userCode[index]; @@ -85,6 +86,7 @@ function skipDirective(userCode: string, startIndex: number): number | undefined if (char === quote) { index += 1; + foundClosingQuote = true; break; } @@ -95,7 +97,7 @@ function skipDirective(userCode: string, startIndex: number): number | undefined index += 1; } - if (index > userCode.length || userCode[index - 1] !== quote) { + if (!foundClosingQuote) { return undefined; } diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts index 38b0a4ac780a..8e2f572e83e1 100644 --- a/packages/nextjs/test/config/valueInjectionLoader.test.ts +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -221,6 +221,12 @@ describe('findInjectionIndexAfterDirectives', () => { expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); }); + it('does not treat an escaped quote at EOF as a closed directive', () => { + const userCode = '"use client\\"'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + }); + it('treats a block comment without a line break as part of the same statement', () => { const userCode = '"use client" /* comment */ + suffix;'; From c1d051863f56ad21bcacb5107df32203f7d8eb1c Mon Sep 17 00:00:00 2001 From: igz0 <37741728+igz0@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:38:23 +0900 Subject: [PATCH 4/5] refactor(nextjs): simplify directive injection scanner --- .../config/loaders/valueInjectionLoader.ts | 164 ++++++++---------- 1 file changed, 71 insertions(+), 93 deletions(-) diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 0fbec19e7117..50b46adbc344 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -11,70 +11,99 @@ export function findInjectionIndexAfterDirectives(userCode: string): number { let lastDirectiveEndIndex: number | undefined; while (index < userCode.length) { - const statementStartIndex = skipWhitespaceAndComments(userCode, index); + const scanStartIndex = index; - const nextDirectiveIndex = skipDirective(userCode, statementStartIndex); - if (nextDirectiveIndex === undefined) { + // Comments can appear between directive prologue entries, so keep scanning until we reach the next statement. + while (index < userCode.length) { + const char = userCode[index]; + + if (char && /\s/.test(char)) { + index += 1; + continue; + } + + if (userCode.startsWith('//', index)) { + const newlineIndex = userCode.indexOf('\n', index + 2); + index = newlineIndex === -1 ? userCode.length : newlineIndex + 1; + continue; + } + + if (userCode.startsWith('/*', index)) { + const commentEndIndex = userCode.indexOf('*/', index + 2); + if (commentEndIndex === -1) { + return lastDirectiveEndIndex ?? scanStartIndex; + } + + index = commentEndIndex + 2; + continue; + } + + break; + } + + const statementStartIndex = index; + const quote = userCode[statementStartIndex]; + if (quote !== '"' && quote !== "'") { return lastDirectiveEndIndex ?? statementStartIndex; } - const statementEndIndex = skipDirectiveTerminator(userCode, nextDirectiveIndex); - if (statementEndIndex === undefined) { + const stringEndIndex = findStringLiteralEnd(userCode, statementStartIndex); + if (stringEndIndex === undefined) { return lastDirectiveEndIndex ?? statementStartIndex; } - index = statementEndIndex; - lastDirectiveEndIndex = statementEndIndex; - } + let statementEndIndex = stringEndIndex; - return lastDirectiveEndIndex ?? index; -} + // Only a bare string literal followed by a statement terminator counts as a directive. + while (statementEndIndex < userCode.length) { + const char = userCode[statementEndIndex]; -function skipWhitespaceAndComments(userCode: string, startIndex: number): number { - let index = startIndex; + if (char === ';') { + statementEndIndex += 1; + break; + } - while (index < userCode.length) { - const char = userCode[index]; - const nextChar = userCode[index + 1]; + if (char === '\n' || char === '\r' || char === '}') { + break; + } - if (char && /\s/.test(char)) { - index += 1; - continue; - } + if (char && /\s/.test(char)) { + statementEndIndex += 1; + continue; + } - if (char === '/' && nextChar === '/') { - index += 2; - while (index < userCode.length && userCode[index] !== '\n' && userCode[index] !== '\r') { - index += 1; + if (userCode.startsWith('//', statementEndIndex)) { + break; } - continue; - } - if (char === '/' && nextChar === '*') { - const commentEndIndex = userCode.indexOf('*/', index + 2); - if (commentEndIndex === -1) { - return startIndex; + if (userCode.startsWith('/*', statementEndIndex)) { + const commentEndIndex = userCode.indexOf('*/', statementEndIndex + 2); + if (commentEndIndex === -1) { + return lastDirectiveEndIndex ?? statementStartIndex; + } + + const comment = userCode.slice(statementEndIndex + 2, commentEndIndex); + if (comment.includes('\n') || comment.includes('\r')) { + break; + } + + statementEndIndex = commentEndIndex + 2; + continue; } - index = commentEndIndex + 2; - continue; + return lastDirectiveEndIndex ?? statementStartIndex; } - return index; + index = statementEndIndex; + lastDirectiveEndIndex = statementEndIndex; } - return index; + return lastDirectiveEndIndex ?? index; } -function skipDirective(userCode: string, startIndex: number): number | undefined { +function findStringLiteralEnd(userCode: string, startIndex: number): number | undefined { const quote = userCode[startIndex]; - - if (quote !== '"' && quote !== "'") { - return undefined; - } - let index = startIndex + 1; - let foundClosingQuote = false; while (index < userCode.length) { const char = userCode[index]; @@ -85,9 +114,7 @@ function skipDirective(userCode: string, startIndex: number): number | undefined } if (char === quote) { - index += 1; - foundClosingQuote = true; - break; + return index + 1; } if (char === '\n' || char === '\r') { @@ -97,56 +124,7 @@ function skipDirective(userCode: string, startIndex: number): number | undefined index += 1; } - if (!foundClosingQuote) { - return undefined; - } - - return index; -} - -function skipDirectiveTerminator(userCode: string, startIndex: number): number | undefined { - let index = startIndex; - - while (index < userCode.length) { - const char = userCode[index]; - const nextChar = userCode[index + 1]; - - if (char === ';') { - return index + 1; - } - - if (char === '\n' || char === '\r' || char === '}') { - return index; - } - - if (char && /\s/.test(char)) { - index += 1; - continue; - } - - if (char === '/' && nextChar === '/') { - return index; - } - - if (char === '/' && nextChar === '*') { - const commentEndIndex = userCode.indexOf('*/', index + 2); - if (commentEndIndex === -1) { - return undefined; - } - - const comment = userCode.slice(index + 2, commentEndIndex); - if (comment.includes('\n') || comment.includes('\r')) { - return index; - } - - index = commentEndIndex + 2; - continue; - } - - return undefined; - } - - return index; + return undefined; } /** From 215d6bf67d3fa7c25ae3e02cc64c15aa4e5c3758 Mon Sep 17 00:00:00 2001 From: igz0 <37741728+igz0@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:07:01 +0900 Subject: [PATCH 5/5] refactor(nextjs): flatten directive injection scanner --- .../config/loaders/valueInjectionLoader.ts | 144 ++++++++++-------- .../test/config/valueInjectionLoader.test.ts | 12 ++ 2 files changed, 94 insertions(+), 62 deletions(-) diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 50b46adbc344..d388b6bb4dd0 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -11,37 +11,16 @@ export function findInjectionIndexAfterDirectives(userCode: string): number { let lastDirectiveEndIndex: number | undefined; while (index < userCode.length) { - const scanStartIndex = index; - - // Comments can appear between directive prologue entries, so keep scanning until we reach the next statement. - while (index < userCode.length) { - const char = userCode[index]; - - if (char && /\s/.test(char)) { - index += 1; - continue; - } - - if (userCode.startsWith('//', index)) { - const newlineIndex = userCode.indexOf('\n', index + 2); - index = newlineIndex === -1 ? userCode.length : newlineIndex + 1; - continue; - } - - if (userCode.startsWith('/*', index)) { - const commentEndIndex = userCode.indexOf('*/', index + 2); - if (commentEndIndex === -1) { - return lastDirectiveEndIndex ?? scanStartIndex; - } - - index = commentEndIndex + 2; - continue; - } + const statementStartIndex = skipWhitespaceAndComments(userCode, index); + if (statementStartIndex === undefined) { + return lastDirectiveEndIndex ?? 0; + } - break; + index = statementStartIndex; + if (statementStartIndex === userCode.length) { + return lastDirectiveEndIndex ?? statementStartIndex; } - const statementStartIndex = index; const quote = userCode[statementStartIndex]; if (quote !== '"' && quote !== "'") { return lastDirectiveEndIndex ?? statementStartIndex; @@ -52,53 +31,49 @@ export function findInjectionIndexAfterDirectives(userCode: string): number { return lastDirectiveEndIndex ?? statementStartIndex; } - let statementEndIndex = stringEndIndex; - - // Only a bare string literal followed by a statement terminator counts as a directive. - while (statementEndIndex < userCode.length) { - const char = userCode[statementEndIndex]; + const statementEndIndex = findDirectiveTerminator(userCode, stringEndIndex); + if (statementEndIndex === undefined) { + return lastDirectiveEndIndex ?? statementStartIndex; + } - if (char === ';') { - statementEndIndex += 1; - break; - } + index = statementEndIndex; + lastDirectiveEndIndex = statementEndIndex; + } - if (char === '\n' || char === '\r' || char === '}') { - break; - } + return lastDirectiveEndIndex ?? index; +} - if (char && /\s/.test(char)) { - statementEndIndex += 1; - continue; - } +function skipWhitespaceAndComments(userCode: string, startIndex: number): number | undefined { + let index = startIndex; - if (userCode.startsWith('//', statementEndIndex)) { - break; - } + while (index < userCode.length) { + const char = userCode[index]; - if (userCode.startsWith('/*', statementEndIndex)) { - const commentEndIndex = userCode.indexOf('*/', statementEndIndex + 2); - if (commentEndIndex === -1) { - return lastDirectiveEndIndex ?? statementStartIndex; - } + if (char && /\s/.test(char)) { + index += 1; + continue; + } - const comment = userCode.slice(statementEndIndex + 2, commentEndIndex); - if (comment.includes('\n') || comment.includes('\r')) { - break; - } + if (userCode.startsWith('//', index)) { + const newlineIndex = userCode.indexOf('\n', index + 2); + index = newlineIndex === -1 ? userCode.length : newlineIndex + 1; + continue; + } - statementEndIndex = commentEndIndex + 2; - continue; + if (userCode.startsWith('/*', index)) { + const commentEndIndex = userCode.indexOf('*/', index + 2); + if (commentEndIndex === -1) { + return undefined; } - return lastDirectiveEndIndex ?? statementStartIndex; + index = commentEndIndex + 2; + continue; } - index = statementEndIndex; - lastDirectiveEndIndex = statementEndIndex; + break; } - return lastDirectiveEndIndex ?? index; + return index; } function findStringLiteralEnd(userCode: string, startIndex: number): number | undefined { @@ -127,6 +102,51 @@ function findStringLiteralEnd(userCode: string, startIndex: number): number | un return undefined; } +function findDirectiveTerminator(userCode: string, startIndex: number): number | undefined { + let index = startIndex; + + // Only a bare string literal followed by a statement terminator counts as a directive. + while (index < userCode.length) { + const char = userCode[index]; + + if (char === ';') { + return index + 1; + } + + if (char === '\n' || char === '\r' || char === '}') { + return index; + } + + if (char && /\s/.test(char)) { + index += 1; + continue; + } + + if (userCode.startsWith('//', index)) { + return index; + } + + if (userCode.startsWith('/*', index)) { + const commentEndIndex = userCode.indexOf('*/', index + 2); + if (commentEndIndex === -1) { + return undefined; + } + + const comment = userCode.slice(index + 2, commentEndIndex); + if (comment.includes('\n') || comment.includes('\r')) { + return index; + } + + index = commentEndIndex + 2; + continue; + } + + return undefined; + } + + return index; +} + /** * Set values on the global/window object at the start of a module. * diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts index 8e2f572e83e1..6d60819c9cd6 100644 --- a/packages/nextjs/test/config/valueInjectionLoader.test.ts +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -227,6 +227,18 @@ describe('findInjectionIndexAfterDirectives', () => { expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); }); + it('returns 0 for an unterminated leading block comment', () => { + const userCode = '/* unterminated'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + }); + + it('returns the last complete directive when followed by an unterminated block comment', () => { + const userCode = '"use client"; /* unterminated'; + + expect(findInjectionIndexAfterDirectives(userCode)).toBe('"use client";'.length); + }); + it('treats a block comment without a line break as part of the same statement', () => { const userCode = '"use client" /* comment */ + suffix;';