33const {
44 ArrayPrototypePush,
55 JSONParse,
6- ObjectKeys,
76 RegExpPrototypeExec,
7+ SafeFinalizationRegistry,
88 SafeMap,
9+ SafeWeakRef,
910 StringPrototypeCodePointAt,
1011 StringPrototypeSplit,
1112} = primordials ;
@@ -23,19 +24,26 @@ const {
2324const {
2425 setInternalPrepareStackTrace,
2526} = require ( 'internal/errors' ) ;
26- const { getLazy } = require ( 'internal/util' ) ;
27-
28- // Since the CJS module cache is mutable, which leads to memory leaks when
29- // modules are deleted, we use a WeakMap so that the source map cache will
30- // be purged automatically:
31- const getCjsSourceMapCache = getLazy ( ( ) => {
32- const { IterableWeakMap } = require ( 'internal/util/iterable_weak_map' ) ;
33- return new IterableWeakMap ( ) ;
34- } ) ;
3527
36- // The esm cache is not mutable, so we can use a Map without memory concerns:
37- const esmSourceMapCache = new SafeMap ( ) ;
38- // The generated sources is not mutable, so we can use a Map without memory concerns:
28+ // The cached module instance can be removed from the global module registry
29+ // with approaches like mutating `require.cache`.
30+ // The `moduleSourceMapCache` exposes entries by `filename` and `sourceURL`.
31+ // In the case of mutated module registry, obsolete entries are removed from
32+ // the cache by the `moduleFinalizationRegistry`.
33+ const moduleSourceMapCache = new SafeMap ( ) ;
34+ const moduleFinalizationRegistry = new SafeFinalizationRegistry ( ( key ) => {
35+ const instanceRef = moduleSourceMapCache . get ( key ) . moduleInstanceRef ;
36+ // Delete the entry if the instanceRef has been reclaimed.
37+ // If the instanceRef is not reclaimed, the entry was overridden by a new
38+ // module instance.
39+ if ( instanceRef && instanceRef . deref ( ) === undefined ) {
40+ moduleSourceMapCache . delete ( key ) ;
41+ }
42+ } ) ;
43+ // The generated source module/script instance is not accessible, so we can use
44+ // a Map without memory concerns. Separate generated source entries with the module
45+ // source entries to avoid overriding the module source entries with arbitrary
46+ // source url magic comments.
3947const generatedSourceMapCache = new SafeMap ( ) ;
4048const kLeadingProtocol = / ^ \w + : \/ \/ / ;
4149const kSourceMappingURLMagicComment = / \/ [ * / ] # \s + s o u r c e M a p p i n g U R L = (?< sourceMappingURL > [ ^ \s ] + ) / g;
@@ -52,6 +60,10 @@ function getSourceMapsEnabled() {
5260 return sourceMapsEnabled ;
5361}
5462
63+ /**
64+ * Enables or disables source maps programmatically.
65+ * @param {boolean } val
66+ */
5567function setSourceMapsEnabled ( val ) {
5668 validateBoolean ( val , 'val' ) ;
5769
@@ -72,6 +84,14 @@ function setSourceMapsEnabled(val) {
7284 sourceMapsEnabled = val ;
7385}
7486
87+ /**
88+ * Extracts the source url from the content if present. For example
89+ * //# sourceURL=file:///path/to/file
90+ *
91+ * Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code
92+ * @param {string } content - source content
93+ * @returns {string | null } source url or null if not present
94+ */
7595function extractSourceURLMagicComment ( content ) {
7696 let match ;
7797 let matchSourceURL ;
@@ -90,6 +110,14 @@ function extractSourceURLMagicComment(content) {
90110 return sourceURL ;
91111}
92112
113+ /**
114+ * Extracts the source map url from the content if present. For example
115+ * //# sourceMappingURL=file:///path/to/file
116+ *
117+ * Read more at: https://tc39.es/source-map-spec/#linking-generated-code
118+ * @param {string } content - source content
119+ * @returns {string | null } source map url or null if not present
120+ */
93121function extractSourceMapURLMagicComment ( content ) {
94122 let match ;
95123 let lastMatch ;
@@ -104,7 +132,17 @@ function extractSourceMapURLMagicComment(content) {
104132 return lastMatch . groups . sourceMappingURL ;
105133}
106134
107- function maybeCacheSourceMap ( filename , content , cjsModuleInstance , isGeneratedSource , sourceURL , sourceMapURL ) {
135+ /**
136+ * Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL.
137+ * @param {string } filename - the actual filename
138+ * @param {string } content - the actual source content
139+ * @param {import('internal/modules/cjs/loader').Module | undefined } moduleInstance - a CJS module instance that
140+ * associated with the source, once this is reclaimed, the source map entry will be removed from the cache
141+ * @param {boolean } isGeneratedSource - if the source was generated and evaluated with the global eval
142+ * @param {string | undefined } sourceURL - the source url
143+ * @param {string | undefined } sourceMapURL - the source map url
144+ */
145+ function maybeCacheSourceMap ( filename , content , moduleInstance , isGeneratedSource , sourceURL , sourceMapURL ) {
108146 const sourceMapsEnabled = getSourceMapsEnabled ( ) ;
109147 if ( ! ( process . env . NODE_V8_COVERAGE || sourceMapsEnabled ) ) return ;
110148 const { normalizeReferrerURL } = require ( 'internal/modules/helpers' ) ;
@@ -129,16 +167,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
129167
130168 const data = dataFromUrl ( filename , sourceMapURL ) ;
131169 const url = data ? null : sourceMapURL ;
132- if ( cjsModuleInstance ) {
133- getCjsSourceMapCache ( ) . set ( cjsModuleInstance , {
134- __proto__ : null ,
135- filename,
136- lineLengths : lineLengths ( content ) ,
137- data,
138- url,
139- sourceURL,
140- } ) ;
141- } else if ( isGeneratedSource ) {
170+ if ( isGeneratedSource ) {
142171 const entry = {
143172 __proto__ : null ,
144173 lineLengths : lineLengths ( content ) ,
@@ -150,23 +179,49 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
150179 if ( sourceURL ) {
151180 generatedSourceMapCache . set ( sourceURL , entry ) ;
152181 }
153- } else {
154- // If there is no cjsModuleInstance and is not generated source assume we are in a
155- // "modules/esm" context.
156- const entry = {
157- __proto__ : null ,
158- lineLengths : lineLengths ( content ) ,
159- data,
160- url,
161- sourceURL,
162- } ;
163- esmSourceMapCache . set ( filename , entry ) ;
164- if ( sourceURL ) {
165- esmSourceMapCache . set ( sourceURL , entry ) ;
166- }
182+ return ;
183+ }
184+
185+ // If it is not a generated source, we assume we are in a "cjs/esm"
186+ // context.
187+ const entry = {
188+ __proto__ : null ,
189+ lineLengths : lineLengths ( content ) ,
190+ data,
191+ url,
192+ sourceURL,
193+ moduleInstanceRef : moduleInstance ? new SafeWeakRef ( moduleInstance ) : undefined ,
194+ } ;
195+ setModuleSourceMapCache ( filename , sourceURL , moduleInstance , entry ) ;
196+ }
197+
198+ /**
199+ * Registers the module entry in the `moduleSourceMapCache` with the filename and sourceURL.
200+ * @param {string } filename - the actual filename, in the form of a file URL
201+ * @param {string | undefined } sourceURL - the source url
202+ * @param {import('internal/modules/cjs/loader').Module | undefined } moduleInstance - a CJS module instance
203+ * @param {object } entry - the source map cache entry
204+ */
205+ function setModuleSourceMapCache ( filename , sourceURL , moduleInstance , entry ) {
206+ moduleSourceMapCache . set ( filename , entry ) ;
207+ if ( sourceURL ) {
208+ moduleSourceMapCache . set ( sourceURL , entry ) ;
209+ }
210+ // Skip if the module instance is not present.
211+ if ( moduleInstance == null ) return ;
212+
213+ // Register the module instance with the finalization registry to clear the
214+ // entry once the module instance is been reclaimed.
215+ moduleFinalizationRegistry . register ( moduleInstance , filename ) ;
216+ if ( sourceURL ) {
217+ moduleFinalizationRegistry . register ( moduleInstance , sourceURL ) ;
167218 }
168219}
169220
221+ /**
222+ * Caches the source map if it is present in the eval'd source.
223+ * @param {string } content - the eval'd source code
224+ */
170225function maybeCacheGeneratedSourceMap ( content ) {
171226 const sourceMapsEnabled = getSourceMapsEnabled ( ) ;
172227 if ( ! ( process . env . NODE_V8_COVERAGE || sourceMapsEnabled ) ) return ;
@@ -184,6 +239,14 @@ function maybeCacheGeneratedSourceMap(content) {
184239 }
185240}
186241
242+ /**
243+ * Resolves source map payload data from the source url and source map url.
244+ * If the source map url is a data url, the data is returned.
245+ * Otherwise the source map url is resolved to a file path and the file is read.
246+ * @param {string } sourceURL - url of the source file
247+ * @param {string } sourceMappingURL - url of the source map
248+ * @returns {object } deserialized source map JSON object
249+ */
187250function dataFromUrl ( sourceURL , sourceMappingURL ) {
188251 try {
189252 const url = new URL ( sourceMappingURL ) ;
@@ -225,7 +288,11 @@ function lineLengths(content) {
225288 return output ;
226289}
227290
228-
291+ /**
292+ * Read source map from file.
293+ * @param {string } mapURL - file url of the source map
294+ * @returns {object } deserialized source map JSON object
295+ */
229296function sourceMapFromFile ( mapURL ) {
230297 try {
231298 const fs = require ( 'fs' ) ;
@@ -279,56 +346,43 @@ function sourcesToAbsolute(baseURL, data) {
279346 return data ;
280347}
281348
282- // WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
283- // shutdown. In particular, they also run when Workers are terminated, making
284- // it important that they do not call out to any user-provided code, including
285- // built-in prototypes that might have been tampered with.
349+ // WARNING: The `sourceMapCacheToObject` run during shutdown. In particular,
350+ // they also run when Workers are terminated, making it important that they do
351+ // not call out to any user-provided code, including built-in prototypes that
352+ // might have been tampered with.
286353
287354// Get serialized representation of source-map cache, this is used
288355// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
289356function sourceMapCacheToObject ( ) {
290- const obj = { __proto__ : null } ;
291-
292- for ( const { 0 : k , 1 : v } of esmSourceMapCache ) {
293- obj [ k ] = v ;
294- }
295-
296- appendCJSCache ( obj ) ;
297-
298- if ( ObjectKeys ( obj ) . length === 0 ) {
357+ if ( moduleSourceMapCache . size === 0 ) {
299358 return undefined ;
300359 }
301- return obj ;
302- }
303360
304- function appendCJSCache ( obj ) {
305- for ( const value of getCjsSourceMapCache ( ) ) {
306- obj [ value . filename ] = {
361+ const obj = { __proto__ : null } ;
362+ for ( const { 0 : k , 1 : v } of moduleSourceMapCache ) {
363+ obj [ k ] = {
307364 __proto__ : null ,
308- lineLengths : value . lineLengths ,
309- data : value . data ,
310- url : value . url ,
365+ lineLengths : v . lineLengths ,
366+ data : v . data ,
367+ url : v . url ,
311368 } ;
312369 }
370+ return obj ;
313371}
314372
373+ /**
374+ * Find a source map for a given actual source URL or path.
375+ * @param {string } sourceURL - actual source URL or path
376+ * @returns {import('internal/source_map/source_map').SourceMap | undefined } a source map or undefined if not found
377+ */
315378function findSourceMap ( sourceURL ) {
316379 if ( RegExpPrototypeExec ( kLeadingProtocol , sourceURL ) === null ) {
317380 sourceURL = pathToFileURL ( sourceURL ) . href ;
318381 }
319382 if ( ! SourceMap ) {
320383 SourceMap = require ( 'internal/source_map/source_map' ) . SourceMap ;
321384 }
322- let entry = esmSourceMapCache . get ( sourceURL ) ?? generatedSourceMapCache . get ( sourceURL ) ;
323- if ( entry === undefined ) {
324- for ( const value of getCjsSourceMapCache ( ) ) {
325- const filename = value . filename ;
326- const cachedSourceURL = value . sourceURL ;
327- if ( sourceURL === filename || sourceURL === cachedSourceURL ) {
328- entry = value ;
329- }
330- }
331- }
385+ const entry = moduleSourceMapCache . get ( sourceURL ) ?? generatedSourceMapCache . get ( sourceURL ) ;
332386 if ( entry === undefined ) {
333387 return undefined ;
334388 }
0 commit comments