Skip to content

Commit 344be6b

Browse files
oesnujs1gr1d
andauthored
fix(nextjs): Prevent sourceMappingURL stripping from truncating minified chunks (#24022)
While deploying our app (Next.js 16.2.10 turbopack build, `@sentry/nextjs` 10.68.0, `deleteSourcemapsAfterUpload: true`), we noticed some JS chunks were shipped cut off mid-file and threw `SyntaxError: Invalid or unexpected token` on every page load. We traced it to the sourceMappingURL stripping step, and this PR addresses it with a small regex change. The regex in `stripSourceMappingURLComments` doesn't check that the comment starts at the beginning of a line: ```js /\n?\/\/[#@] sourceMappingURL=[^\n]+$/ ``` So it also matches comment-shaped text inside a string literal — and since a minified chunk is a single line, everything from the match to EOF gets deleted: ```js // minified chunk — comment-shaped text inside a string const a=1;const worker='self.onmessage=()=>{};\n//# sourceMappingURL=worker.js.map\n';use(worker);const b=2; // after stripping — truncated mid-string, no longer parseable const a=1;const worker='self.onmessage=()=>{};\n ``` In our case the text came from rrweb (the recording engine behind Amplitude Session Replay), which inlines its worker code as a string. The regex is unchanged in 10.73.0 and on current develop. We fixed it by requiring the match to start at a line start — we've been running this in production via `pnpm patch`, upstreaming it in case it's useful: ```js /(?:^|\n)\/\/[#@] sourceMappingURL=[^\s'"`]+$/ ``` While digging around we noticed the repo already does this when *reading* the comment (`debug-id-upload.ts` uses `/^\s*\/\/# sourceMappingURL=(.*)$/m`), so this aligns the stripping side with it. Genuine comments sit on their own line and are stripped exactly as before; string-embedded text no longer matches. Excluding whitespace/quotes from the URL matches how browsers read the value (`data:` URIs still work) and blocks the template-literal variant of the same issue. The CSS regex gets the same line-start condition. Added 4 tests — two reproduce the truncation and fail on the old regex, two pin existing behavior. Would love to hear whether this approach makes sense. If you'd prefer a more minimal change, the URL charset restriction can be dropped — the line-start condition alone fixes the truncation. --------- Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com>
1 parent 7de3c72 commit 344be6b

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

‎packages/nextjs/src/config/handleRunAfterProductionCompile.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ async function warnAboutUncoveredSourcemaps(
137137
}
138138
}
139139

140-
const SOURCEMAPPING_URL_COMMENT_REGEX = /\n?\/\/[#@] sourceMappingURL=[^\n]+$/;
141-
const CSS_SOURCEMAPPING_URL_COMMENT_REGEX = /\n?\/\*[#@] sourceMappingURL=[^\n]+\*\/$/;
140+
const SOURCEMAPPING_URL_COMMENT_REGEX = /(?:^|\n)\/\/[#@] sourceMappingURL=[^\s'"`*]+\s*$/;
141+
const CSS_SOURCEMAPPING_URL_COMMENT_REGEX = /\n?\/\*[#@] sourceMappingURL=[^\s*]+\s*\*\/\s*$/;
142142

143143
/**
144144
* Strips sourceMappingURL comments from all JS/MJS/CJS/CSS files in the given directory.

‎packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,4 +740,108 @@ describe('stripSourceMappingURLComments', () => {
740740
expect(content).not.toContain('sourceMappingURL');
741741
}
742742
});
743+
744+
it('does not modify minified files with a sourceMappingURL marker inside a string literal', async () => {
745+
const filePath = path.join(tmpDir, 'chunks', 'minified.js');
746+
const originalContent = `const worker = 'self.onmessage = () => {};\\n//# sourceMappingURL=worker.js.map\\n'; use(worker);`;
747+
await fs.promises.writeFile(filePath, originalContent);
748+
749+
await stripSourceMappingURLComments(tmpDir);
750+
751+
const content = await fs.promises.readFile(filePath, 'utf-8');
752+
expect(content).toBe(originalContent);
753+
});
754+
755+
it('does not modify files ending with a template literal containing a sourceMappingURL marker', async () => {
756+
const filePath = path.join(tmpDir, 'chunks', 'template.js');
757+
const originalContent = 'const s = `line1\n//# sourceMappingURL=worker.js.map`;';
758+
await fs.promises.writeFile(filePath, originalContent);
759+
760+
await stripSourceMappingURLComments(tmpDir);
761+
762+
const content = await fs.promises.readFile(filePath, 'utf-8');
763+
expect(content).toBe(originalContent);
764+
});
765+
766+
it('strips sourceMappingURL comment from files consisting only of the comment', async () => {
767+
const filePath = path.join(tmpDir, 'chunks', 'comment-only.js');
768+
await fs.promises.writeFile(filePath, '//# sourceMappingURL=comment-only.js.map');
769+
770+
await stripSourceMappingURLComments(tmpDir);
771+
772+
const content = await fs.promises.readFile(filePath, 'utf-8');
773+
expect(content).toBe('');
774+
});
775+
776+
it('strips sourceMappingURL comment with a data: URI', async () => {
777+
const filePath = path.join(tmpDir, 'chunks', 'inline.js');
778+
await fs.promises.writeFile(filePath, 'var a = 1;\n//# sourceMappingURL=data:application/json;base64,eyJ2IjozfQ==');
779+
780+
await stripSourceMappingURLComments(tmpDir);
781+
782+
const content = await fs.promises.readFile(filePath, 'utf-8');
783+
expect(content).toBe('var a = 1;');
784+
});
785+
786+
it('strips sourceMappingURL comment followed by trailing whitespace', async () => {
787+
const filePath = path.join(tmpDir, 'chunks', 'trailing-whitespace.js');
788+
await fs.promises.writeFile(filePath, 'var a = 1;\n//# sourceMappingURL=trailing-whitespace.js.map \r\n');
789+
790+
await stripSourceMappingURLComments(tmpDir);
791+
792+
const content = await fs.promises.readFile(filePath, 'utf-8');
793+
expect(content).toBe('var a = 1;');
794+
});
795+
796+
it('strips sourceMappingURL comment from CSS files when it is on the same line', async () => {
797+
const filePath = path.join(tmpDir, 'chunks', 'same-line.css');
798+
await fs.promises.writeFile(filePath, '.foo{color:red}/*# sourceMappingURL=same-line.css.map */\n');
799+
800+
await stripSourceMappingURLComments(tmpDir);
801+
802+
const content = await fs.promises.readFile(filePath, 'utf-8');
803+
expect(content).toBe('.foo{color:red}');
804+
});
805+
806+
it('does not strip past an earlier sourceMappingURL comment in minified CSS files', async () => {
807+
const filePath = path.join(tmpDir, 'chunks', 'concat.css');
808+
await fs.promises.writeFile(filePath, '.a{}/*# sourceMappingURL=a.css.map*/.b{}/*x*/');
809+
810+
await stripSourceMappingURLComments(tmpDir);
811+
812+
const content = await fs.promises.readFile(filePath, 'utf-8');
813+
expect(content).toBe('.a{}/*# sourceMappingURL=a.css.map*/.b{}/*x*/');
814+
});
815+
816+
it('does not modify files ending with a block comment containing a sourceMappingURL marker', async () => {
817+
const filePath = path.join(tmpDir, 'chunks', 'block-comment.js');
818+
const originalContent = 'var a = 1;\n/*\n//# sourceMappingURL=block-comment.js.map*/';
819+
await fs.promises.writeFile(filePath, originalContent);
820+
821+
await stripSourceMappingURLComments(tmpDir);
822+
823+
const content = await fs.promises.readFile(filePath, 'utf-8');
824+
expect(content).toBe(originalContent);
825+
});
826+
827+
it('does not strip a sourceMappingURL comment that is not at the end of the file', async () => {
828+
const filePath = path.join(tmpDir, 'chunks', 'not-at-eof.js');
829+
const originalContent = '//# sourceMappingURL=not-at-eof.js.map\nvar a = 1;';
830+
await fs.promises.writeFile(filePath, originalContent);
831+
832+
await stripSourceMappingURLComments(tmpDir);
833+
834+
const content = await fs.promises.readFile(filePath, 'utf-8');
835+
expect(content).toBe(originalContent);
836+
});
837+
838+
it.fails('strips sourceMappingURL comment from JS files when it is on the same line', async () => {
839+
const filePath = path.join(tmpDir, 'chunks', 'same-line.js');
840+
await fs.promises.writeFile(filePath, 'var a=1;//# sourceMappingURL=same-line.js.map');
841+
842+
await stripSourceMappingURLComments(tmpDir);
843+
844+
const content = await fs.promises.readFile(filePath, 'utf-8');
845+
expect(content).toBe('var a=1;');
846+
});
743847
});

0 commit comments

Comments
 (0)