@@ -13,26 +13,42 @@ import fsp from 'fs/promises';
1313// bundler renaming step.
1414const DEBUG_ID_RX = / " ? d d D e b u g I d " ? : " ( [ 0 - 9 a - f A - F - ] { 36 } ) " / ;
1515
16- // The RUM plugin injects its snippet as a BEFORE-position banner (packages/plugins/rum/src/index.ts),
17- // so the ddDebugId literal always lands within the file's first couple hundred bytes, regardless
18- // of the file's total size — no need to read the whole (potentially large) minified bundle to
19- // find it. Measured against ~2.8k real built chunks (mixed bundlers/minifiers), the match always
20- // ended by byte 242; this leaves ~4x headroom for longer service/version strings.
21- export const DEBUG_ID_SEARCH_PREFIX_BYTES = 1024 ;
16+ // Read progressively so the common case only needs the first KiB, while still supporting
17+ // bundlers or transforms that place the injected snippet later in the artifact.
18+ export const DEBUG_ID_SEARCH_CHUNK_BYTES = 1024 ;
19+
20+ // Keep enough content from the previous chunk to match a debug ID literal split across a read
21+ // boundary. The longest supported literal is shorter than this overlap.
22+ const DEBUG_ID_SEARCH_OVERLAP_CHARACTERS = 64 ;
2223
2324const matchDebugId = ( fileContent : string ) : string | undefined => {
2425 return DEBUG_ID_RX . exec ( fileContent ) ?. [ 1 ] ;
2526} ;
2627
27- // Read only the first DEBUG_ID_SEARCH_PREFIX_BYTES bytes of the file, since that's all
28- // we need to find the ddDebugId literal and reading the whole (potentially large)
29- // minified bundle into memory would be wasteful.
30- const readFilePrefix = async ( filePath : string ) : Promise < string > => {
28+ // Search in fixed-size reads and stop as soon as the debug ID is found. Only a small overlap is
29+ // retained between reads, so even the worst case (scanning to EOF) uses bounded memory.
30+ const readDebugId = async ( filePath : string ) : Promise < string | undefined > => {
3131 const fd = await fsp . open ( filePath , 'r' ) ;
3232 try {
33- const buffer = Buffer . alloc ( DEBUG_ID_SEARCH_PREFIX_BYTES ) ;
34- const { bytesRead } = await fd . read ( buffer , 0 , DEBUG_ID_SEARCH_PREFIX_BYTES , 0 ) ;
35- return buffer . toString ( 'utf-8' , 0 , bytesRead ) ;
33+ const buffer = Buffer . alloc ( DEBUG_ID_SEARCH_CHUNK_BYTES ) ;
34+ let overlap = '' ;
35+ let position = 0 ;
36+
37+ while ( true ) {
38+ const { bytesRead } = await fd . read ( buffer , 0 , DEBUG_ID_SEARCH_CHUNK_BYTES , position ) ;
39+ if ( bytesRead === 0 ) {
40+ return undefined ;
41+ }
42+
43+ const searchableContent = overlap + buffer . toString ( 'utf-8' , 0 , bytesRead ) ;
44+ const debugId = matchDebugId ( searchableContent ) ;
45+ if ( debugId ) {
46+ return debugId ;
47+ }
48+
49+ overlap = searchableContent . slice ( - DEBUG_ID_SEARCH_OVERLAP_CHARACTERS ) ;
50+ position += bytesRead ;
51+ }
3652 } finally {
3753 await fd . close ( ) ;
3854 }
@@ -43,6 +59,5 @@ const readFilePrefix = async (filePath: string): Promise<string> => {
4359// rename the file after injection (e.g. webpack/rspack's realContentHash), but the content,
4460// and the debug_id embedded in it, is unaffected.
4561export const extractDebugId = async ( filePath : string ) : Promise < string | undefined > => {
46- const fileContent = await readFilePrefix ( filePath ) . catch ( ( ) => undefined ) ;
47- return fileContent ? matchDebugId ( fileContent ) : undefined ;
62+ return readDebugId ( filePath ) . catch ( ( ) => undefined ) ;
4863} ;
0 commit comments