diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 69041d154c6fa2..78274269d1a80c 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1335,7 +1335,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) { // Cache the source map for the module if present. const { sourceMapURL } = script; if (sourceMapURL) { - maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL); + maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, sourceMapURL); } return { @@ -1358,7 +1358,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) { // Cache the source map for the module if present. if (result.sourceMapURL) { - maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); + maybeCacheSourceMap(filename, content, cjsModuleInstance, false, undefined, result.sourceMapURL); } return result; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 89754b6453dbcc..bfd6a30d256c98 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -178,13 +178,12 @@ translators.set('module', function moduleStrategy(url, source, isMain) { function loadCJSModule(module, source, url, filename, isMain) { const compileResult = compileFunctionForCJSLoader(source, filename, isMain, false); + const { function: compiledWrapper, sourceMapURL } = compileResult; // Cache the source map for the cjs module if present. - if (compileResult.sourceMapURL) { - maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL); + if (sourceMapURL) { + maybeCacheSourceMap(url, source, module, false, undefined, sourceMapURL); } - const compiledWrapper = compileResult.function; - const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const __dirname = dirname(filename); // eslint-disable-next-line func-name-matching,func-style diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 70deb335f0e8c1..d393d4336a0c1e 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -341,7 +341,7 @@ function compileSourceTextModule(url, source, cascadedLoader) { } // Cache the source map for the module if present. if (wrap.sourceMapURL) { - maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL); + maybeCacheSourceMap(url, source, wrap, false, undefined, wrap.sourceMapURL); } return wrap; } diff --git a/lib/internal/source_map/prepare_stack_trace.js b/lib/internal/source_map/prepare_stack_trace.js index 776bb470bd13e4..1b12bb89f084ca 100644 --- a/lib/internal/source_map/prepare_stack_trace.js +++ b/lib/internal/source_map/prepare_stack_trace.js @@ -114,9 +114,15 @@ function getOriginalSymbolName(sourceMap, trace, curIndex) { } } -// Places a snippet of code from where the exception was originally thrown -// above the stack trace. This logic is modeled after GetErrorSource in -// node_errors.cc. +/** + * Return a snippet of code from where the exception was originally thrown + * above the stack trace. This called from GetErrorSource in node_errors.cc. + * @param {import('internal/source_map/source_map').SourceMap} sourceMap - the source map to be used + * @param {string} originalSourcePath - path or url of the original source + * @param {number} originalLine - line number in the original source + * @param {number} originalColumn - column number in the original source + * @returns {string | undefined} - the exact line in the source content or undefined if file not found + */ function getErrorSource( sourceMap, originalSourcePath, @@ -154,6 +160,12 @@ function getErrorSource( return exceptionLine; } +/** + * Retrieve the original source code from the source map's `sources` list or disk. + * @param {import('internal/source_map/source_map').SourceMap.payload} payload + * @param {string} originalSourcePath - path or url of the original source + * @returns {string | undefined} - the source content or undefined if file not found + */ function getOriginalSource(payload, originalSourcePath) { let source; // payload.sources has been normalized to be an array of absolute urls. @@ -177,6 +189,13 @@ function getOriginalSource(payload, originalSourcePath) { return source; } +/** + * Retrieve exact line in the original source code from the source map's `sources` list or disk. + * @param {string} fileName - actual file name + * @param {number} lineNumber - actual line number + * @param {number} columnNumber - actual column number + * @returns {string | undefined} - the source content or undefined if file not found + */ function getSourceMapErrorSource(fileName, lineNumber, columnNumber) { const sm = findSourceMap(fileName); if (sm === undefined) { diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js index 53c3374fc09176..f21b3719ad806a 100644 --- a/lib/internal/source_map/source_map_cache.js +++ b/lib/internal/source_map/source_map_cache.js @@ -3,7 +3,6 @@ const { ArrayPrototypePush, JSONParse, - ObjectKeys, RegExpPrototypeExec, SafeMap, StringPrototypeCodePointAt, @@ -25,17 +24,15 @@ const { } = require('internal/errors'); const { getLazy } = require('internal/util'); -// Since the CJS module cache is mutable, which leads to memory leaks when -// modules are deleted, we use a WeakMap so that the source map cache will -// be purged automatically: -const getCjsSourceMapCache = getLazy(() => { - const { IterableWeakMap } = require('internal/util/iterable_weak_map'); - return new IterableWeakMap(); +const getModuleSourceMapCache = getLazy(() => { + const { SourceMapCacheMap } = require('internal/source_map/source_map_cache_map'); + return new SourceMapCacheMap(); }); -// The esm cache is not mutable, so we can use a Map without memory concerns: -const esmSourceMapCache = new SafeMap(); -// The generated sources is not mutable, so we can use a Map without memory concerns: +// The generated source module/script instance is not accessible, so we can use +// a Map without memory concerns. Separate generated source entries with the module +// source entries to avoid overriding the module source entries with arbitrary +// source url magic comments. const generatedSourceMapCache = new SafeMap(); const kLeadingProtocol = /^\w+:\/\//; const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?[^\s]+)/g; @@ -52,6 +49,10 @@ function getSourceMapsEnabled() { return sourceMapsEnabled; } +/** + * Enables or disables source maps programmatically. + * @param {boolean} val + */ function setSourceMapsEnabled(val) { validateBoolean(val, 'val'); @@ -72,6 +73,14 @@ function setSourceMapsEnabled(val) { sourceMapsEnabled = val; } +/** + * Extracts the source url from the content if present. For example + * //# sourceURL=file:///path/to/file + * + * Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code + * @param {string} content - source content + * @returns {string | null} source url or null if not present + */ function extractSourceURLMagicComment(content) { let match; let matchSourceURL; @@ -90,6 +99,14 @@ function extractSourceURLMagicComment(content) { return sourceURL; } +/** + * Extracts the source map url from the content if present. For example + * //# sourceMappingURL=file:///path/to/file + * + * Read more at: https://tc39.es/source-map-spec/#linking-generated-code + * @param {string} content - source content + * @returns {string | null} source map url or null if not present + */ function extractSourceMapURLMagicComment(content) { let match; let lastMatch; @@ -104,7 +121,17 @@ function extractSourceMapURLMagicComment(content) { return lastMatch.groups.sourceMappingURL; } -function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) { +/** + * Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL. + * @param {string} filename - the actual filename + * @param {string} content - the actual source content + * @param {import('internal/modules/cjs/loader').Module | ModuleWrap} moduleInstance - a module instance that + * associated with the source, once this is reclaimed, the source map entry will be removed from the cache + * @param {boolean} isGeneratedSource - if the source was generated and evaluated with the global eval + * @param {string | undefined} sourceURL - the source url + * @param {string | undefined} sourceMapURL - the source map url + */ +function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSource, sourceURL, sourceMapURL) { const sourceMapsEnabled = getSourceMapsEnabled(); if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return; const { normalizeReferrerURL } = require('internal/modules/helpers'); @@ -130,45 +157,32 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo } const data = dataFromUrl(filename, sourceMapURL); - const url = data ? null : sourceMapURL; - if (cjsModuleInstance) { - getCjsSourceMapCache().set(cjsModuleInstance, { - __proto__: null, - filename, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }); - } else if (isGeneratedSource) { - const entry = { - __proto__: null, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }; + const entry = { + __proto__: null, + lineLengths: lineLengths(content), + data, + // Save the source map url if it is not a data url. + sourceMapURL: data ? null : sourceMapURL, + sourceURL, + }; + + if (isGeneratedSource) { generatedSourceMapCache.set(filename, entry); if (sourceURL) { generatedSourceMapCache.set(sourceURL, entry); } - } else { - // If there is no cjsModuleInstance and is not generated source assume we are in a - // "modules/esm" context. - const entry = { - __proto__: null, - lineLengths: lineLengths(content), - data, - url, - sourceURL, - }; - esmSourceMapCache.set(filename, entry); - if (sourceURL) { - esmSourceMapCache.set(sourceURL, entry); - } + return; } + // If it is not a generated source, we assume we are in a "cjs/esm" + // context. + const keys = sourceURL ? [filename, sourceURL] : [filename]; + getModuleSourceMapCache().set(keys, entry, moduleInstance); } +/** + * Caches the source map if it is present in the eval'd source. + * @param {string} content - the eval'd source code + */ function maybeCacheGeneratedSourceMap(content) { const sourceMapsEnabled = getSourceMapsEnabled(); if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return; @@ -186,6 +200,14 @@ function maybeCacheGeneratedSourceMap(content) { } } +/** + * Resolves source map payload data from the source url and source map url. + * If the source map url is a data url, the data is returned. + * Otherwise the source map url is resolved to a file path and the file is read. + * @param {string} sourceURL - url of the source file + * @param {string} sourceMappingURL - url of the source map + * @returns {object} deserialized source map JSON object + */ function dataFromUrl(sourceURL, sourceMappingURL) { try { const url = new URL(sourceMappingURL); @@ -227,7 +249,11 @@ function lineLengths(content) { return output; } - +/** + * Read source map from file. + * @param {string} mapURL - file url of the source map + * @returns {object} deserialized source map JSON object + */ function sourceMapFromFile(mapURL) { try { const fs = require('fs'); @@ -281,39 +307,36 @@ function sourcesToAbsolute(baseURL, data) { return data; } -// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during -// shutdown. In particular, they also run when Workers are terminated, making -// it important that they do not call out to any user-provided code, including -// built-in prototypes that might have been tampered with. +// WARNING: The `sourceMapCacheToObject` runs during shutdown. In particular, +// it also runs when Workers are terminated, making it important that it does +// not call out to any user-provided code, including built-in prototypes that +// might have been tampered with. // Get serialized representation of source-map cache, this is used // to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled. function sourceMapCacheToObject() { - const obj = { __proto__: null }; - - for (const { 0: k, 1: v } of esmSourceMapCache) { - obj[k] = v; - } - - appendCJSCache(obj); - - if (ObjectKeys(obj).length === 0) { + const moduleSourceMapCache = getModuleSourceMapCache(); + if (moduleSourceMapCache.size === 0) { return undefined; } - return obj; -} -function appendCJSCache(obj) { - for (const value of getCjsSourceMapCache()) { - obj[value.filename] = { + const obj = { __proto__: null }; + for (const { 0: k, 1: v } of moduleSourceMapCache) { + obj[k] = { __proto__: null, - lineLengths: value.lineLengths, - data: value.data, - url: value.url, + lineLengths: v.lineLengths, + data: v.data, + url: v.sourceMapURL, }; } + return obj; } +/** + * Find a source map for a given actual source URL or path. + * @param {string} sourceURL - actual source URL or path + * @returns {import('internal/source_map/source_map').SourceMap | undefined} a source map or undefined if not found + */ function findSourceMap(sourceURL) { if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) { sourceURL = pathToFileURL(sourceURL).href; @@ -321,16 +344,7 @@ function findSourceMap(sourceURL) { if (!SourceMap) { SourceMap = require('internal/source_map/source_map').SourceMap; } - let entry = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL); - if (entry === undefined) { - for (const value of getCjsSourceMapCache()) { - const filename = value.filename; - const cachedSourceURL = value.sourceURL; - if (sourceURL === filename || sourceURL === cachedSourceURL) { - entry = value; - } - } - } + const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL); if (entry === undefined) { return undefined; } diff --git a/lib/internal/source_map/source_map_cache_map.js b/lib/internal/source_map/source_map_cache_map.js new file mode 100644 index 00000000000000..e8adfe83708316 --- /dev/null +++ b/lib/internal/source_map/source_map_cache_map.js @@ -0,0 +1,115 @@ +'use strict'; + +const { + ArrayPrototypeForEach, + ObjectFreeze, + SafeFinalizationRegistry, + SafeMap, + SafeWeakRef, + SymbolIterator, +} = primordials; +const { + privateSymbols: { + source_map_data_private_symbol, + }, +} = internalBinding('util'); + +/** + * Specialized map of WeakRefs to module instances that caches source map + * entries by `filename` and `sourceURL`. Cached entries can be iterated with + * `for..of` syntax. + * + * The cache map maintains the cache entries by: + * - `weakModuleMap`(Map): a strong sourceURL -> WeakRef(Module), + * - WeakRef(Module[source_map_data_private_symbol]): source map data. + * + * Obsolete `weakModuleMap` entries are removed by the `finalizationRegistry` + * callback. This pattern decouples the strong url reference to the source map + * data and allow the cache to be reclaimed eagerly, without depending on an + * undeterministic callback of a finalization registry. + */ +class SourceMapCacheMap { + /** + * @type {Map>} + * The cached module instance can be removed from the global module registry + * with approaches like mutating `require.cache`. + * The `weakModuleMap` exposes entries by `filename` and `sourceURL`. + * In the case of mutated module registry, obsolete entries are removed from + * the cache by the `finalizationRegistry`. + */ + #weakModuleMap = new SafeMap(); + + #cleanup = ({ keys }) => { + // Delete the entry if the weak target has been reclaimed. + // If the weak target is not reclaimed, the entry was overridden by a new + // weak target. + ArrayPrototypeForEach(keys, (key) => { + const ref = this.#weakModuleMap.get(key); + if (ref && ref.deref() === undefined) { + this.#weakModuleMap.delete(key); + } + }); + }; + #finalizationRegistry = new SafeFinalizationRegistry(this.#cleanup); + + /** + * Sets the value for the given key, associated with the given module + * instance. + * @param {string[]} keys array of urls to index the value entry. + * @param {*} sourceMapData the value entry. + * @param {object} moduleInstance an object that can be weakly referenced and + * invalidate the [key, value] entry after this object is reclaimed. + */ + set(keys, sourceMapData, moduleInstance) { + const weakRef = new SafeWeakRef(moduleInstance); + ArrayPrototypeForEach(keys, (key) => this.#weakModuleMap.set(key, weakRef)); + moduleInstance[source_map_data_private_symbol] = sourceMapData; + this.#finalizationRegistry.register(moduleInstance, { keys }); + } + + /** + * Get an entry by the given key. + * @param {string} key a file url or source url + */ + get(key) { + const weakRef = this.#weakModuleMap.get(key); + const moduleInstance = weakRef?.deref(); + if (moduleInstance === undefined) { + return; + } + return moduleInstance[source_map_data_private_symbol]; + } + + /** + * Estimate the size of the cache. The actual size may be smaller because + * some entries may be reclaimed with the module instance. + */ + get size() { + return this.#weakModuleMap.size; + } + + [SymbolIterator]() { + const iterator = this.#weakModuleMap.entries(); + + const next = () => { + const result = iterator.next(); + if (result.done) return result; + const { 0: key, 1: weakRef } = result.value; + const moduleInstance = weakRef.deref(); + if (moduleInstance == null) return next(); + const value = moduleInstance[source_map_data_private_symbol]; + return { done: false, value: [key, value] }; + }; + + return { + [SymbolIterator]() { return this; }, + next, + }; + } +} + +ObjectFreeze(SourceMapCacheMap.prototype); + +module.exports = { + SourceMapCacheMap, +}; diff --git a/lib/internal/util/iterable_weak_map.js b/lib/internal/util/iterable_weak_map.js deleted file mode 100644 index 16694ffdb11de8..00000000000000 --- a/lib/internal/util/iterable_weak_map.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -const { - ObjectFreeze, - SafeFinalizationRegistry, - SafeSet, - SafeWeakMap, - SafeWeakRef, - SymbolIterator, -} = primordials; - -// This class is modified from the example code in the WeakRefs specification: -// https://github.com/tc39/proposal-weakrefs -// Licensed under ECMA's MIT-style license, see: -// https://github.com/tc39/ecma262/blob/HEAD/LICENSE.md -class IterableWeakMap { - #weakMap = new SafeWeakMap(); - #refSet = new SafeSet(); - #finalizationGroup = new SafeFinalizationRegistry(cleanup); - - set(key, value) { - const entry = this.#weakMap.get(key); - if (entry) { - // If there's already an entry for the object represented by "key", - // the value can be updated without creating a new WeakRef: - this.#weakMap.set(key, { value, ref: entry.ref }); - } else { - const ref = new SafeWeakRef(key); - this.#weakMap.set(key, { value, ref }); - this.#refSet.add(ref); - this.#finalizationGroup.register(key, { - set: this.#refSet, - ref, - }, ref); - } - } - - get(key) { - return this.#weakMap.get(key)?.value; - } - - has(key) { - return this.#weakMap.has(key); - } - - delete(key) { - const entry = this.#weakMap.get(key); - if (!entry) { - return false; - } - this.#weakMap.delete(key); - this.#refSet.delete(entry.ref); - this.#finalizationGroup.unregister(entry.ref); - return true; - } - - [SymbolIterator]() { - const iterator = this.#refSet[SymbolIterator](); - - const next = () => { - const result = iterator.next(); - if (result.done) return result; - const key = result.value.deref(); - if (key == null) return next(); - const { value } = this.#weakMap.get(key); - return { done: false, value }; - }; - - return { - [SymbolIterator]() { return this; }, - next, - }; - } -} - -function cleanup({ set, ref }) { - set.delete(ref); -} - -ObjectFreeze(IterableWeakMap.prototype); - -module.exports = { - IterableWeakMap, -}; diff --git a/src/env_properties.h b/src/env_properties.h index 8bf62103a394d9..b8e0f8c3bb12cd 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -35,7 +35,8 @@ V(napi_wrapper, "node:napi:wrapper") \ V(untransferable_object_private_symbol, "node:untransferableObject") \ V(exit_info_private_symbol, "node:exit_info_private_symbol") \ - V(promise_trace_id, "node:promise_trace_id") + V(promise_trace_id, "node:promise_trace_id") \ + V(source_map_data_private_symbol, "node:source_map_data_private_symbol") // Symbols are per-isolate primitives but Environment proxies them // for the sake of convenience. diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 35139e874bca18..1e2ceb931bbfac 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -312,6 +312,14 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { return; } + // Initialize an empty slot for source map cache before the object is frozen. + if (that->SetPrivate(context, + realm->isolate_data()->source_map_data_private_symbol(), + Undefined(isolate)) + .IsNothing()) { + return; + } + // Use the extras object as an object whose GetCreationContext() will be the // original `context`, since the `Context` itself strictly speaking cannot // be stored in an internal field. diff --git a/test/fixtures/source-map/no-throw.js b/test/fixtures/source-map/no-throw.js new file mode 100644 index 00000000000000..4c4e5fa47158da --- /dev/null +++ b/test/fixtures/source-map/no-throw.js @@ -0,0 +1,34 @@ +var Foo = /** @class */ (function () { + function Foo(x) { + if (x === void 0) { x = 33; } + this.x = x ? x : 99; + if (this.x) { + this.methodA(); + } + else { + this.methodB(); + } + this.methodC(); + } + Foo.prototype.methodA = function () { + }; + Foo.prototype.methodB = function () { + }; + Foo.prototype.methodC = function () { + }; + Foo.prototype.methodD = function () { + }; + return Foo; +}()); +var a = new Foo(0); +var b = new Foo(33); +a.methodD(); +module.exports = { + a: a, + b: b, + Foo: Foo, +}; +// To recreate: +// +// npx tsc --outDir test/fixtures/source-map --sourceMap --inlineSources test/fixtures/source-map/no-throw.ts +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm8tdGhyb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJuby10aHJvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtJQUVFLGFBQWEsQ0FBTTtRQUFOLGtCQUFBLEVBQUEsTUFBTTtRQUNqQixJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDbkIsSUFBSSxJQUFJLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDWCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQzthQUFNLENBQUM7WUFDTixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQztRQUNELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUNoQixDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDRCxxQkFBTyxHQUFQO0lBRUEsQ0FBQztJQUNELHFCQUFPLEdBQVA7SUFFQSxDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDSCxVQUFDO0FBQUQsQ0FBQyxBQXZCRCxJQXVCQztBQUVELElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFBO0FBQ3BCLElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFBO0FBQ3JCLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtBQU1YLE1BQU0sQ0FBQyxPQUFPLEdBQUc7SUFDZixDQUFDLEdBQUE7SUFDRCxDQUFDLEdBQUE7SUFDRCxHQUFHLEtBQUE7Q0FDSixDQUFBO0FBRUQsZUFBZTtBQUNmLEVBQUU7QUFDRiw2R0FBNkciLCJzb3VyY2VzQ29udGVudCI6WyJjbGFzcyBGb28ge1xuICB4O1xuICBjb25zdHJ1Y3RvciAoeCA9IDMzKSB7XG4gICAgdGhpcy54ID0geCA/IHggOiA5OVxuICAgIGlmICh0aGlzLngpIHtcbiAgICAgIHRoaXMubWV0aG9kQSgpXG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMubWV0aG9kQigpXG4gICAgfVxuICAgIHRoaXMubWV0aG9kQygpXG4gIH1cbiAgbWV0aG9kQSAoKSB7XG5cbiAgfVxuICBtZXRob2RCICgpIHtcblxuICB9XG4gIG1ldGhvZEMgKCkge1xuXG4gIH1cbiAgbWV0aG9kRCAoKSB7XG5cbiAgfVxufVxuXG5jb25zdCBhID0gbmV3IEZvbygwKVxuY29uc3QgYiA9IG5ldyBGb28oMzMpXG5hLm1ldGhvZEQoKVxuXG5kZWNsYXJlIGNvbnN0IG1vZHVsZToge1xuICBleHBvcnRzOiBhbnlcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG4gIGEsXG4gIGIsXG4gIEZvbyxcbn1cblxuLy8gVG8gcmVjcmVhdGU6XG4vL1xuLy8gbnB4IHRzYyAtLW91dERpciB0ZXN0L2ZpeHR1cmVzL3NvdXJjZS1tYXAgLS1zb3VyY2VNYXAgLS1pbmxpbmVTb3VyY2VzIHRlc3QvZml4dHVyZXMvc291cmNlLW1hcC9uby10aHJvdy50c1xuIl19 \ No newline at end of file diff --git a/test/fixtures/source-map/no-throw.ts b/test/fixtures/source-map/no-throw.ts new file mode 100644 index 00000000000000..71d065bca933d2 --- /dev/null +++ b/test/fixtures/source-map/no-throw.ts @@ -0,0 +1,42 @@ +class Foo { + x; + constructor (x = 33) { + this.x = x ? x : 99 + if (this.x) { + this.methodA() + } else { + this.methodB() + } + this.methodC() + } + methodA () { + + } + methodB () { + + } + methodC () { + + } + methodD () { + + } +} + +const a = new Foo(0) +const b = new Foo(33) +a.methodD() + +declare const module: { + exports: any +} + +module.exports = { + a, + b, + Foo, +} + +// To recreate: +// +// npx tsc --outDir test/fixtures/source-map --inlineSourceMap --inlineSources test/fixtures/source-map/no-throw.ts diff --git a/test/fixtures/source-map/no-throw2.js b/test/fixtures/source-map/no-throw2.js new file mode 100644 index 00000000000000..57a294ff3a2a63 --- /dev/null +++ b/test/fixtures/source-map/no-throw2.js @@ -0,0 +1,35 @@ +var Foo = /** @class */ (function () { + function Foo(x) { + if (x === void 0) { x = 33; } + this.x = x ? x : 99; + if (this.x) { + this.methodA(); + } + else { + this.methodB(); + } + this.methodC(); + } + Foo.prototype.methodA = function () { + }; + Foo.prototype.methodB = function () { + }; + Foo.prototype.methodC = function () { + }; + Foo.prototype.methodD = function () { + }; + return Foo; +}()); +var a = new Foo(0); +var b = new Foo(33); +a.methodD(); +module.exports = { + a: a, + b: b, + Foo: Foo, +}; +// To recreate: +// +// npx tsc --outDir test/fixtures/source-map --sourceMap --inlineSources test/fixtures/source-map/no-throw.ts +// cp test/fixtures/source-map/no-throw.ts test/fixtures/source-map/no-throw2.ts +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm8tdGhyb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJuby10aHJvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtJQUVFLGFBQWEsQ0FBTTtRQUFOLGtCQUFBLEVBQUEsTUFBTTtRQUNqQixJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDbkIsSUFBSSxJQUFJLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDWCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQzthQUFNLENBQUM7WUFDTixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDaEIsQ0FBQztRQUNELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUNoQixDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDRCxxQkFBTyxHQUFQO0lBRUEsQ0FBQztJQUNELHFCQUFPLEdBQVA7SUFFQSxDQUFDO0lBQ0QscUJBQU8sR0FBUDtJQUVBLENBQUM7SUFDSCxVQUFDO0FBQUQsQ0FBQyxBQXZCRCxJQXVCQztBQUVELElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFBO0FBQ3BCLElBQU0sQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFBO0FBQ3JCLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtBQU1YLE1BQU0sQ0FBQyxPQUFPLEdBQUc7SUFDZixDQUFDLEdBQUE7SUFDRCxDQUFDLEdBQUE7SUFDRCxHQUFHLEtBQUE7Q0FDSixDQUFBO0FBRUQsZUFBZTtBQUNmLEVBQUU7QUFDRiw2R0FBNkciLCJzb3VyY2VzQ29udGVudCI6WyJjbGFzcyBGb28ge1xuICB4O1xuICBjb25zdHJ1Y3RvciAoeCA9IDMzKSB7XG4gICAgdGhpcy54ID0geCA/IHggOiA5OVxuICAgIGlmICh0aGlzLngpIHtcbiAgICAgIHRoaXMubWV0aG9kQSgpXG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMubWV0aG9kQigpXG4gICAgfVxuICAgIHRoaXMubWV0aG9kQygpXG4gIH1cbiAgbWV0aG9kQSAoKSB7XG5cbiAgfVxuICBtZXRob2RCICgpIHtcblxuICB9XG4gIG1ldGhvZEMgKCkge1xuXG4gIH1cbiAgbWV0aG9kRCAoKSB7XG5cbiAgfVxufVxuXG5jb25zdCBhID0gbmV3IEZvbygwKVxuY29uc3QgYiA9IG5ldyBGb28oMzMpXG5hLm1ldGhvZEQoKVxuXG5kZWNsYXJlIGNvbnN0IG1vZHVsZToge1xuICBleHBvcnRzOiBhbnlcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG4gIGEsXG4gIGIsXG4gIEZvbyxcbn1cblxuLy8gVG8gcmVjcmVhdGU6XG4vL1xuLy8gbnB4IHRzYyAtLW91dERpciB0ZXN0L2ZpeHR1cmVzL3NvdXJjZS1tYXAgLS1zb3VyY2VNYXAgLS1pbmxpbmVTb3VyY2VzIHRlc3QvZml4dHVyZXMvc291cmNlLW1hcC9uby10aHJvdy50c1xuIl19 \ No newline at end of file diff --git a/test/fixtures/source-map/output/source_map_disabled_by_api.js b/test/fixtures/source-map/output/source_map_disabled_by_api.js index d94a6310cff7ae..8f455f26b6c9c4 100644 --- a/test/fixtures/source-map/output/source_map_disabled_by_api.js +++ b/test/fixtures/source-map/output/source_map_disabled_by_api.js @@ -15,10 +15,10 @@ try { console.log(e); } +// Delete the CJS module cache and loading the module again with source maps +// support enabled programmatically. delete require.cache[require .resolve('../enclosing-call-site-min.js')]; - -// Re-enable. process.setSourceMapsEnabled(true); assert.strictEqual(process.sourceMapsEnabled, true); diff --git a/test/fixtures/source-map/output/source_map_prepare_stack_trace.js b/test/fixtures/source-map/output/source_map_prepare_stack_trace.js index d49bad116b479f..1b04e0a3ac221b 100644 --- a/test/fixtures/source-map/output/source_map_prepare_stack_trace.js +++ b/test/fixtures/source-map/output/source_map_prepare_stack_trace.js @@ -20,10 +20,8 @@ try { console.log(e); } -delete require.cache[require - .resolve('../enclosing-call-site-min.js')]; - -// Disable +// Source maps support is disabled programmatically even without deleting the +// CJS module cache. process.setSourceMapsEnabled(false); assert.strictEqual(process.sourceMapsEnabled, false); diff --git a/test/parallel/test-internal-iterable-weak-map.js b/test/parallel/test-internal-iterable-weak-map.js deleted file mode 100644 index f2befe13da87f3..00000000000000 --- a/test/parallel/test-internal-iterable-weak-map.js +++ /dev/null @@ -1,101 +0,0 @@ -// Flags: --expose-gc --expose-internals -'use strict'; - -const common = require('../common'); -const { deepStrictEqual, strictEqual } = require('assert'); -const { IterableWeakMap } = require('internal/util/iterable_weak_map'); - -// Ensures iterating over the map does not rely on methods which can be -// mutated by users. -Reflect.getPrototypeOf(function*() {}).prototype.next = common.mustNotCall(); -Reflect.getPrototypeOf(new Set()[Symbol.iterator]()).next = - common.mustNotCall(); - -// It drops entry if a reference is no longer held. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - delete _cache.moduleB; - setImmediate(() => { - _cache; // eslint-disable-line no-unused-expressions - globalThis.gc(); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); - }); -} - -// It updates an existing entry, if the same key is provided twice. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'goodbye'); - wm.set(_cache.moduleB, 'goodnight'); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodnight']); -} - -// It allows entry to be deleted by key. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - wm.delete(_cache.moduleB); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); -} - -// It handles delete for key that does not exist. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleC, 'goodbye'); - wm.delete(_cache.moduleB); - const values = [...wm]; - deepStrictEqual(values, ['hello', 'goodbye']); -} - -// It allows an entry to be fetched by key. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - moduleB: {}, - moduleC: {}, - }; - wm.set(_cache.moduleA, 'hello'); - wm.set(_cache.moduleB, 'discard'); - wm.set(_cache.moduleC, 'goodbye'); - strictEqual(wm.get(_cache.moduleB), 'discard'); -} - -// It returns true for has() if key exists. -{ - const wm = new IterableWeakMap(); - const _cache = { - moduleA: {}, - }; - wm.set(_cache.moduleA, 'hello'); - strictEqual(wm.has(_cache.moduleA), true); -} diff --git a/test/parallel/test-source-map-cjs-require-cache.js b/test/parallel/test-source-map-cjs-require-cache.js new file mode 100644 index 00000000000000..42ec0b229526fa --- /dev/null +++ b/test/parallel/test-source-map-cjs-require-cache.js @@ -0,0 +1,36 @@ +// Flags: --enable-source-maps --max-old-space-size=10 --expose-gc + +/** + * This test verifies that the source map of a CJS module is cleared after the + * CJS module is reclaimed by GC. + */ + +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const { findSourceMap } = require('node:module'); + +const moduleId = require.resolve('../fixtures/source-map/no-throw.js'); +const moduleIdRepeat = require.resolve('../fixtures/source-map/no-throw2.js'); + +function run(moduleId) { + require(moduleId); + delete require.cache[moduleId]; + const idx = module.children.findIndex((child) => child.id === moduleId); + assert.ok(idx >= 0); + module.children.splice(idx, 1); + + // Verify that the source map is still available + assert.notStrictEqual(findSourceMap(moduleId), undefined); +} + +// Run the test in a function scope so that every variable can be reclaimed by GC. +run(moduleId); + +// Run until the source map is cleared by GC, or fail the test after determined iterations. +common.gcUntil('SourceMap of deleted CJS module is cleared', () => { + // Repetitively load a second module with --max-old-space-size=10 to make GC more aggressive. + run(moduleIdRepeat); + // Verify that the source map is cleared. + return findSourceMap(moduleId) == null; +});