lib: allow CJS source map cache to be reclaimed · nodejs/node@34221a1
@@ -3,7 +3,6 @@
33const {
44 ArrayPrototypePush,
55 JSONParse,
6- ObjectKeys,
76 RegExpPrototypeExec,
87 SafeMap,
98 StringPrototypeCodePointAt,
@@ -25,17 +24,15 @@ const {
2524} = require('internal/errors');
2625const { getLazy } = require('internal/util');
272628-// 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();
27+const getModuleSourceMapCache = getLazy(() => {
28+const { SourceMapCacheMap } = require('internal/source_map/source_map_cache_map');
29+return new SourceMapCacheMap();
3430});
353136-// 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:
32+// The generated source module/script instance is not accessible, so we can use
33+// a Map without memory concerns. Separate generated source entries with the module
34+// source entries to avoid overriding the module source entries with arbitrary
35+// source url magic comments.
3936const generatedSourceMapCache = new SafeMap();
4037const kLeadingProtocol = /^\w+:\/\//;
4138const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/g;
@@ -52,6 +49,10 @@ function getSourceMapsEnabled() {
5249return sourceMapsEnabled;
5350}
545152+/**
53+ * Enables or disables source maps programmatically.
54+ * @param {boolean} val
55+ */
5556function setSourceMapsEnabled(val) {
5657validateBoolean(val, 'val');
5758@@ -72,6 +73,14 @@ function setSourceMapsEnabled(val) {
7273sourceMapsEnabled = val;
7374}
747576+/**
77+ * Extracts the source url from the content if present. For example
78+ * //# sourceURL=file:///path/to/file
79+ *
80+ * Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code
81+ * @param {string} content - source content
82+ * @returns {string | null} source url or null if not present
83+ */
7584function extractSourceURLMagicComment(content) {
7685let match;
7786let matchSourceURL;
@@ -90,6 +99,14 @@ function extractSourceURLMagicComment(content) {
9099return sourceURL;
91100}
92101102+/**
103+ * Extracts the source map url from the content if present. For example
104+ * //# sourceMappingURL=file:///path/to/file
105+ *
106+ * Read more at: https://tc39.es/source-map-spec/#linking-generated-code
107+ * @param {string} content - source content
108+ * @returns {string | null} source map url or null if not present
109+ */
93110function extractSourceMapURLMagicComment(content) {
94111let match;
95112let lastMatch;
@@ -104,7 +121,17 @@ function extractSourceMapURLMagicComment(content) {
104121return lastMatch.groups.sourceMappingURL;
105122}
106123107-function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
124+/**
125+ * Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL.
126+ * @param {string} filename - the actual filename
127+ * @param {string} content - the actual source content
128+ * @param {import('internal/modules/cjs/loader').Module | ModuleWrap} moduleInstance - a module instance that
129+ * associated with the source, once this is reclaimed, the source map entry will be removed from the cache
130+ * @param {boolean} isGeneratedSource - if the source was generated and evaluated with the global eval
131+ * @param {string | undefined} sourceURL - the source url
132+ * @param {string | undefined} sourceMapURL - the source map url
133+ */
134+function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
108135const sourceMapsEnabled = getSourceMapsEnabled();
109136if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
110137const { normalizeReferrerURL } = require('internal/modules/helpers');
@@ -130,45 +157,32 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
130157}
131158132159const data = dataFromUrl(filename, sourceMapURL);
133-const url = data ? null : sourceMapURL;
134-if (cjsModuleInstance) {
135-getCjsSourceMapCache().set(cjsModuleInstance, {
136-__proto__: null,
137- filename,
138-lineLengths: lineLengths(content),
139- data,
140- url,
141- sourceURL,
142-});
143-} else if (isGeneratedSource) {
144-const entry = {
145-__proto__: null,
146-lineLengths: lineLengths(content),
147- data,
148- url,
149- sourceURL,
150-};
160+const entry = {
161+__proto__: null,
162+lineLengths: lineLengths(content),
163+ data,
164+// Save the source map url if it is not a data url.
165+sourceMapURL: data ? null : sourceMapURL,
166+ sourceURL,
167+};
168+169+if (isGeneratedSource) {
151170generatedSourceMapCache.set(filename, entry);
152171if (sourceURL) {
153172generatedSourceMapCache.set(sourceURL, entry);
154173}
155-} else {
156-// If there is no cjsModuleInstance and is not generated source assume we are in a
157-// "modules/esm" context.
158-const entry = {
159-__proto__: null,
160-lineLengths: lineLengths(content),
161- data,
162- url,
163- sourceURL,
164-};
165-esmSourceMapCache.set(filename, entry);
166-if (sourceURL) {
167-esmSourceMapCache.set(sourceURL, entry);
168-}
174+return;
169175}
176+// If it is not a generated source, we assume we are in a "cjs/esm"
177+// context.
178+const keys = sourceURL ? [filename, sourceURL] : [filename];
179+getModuleSourceMapCache().set(keys, entry, moduleInstance);
170180}
171181182+/**
183+ * Caches the source map if it is present in the eval'd source.
184+ * @param {string} content - the eval'd source code
185+ */
172186function maybeCacheGeneratedSourceMap(content) {
173187const sourceMapsEnabled = getSourceMapsEnabled();
174188if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
@@ -186,6 +200,14 @@ function maybeCacheGeneratedSourceMap(content) {
186200}
187201}
188202203+/**
204+ * Resolves source map payload data from the source url and source map url.
205+ * If the source map url is a data url, the data is returned.
206+ * Otherwise the source map url is resolved to a file path and the file is read.
207+ * @param {string} sourceURL - url of the source file
208+ * @param {string} sourceMappingURL - url of the source map
209+ * @returns {object} deserialized source map JSON object
210+ */
189211function dataFromUrl(sourceURL, sourceMappingURL) {
190212try {
191213const url = new URL(sourceMappingURL);
@@ -227,7 +249,11 @@ function lineLengths(content) {
227249return output;
228250}
229251230-252+/**
253+ * Read source map from file.
254+ * @param {string} mapURL - file url of the source map
255+ * @returns {object} deserialized source map JSON object
256+ */
231257function sourceMapFromFile(mapURL) {
232258try {
233259const fs = require('fs');
@@ -281,56 +307,44 @@ function sourcesToAbsolute(baseURL, data) {
281307return data;
282308}
283309284-// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
285-// shutdown. In particular, they also run when Workers are terminated, making
286-// it important that they do not call out to any user-provided code, including
287-// built-in prototypes that might have been tampered with.
310+// WARNING: The `sourceMapCacheToObject` runs during shutdown. In particular,
311+// it also runs when Workers are terminated, making it important that it does
312+// not call out to any user-provided code, including built-in prototypes that
313+// might have been tampered with.
288314289315// Get serialized representation of source-map cache, this is used
290316// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
291317function sourceMapCacheToObject() {
292-const obj = { __proto__: null };
293-294-for (const { 0: k, 1: v } of esmSourceMapCache) {
295-obj[k] = v;
296-}
297-298-appendCJSCache(obj);
299-300-if (ObjectKeys(obj).length === 0) {
318+const moduleSourceMapCache = getModuleSourceMapCache();
319+if (moduleSourceMapCache.size === 0) {
301320return undefined;
302321}
303-return obj;
304-}
305322306-function appendCJSCache(obj) {
307-for (const value of getCjsSourceMapCache()) {
308-obj[value.filename] = {
323+ const obj = { __proto__: null };
324+for (const { 0: k, 1: v } of moduleSourceMapCache) {
325+obj[k] = {
309326__proto__: null,
310-lineLengths: value.lineLengths,
311-data: value.data,
312-url: value.url,
327+lineLengths: v.lineLengths,
328+data: v.data,
329+url: v.sourceMapURL,
313330};
314331}
332+return obj;
315333}
316334335+/**
336+ * Find a source map for a given actual source URL or path.
337+ * @param {string} sourceURL - actual source URL or path
338+ * @returns {import('internal/source_map/source_map').SourceMap | undefined} a source map or undefined if not found
339+ */
317340function findSourceMap(sourceURL) {
318341if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
319342sourceURL = pathToFileURL(sourceURL).href;
320343}
321344if (!SourceMap) {
322345SourceMap = require('internal/source_map/source_map').SourceMap;
323346}
324-let entry = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
325-if (entry === undefined) {
326-for (const value of getCjsSourceMapCache()) {
327-const filename = value.filename;
328-const cachedSourceURL = value.sourceURL;
329-if (sourceURL === filename || sourceURL === cachedSourceURL) {
330-entry = value;
331-}
332-}
333-}
347+const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
334348if (entry === undefined) {
335349return undefined;
336350}