88
99import type { EncodedSourceMap } from '@ampproject/remapping' ;
1010import { existsSync , readFileSync } from 'node:fs' ;
11+ import { createRequire } from 'node:module' ;
1112import { dirname , resolve } from 'node:path' ;
1213import { fileURLToPath } from 'node:url' ;
1314
1415export const SOURCEMAP_COMMENT_PREFIX = '//# sourceMappingURL=' ;
1516export const SOURCEMAP_COMMENT_BYTES = Buffer . from ( SOURCEMAP_COMMENT_PREFIX ) ;
17+ const SOURCEMAP_COMMENT_VALUE_PREFIX = SOURCEMAP_COMMENT_PREFIX . slice ( 2 ) ;
18+
19+ let parseSync : typeof import ( 'oxc-parser' ) . parseSync | undefined ;
1620
1721/**
1822 * Checks for a single trailing `//# sourceMappingURL=` comment on a raw buffer.
@@ -59,10 +63,11 @@ export function findTrailingSourceMapComment(
5963
6064/**
6165 * Removes `//# sourceMappingURL=` comments safely from the given JavaScript code,
62- * ignoring any occurrences that are inside string literals, template literals, or block comments.
66+ * ignoring any occurrences that are inside string literals, template literals, regex literals,
67+ * or block comments.
6368 *
64- * For raw Uint8Array / Buffer inputs, it optimizes performance by inspecting trailing byte sequences
65- * to slice the buffer directly without full string decoding .
69+ * Single trailing sourcemap comments are sliced directly via a fast path; files with multiple
70+ * or non-trailing occurrences lazily fall back to `oxc-parser` comment extraction .
6671 *
6772 * @param code The JavaScript source code as a string or Uint8Array.
6873 * @returns The code with top-level sourcemap comments removed.
@@ -83,179 +88,123 @@ export function removeSourceMappingURL(code: string | Uint8Array): string | Uint
8388 return trailing . code ;
8489 }
8590
86- // Fallback to full decode and state-machine stripping for multiple comments or non-trailing comments
91+ // Fallback to full decode and AST lexer comment stripping for multiple or non-trailing comments
8792 const dataBuffer = Buffer . isBuffer ( code )
8893 ? code
8994 : Buffer . from ( code . buffer , code . byteOffset , code . byteLength ) ;
9095 const text = dataBuffer . toString ( 'utf-8' ) ;
91- const stripped = removeSourceMappingURLFromString ( text ) ;
96+ const stripped = removeSourceMappingURLFromString ( text , true ) ;
9297
9398 return stripped === text ? code : Buffer . from ( stripped , 'utf-8' ) ;
9499}
95100
96- function removeSourceMappingURLFromString ( code : string ) : string {
97- if ( ! code . includes ( SOURCEMAP_COMMENT_PREFIX ) ) {
101+ function removeSourceMappingURLFromString ( code : string , skipTrailingCheck = false ) : string {
102+ const firstIndex = code . indexOf ( SOURCEMAP_COMMENT_PREFIX ) ;
103+ if ( firstIndex === - 1 ) {
98104 return code ;
99105 }
100106
107+ if ( ! skipTrailingCheck ) {
108+ const lastIndex = code . lastIndexOf ( SOURCEMAP_COMMENT_PREFIX ) ;
109+ let prevIdx = lastIndex - 1 ;
110+ while ( prevIdx >= 0 && ( code [ prevIdx ] === ' ' || code [ prevIdx ] === '\t' ) ) {
111+ prevIdx -- ;
112+ }
113+ const isLineStart = prevIdx < 0 || code [ prevIdx ] === '\n' || code [ prevIdx ] === '\r' ;
114+ if (
115+ firstIndex === lastIndex &&
116+ isLineStart &&
117+ isTrailingSourceMapComment ( code . slice ( lastIndex + SOURCEMAP_COMMENT_PREFIX . length ) )
118+ ) {
119+ return code . slice ( 0 , prevIdx < 0 ? 0 : prevIdx + 1 ) ;
120+ }
121+ }
122+
123+ parseSync ??= ( createRequire ( __filename ) ( 'oxc-parser' ) as typeof import ( 'oxc-parser' ) ) . parseSync ;
124+
125+ const { comments } = parseSync ( 'source.js' , code , {
126+ lang : 'js' ,
127+ sourceType : 'unambiguous' ,
128+ } ) ;
129+
101130 const result : string [ ] = [ ] ;
102131 let lastCopiedIndex = 0 ;
103- let i = 0 ;
104- const len = code . length ;
105-
106- // Stack to track template literal state and curly brace depth for nested interpolations.
107- const stack : { type : 'template' ; braceDepth : number } [ ] = [ ] ;
108- let currentState :
109- 'normal' | 'string_double' | 'string_single' | 'template' | 'comment_block' | 'comment_line' =
110- 'normal' ;
111-
112- while ( i < len ) {
113- const char = code [ i ] ;
114- const nextChar = code [ i + 1 ] ;
115-
116- if ( currentState === 'normal' ) {
117- if ( char === '/' && nextChar === '*' ) {
118- currentState = 'comment_block' ;
119- i += 2 ;
120- continue ;
121- }
122- if ( char === '/' && nextChar === '/' ) {
123- // Detect if the comment is escaped (e.g. inside a regex literal like `/\/\/#/`).
124- let isEscaped = false ;
125- let prevIdx = i - 1 ;
126- while ( prevIdx >= 0 && / \s / . test ( code [ prevIdx ] ) ) {
127- prevIdx -- ;
128- }
129- if ( prevIdx >= 0 && code [ prevIdx ] === '\\' ) {
130- let bsCount = 0 ;
131- while ( prevIdx >= 0 && code [ prevIdx ] === '\\' ) {
132- bsCount ++ ;
133- prevIdx -- ;
134- }
135- if ( bsCount % 2 === 1 ) {
136- isEscaped = true ;
137- }
138- }
139-
140- if ( ! isEscaped && code . startsWith ( SOURCEMAP_COMMENT_PREFIX , i ) ) {
141- if ( i > lastCopiedIndex ) {
142- result . push ( code . slice ( lastCopiedIndex , i ) ) ;
143- }
144- // Skip the rest of the comment line up to the newline
145- i += SOURCEMAP_COMMENT_PREFIX . length ;
146- while ( i < len && code [ i ] !== '\n' && code [ i ] !== '\r' ) {
147- i ++ ;
148- }
149- lastCopiedIndex = i ;
150- continue ;
151- } else {
152- currentState = 'comment_line' ;
153- i += 2 ;
154- continue ;
155- }
156- }
157- if ( char === '"' ) {
158- currentState = 'string_double' ;
159- i ++ ;
160- continue ;
161- }
162- if ( char === "'" ) {
163- currentState = 'string_single' ;
164- i ++ ;
165- continue ;
166- }
167- if ( char === '`' ) {
168- currentState = 'template' ;
169- stack . push ( { type : 'template' , braceDepth : 0 } ) ;
170- i ++ ;
171- continue ;
172- }
173- if ( char === '{' ) {
174- const top = stack [ stack . length - 1 ] ;
175- if ( top ) {
176- top . braceDepth ++ ;
177- }
178- i ++ ;
179- continue ;
180- }
181- if ( char === '}' ) {
182- const top = stack [ stack . length - 1 ] ;
183- if ( top ) {
184- top . braceDepth -- ;
185- if ( top . braceDepth < 0 ) {
186- // Exiting a template literal interpolation ${ ... }
187- stack . pop ( ) ;
188- currentState = 'template' ;
189- i ++ ;
190- continue ;
191- }
192- }
193- i ++ ;
194- continue ;
195- }
196132
197- i ++ ;
198- } else if ( currentState === 'string_double' ) {
199- if ( char === '\\' ) {
200- i += 2 ;
201- continue ;
202- }
203- if ( char === '"' ) {
204- currentState = 'normal' ;
205- }
206- i ++ ;
207- } else if ( currentState === 'string_single' ) {
208- if ( char === '\\' ) {
209- i += 2 ;
210- continue ;
211- }
212- if ( char === "'" ) {
213- currentState = 'normal' ;
214- }
215- i ++ ;
216- } else if ( currentState === 'template' ) {
217- if ( char === '\\' ) {
218- i += 2 ;
219- continue ;
220- }
221- if ( char === '$' && nextChar === '{' ) {
222- // Entering template literal interpolation context
223- currentState = 'normal' ;
224- stack . push ( { type : 'template' , braceDepth : 0 } ) ;
225- i += 2 ;
226- continue ;
227- }
228- if ( char === '`' ) {
229- stack . pop ( ) ;
230- currentState = 'normal' ;
231- }
232- i ++ ;
233- } else if ( currentState === 'comment_block' ) {
234- if ( char === '*' && nextChar === '/' ) {
235- currentState = 'normal' ;
236- i += 2 ;
237- continue ;
238- }
239- i ++ ;
240- } else if ( currentState === 'comment_line' ) {
241- if ( char === '\n' || char === '\r' ) {
242- currentState = 'normal' ;
133+ for ( const comment of comments ) {
134+ if ( comment . type !== 'Line' || ! comment . value . startsWith ( SOURCEMAP_COMMENT_VALUE_PREFIX ) ) {
135+ continue ;
136+ }
137+
138+ let start = comment . start ;
139+ let end = comment . end ;
140+
141+ // Match the trailing fast path: when the comment is the only content on its line, remove the
142+ // entire line (or everything up to EOF if only whitespace follows). Comments that follow code
143+ // on the same line keep their line terminator to avoid altering automatic semicolon insertion.
144+ let lineStart = start ;
145+ while ( lineStart > lastCopiedIndex && isHorizontalWhitespace ( code . charCodeAt ( lineStart - 1 ) ) ) {
146+ lineStart -- ;
147+ }
148+ if ( lineStart === 0 || isLineTerminator ( code . charCodeAt ( lineStart - 1 ) ) ) {
149+ start = lineStart ;
150+ if ( isWhitespaceToEnd ( code , end ) ) {
151+ end = code . length ;
152+ } else if ( code . charCodeAt ( end ) === 13 && code . charCodeAt ( end + 1 ) === 10 ) {
153+ end += 2 ;
154+ } else if ( isLineTerminator ( code . charCodeAt ( end ) ) ) {
155+ end ++ ;
243156 }
244- i ++ ;
245157 }
158+
159+ if ( start > lastCopiedIndex ) {
160+ result . push ( code . slice ( lastCopiedIndex , start ) ) ;
161+ }
162+ lastCopiedIndex = end ;
246163 }
247164
248165 if ( lastCopiedIndex === 0 ) {
249166 return code ;
250167 }
251168
252- if ( lastCopiedIndex < len ) {
169+ if ( lastCopiedIndex < code . length ) {
253170 result . push ( code . slice ( lastCopiedIndex ) ) ;
254171 }
255172
256173 return result . join ( '' ) ;
257174}
258175
176+ function isHorizontalWhitespace ( code : number ) : boolean {
177+ return code === 32 || code === 9 ; // space, tab
178+ }
179+
180+ function isLineTerminator ( code : number ) : boolean {
181+ return code === 10 || code === 13 ; // \n, \r
182+ }
183+
184+ function isWhitespaceToEnd ( code : string , start : number ) : boolean {
185+ for ( let i = start ; i < code . length ; i ++ ) {
186+ const char = code . charCodeAt ( i ) ;
187+ if ( ! isHorizontalWhitespace ( char ) && ! isLineTerminator ( char ) ) {
188+ return false ;
189+ }
190+ }
191+
192+ return true ;
193+ }
194+
195+ function isBase64Char ( code : number ) : boolean {
196+ return (
197+ ( code >= 65 && code <= 90 ) || // A-Z
198+ ( code >= 97 && code <= 122 ) || // a-z
199+ ( code >= 48 && code <= 57 ) || // 0-9
200+ code === 43 || // +
201+ code === 47 || // /
202+ code === 61 || // =
203+ code === 45 || // -
204+ code === 95 // _
205+ ) ;
206+ }
207+
259208/**
260209 * Extracts the base64 payload from an inline sourcemap data URI line and verifies
261210 * that only trailing whitespace follows the payload.
@@ -274,13 +223,22 @@ function extractTrailingBase64Payload(urlLine: string): string | undefined {
274223
275224 const payloadStart = base64StartIndex + 7 ;
276225 let payloadEnd = urlLine . length ;
277- // Find the first trailing whitespace character that marks the end of the base64 payload.
226+ // Find the first trailing whitespace character that marks the end of the base64 payload,
227+ // while ensuring all payload characters are valid base64 (preventing false positives when
228+ // closing quotes, backticks, or `*/` immediately follow the payload).
278229 for ( let i = payloadStart ; i < urlLine . length ; i ++ ) {
279230 const char = urlLine [ i ] ;
280231 if ( char === ' ' || char === '\r' || char === '\n' || char === '\t' ) {
281232 payloadEnd = i ;
282233 break ;
283234 }
235+ if ( ! isBase64Char ( urlLine . charCodeAt ( i ) ) ) {
236+ return undefined ;
237+ }
238+ }
239+
240+ if ( payloadEnd === payloadStart ) {
241+ return undefined ;
284242 }
285243
286244 // Verify that everything after the base64 payload is trailing whitespace
@@ -307,7 +265,7 @@ function extractTrailingUrl(urlLine: string): string | undefined {
307265 }
308266
309267 const urlMatch = / ^ ( [ ^ \r \n \s ' " ` ] + ) / . exec ( urlLine ) ;
310- if ( ! urlMatch ) {
268+ if ( ! urlMatch || urlMatch [ 1 ] . includes ( '*/' ) ) {
311269 return undefined ;
312270 }
313271
0 commit comments