-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(nextjs): preserve directive prologues in turbopack loaders #20103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 4 commits
a648434
dd6204f
81516b3
c1d0518
215d6bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,131 @@ | ||
| // 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<string, unknown>; | ||
| }; | ||
|
|
||
| // 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 (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; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
|
|
||
| const statementStartIndex = index; | ||
| const quote = userCode[statementStartIndex]; | ||
| if (quote !== '"' && quote !== "'") { | ||
| return lastDirectiveEndIndex ?? statementStartIndex; | ||
| } | ||
|
|
||
| const stringEndIndex = findStringLiteralEnd(userCode, statementStartIndex); | ||
| if (stringEndIndex === undefined) { | ||
| 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]; | ||
|
|
||
| if (char === ';') { | ||
| statementEndIndex += 1; | ||
| break; | ||
| } | ||
|
|
||
| if (char === '\n' || char === '\r' || char === '}') { | ||
| break; | ||
| } | ||
|
|
||
| if (char && /\s/.test(char)) { | ||
| statementEndIndex += 1; | ||
| continue; | ||
| } | ||
|
|
||
| if (userCode.startsWith('//', statementEndIndex)) { | ||
| break; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| return lastDirectiveEndIndex ?? statementStartIndex; | ||
| } | ||
|
|
||
| index = statementEndIndex; | ||
| lastDirectiveEndIndex = statementEndIndex; | ||
| } | ||
|
|
||
| return lastDirectiveEndIndex ?? index; | ||
| } | ||
|
|
||
| function findStringLiteralEnd(userCode: string, startIndex: number): number | undefined { | ||
| const quote = userCode[startIndex]; | ||
| let index = startIndex + 1; | ||
|
|
||
| while (index < userCode.length) { | ||
| const char = userCode[index]; | ||
|
|
||
| if (char === '\\') { | ||
| index += 2; | ||
| continue; | ||
| } | ||
|
|
||
| if (char === quote) { | ||
| return index + 1; | ||
| } | ||
|
|
||
| if (char === '\n' || char === '\r') { | ||
| return undefined; | ||
| } | ||
|
|
||
| index += 1; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Set values on the global/window object at the start of a module. | ||
|
|
@@ -36,7 +149,6 @@ export default function valueInjectionLoader(this: LoaderThis<ValueInjectionLoad | |
| .map(([key, value]) => `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)}`; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable is purely to use in fallback returns inside the inner loop (return
lastDirectiveEndIndex ?? scanStartIndex). But at that point,scanStartIndexis always equivalent tolastDirectiveEndIndex ?? 0:scanStartIndexis0andlastDirectiveEndIndexisundefined, so?? scanStartIndex=?? 0lastDirectiveEndIndexis already set, soscanStartIndexis never usedI think you can delete
scanStartIndexentirely and writereturn lastDirectiveEndIndex ?? 0.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I removed
scanStartIndexand simplified those fallback paths tolastDirectiveEndIndex ?? 0/lastDirectiveEndIndex ?? statementStartIndex, depending on whether we've already reached a concrete statement boundary.Applied in 215d6bf.