Skip to content

Commit 44aa272

Browse files
committed
lib: cache source maps in vm sources
Cache source maps found in sources parsed with `vm.Script`, `vm.compileFunction`, and `vm.SourceTextModule`. Also, retrieve source url from V8 parsing results. Not like filenames returned by `CallSite.getFileName()` in translating stack traces, when generating source lines prepended to exceptions, only resource names can be used as an index to find source maps, which can be source url magic comments instead. Source url magic comments can be either a file path or a URL. To verify that source urls with absolute file paths in the source lines are correctly translated, snapshots should include the full snapshot urls rather than neutralizing all the path strings in the stack traces.
1 parent bae14b7 commit 44aa272

35 files changed

+284
-101
lines changed

lib/internal/modules/cjs/loader.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1308,9 +1308,9 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
13081308
);
13091309

13101310
// Cache the source map for the module if present.
1311-
const { sourceMapURL } = script;
1311+
const { sourceMapURL, sourceURL } = script;
13121312
if (sourceMapURL) {
1313-
maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL);
1313+
maybeCacheSourceMap(filename, content, this, false, sourceURL, sourceMapURL);
13141314
}
13151315

13161316
return {
@@ -1332,7 +1332,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
13321332

13331333
// Cache the source map for the module if present.
13341334
if (result.sourceMapURL) {
1335-
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
1335+
maybeCacheSourceMap(filename, content, this, false, result.sourceURL, result.sourceMapURL);
13361336
}
13371337

13381338
return result;

lib/internal/modules/esm/loader.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ class ModuleLoader {
280280
const wrap = new ModuleWrap(url, undefined, source, 0, 0);
281281
// Cache the source map for the module if present.
282282
if (wrap.sourceMapURL) {
283-
maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL);
283+
maybeCacheSourceMap(url, source, null, false, wrap.sourceURL, wrap.sourceMapURL);
284284
}
285285
const { registerModule } = require('internal/modules/esm/utils');
286286
// TODO(joyeecheung): refactor so that the default options are shared across

lib/internal/modules/esm/translators.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
177177
const module = new ModuleWrap(url, undefined, source, 0, 0);
178178
// Cache the source map for the module if present.
179179
if (module.sourceMapURL) {
180-
maybeCacheSourceMap(url, source, null, false, undefined, module.sourceMapURL);
180+
maybeCacheSourceMap(url, source, null, false, module.sourceURL, module.sourceMapURL);
181181
}
182182
const { registerModule } = require('internal/modules/esm/utils');
183183
registerModule(module, {
@@ -227,7 +227,7 @@ function loadCJSModule(module, source, url, filename) {
227227
}
228228
// Cache the source map for the cjs module if present.
229229
if (compileResult.sourceMapURL) {
230-
maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL);
230+
maybeCacheSourceMap(url, source, null, false, compileResult.sourceURL, compileResult.sourceMapURL);
231231
}
232232

233233
const compiledWrapper = compileResult.function;

lib/internal/source_map/source_map_cache.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
123123
return;
124124
}
125125

126-
// FIXME: callers should obtain sourceURL from v8 and pass it
127-
// rather than leaving it undefined and extract by regex.
128-
if (sourceURL === undefined) {
129-
sourceURL = extractSourceURLMagicComment(content);
126+
if (sourceURL !== undefined) {
127+
// SourceURL magic comment content might be a file path or URL.
128+
// Normalize the sourceURL to be a file URL if it is a file path.
129+
sourceURL = normalizeReferrerURL(sourceURL);
130130
}
131131

132132
const data = dataFromUrl(filename, sourceMapURL);
@@ -149,9 +149,6 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
149149
sourceURL,
150150
};
151151
generatedSourceMapCache.set(filename, entry);
152-
if (sourceURL) {
153-
generatedSourceMapCache.set(sourceURL, entry);
154-
}
155152
} else {
156153
// If there is no cjsModuleInstance and is not generated source assume we are in a
157154
// "modules/esm" context.
@@ -178,7 +175,7 @@ function maybeCacheGeneratedSourceMap(content) {
178175
return;
179176
}
180177
try {
181-
maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
178+
maybeCacheSourceMap(sourceURL, content, null, true);
182179
} catch (err) {
183180
// This can happen if the filename is not a valid URL.
184181
// If we fail to cache the source map, we should not fail the whole process.

lib/internal/vm.js

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
const {
2626
getOptionValue,
2727
} = require('internal/options');
28+
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
2829
const {
2930
privateSymbols: {
3031
contextify_context_private_symbol,
@@ -151,6 +152,7 @@ function internalCompileFunction(
151152
}
152153

153154
registerImportModuleDynamically(result.function, importModuleDynamically);
155+
maybeCacheSourceMap(filename, code, result.function, false, result.sourceURL, result.sourceMapURL);
154156

155157
return result;
156158
}

lib/internal/vm/module.js

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const kPerContextModuleId = Symbol('kPerContextModuleId');
7777
const kLink = Symbol('kLink');
7878

7979
const { isContext } = require('internal/vm');
80+
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
8081

8182
function isModule(object) {
8283
if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, kWrap)) {
@@ -141,6 +142,7 @@ class Module {
141142
importModuleDynamicallyWrap(options.importModuleDynamically) :
142143
undefined,
143144
};
145+
maybeCacheSourceMap(identifier, sourceText, this, false, this[kWrap].sourceURL, this[kWrap].sourceMapURL);
144146
} else {
145147
assert(syntheticEvaluationSteps);
146148
this[kWrap] = new ModuleWrap(identifier, context,

lib/vm.js

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const {
6363
isContext: _isContext,
6464
registerImportModuleDynamically,
6565
} = require('internal/vm');
66+
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
6667
const {
6768
vm_dynamic_import_main_context_default,
6869
} = internalBinding('symbols');
@@ -126,6 +127,7 @@ class Script extends ContextifyScript {
126127
}
127128

128129
registerImportModuleDynamically(this, importModuleDynamically);
130+
maybeCacheSourceMap(filename, code, this, false, this.sourceURL, this.sourceMapURL);
129131
}
130132

131133
runInThisContext(options) {

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@
300300
V(sni_context_string, "sni_context") \
301301
V(source_string, "source") \
302302
V(source_map_url_string, "sourceMapURL") \
303+
V(source_url_string, "sourceURL") \
303304
V(stack_string, "stack") \
304305
V(standard_name_string, "standardName") \
305306
V(start_time_string, "startTime") \

src/module_wrap.cc

+7
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
265265
return;
266266
}
267267

268+
if (that->Set(context,
269+
realm->env()->source_url_string(),
270+
module->GetUnboundModuleScript()->GetSourceURL())
271+
.IsNothing()) {
272+
return;
273+
}
274+
268275
if (that->Set(context,
269276
realm->env()->source_map_url_string(),
270277
module->GetUnboundModuleScript()->GetSourceMappingURL())

src/node_contextify.cc

+20
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,13 @@ void ContextifyScript::New(const FunctionCallbackInfo<Value>& args) {
905905
return;
906906
}
907907

908+
if (args.This()
909+
->Set(env->context(),
910+
env->source_url_string(),
911+
v8_script->GetSourceURL())
912+
.IsNothing())
913+
return;
914+
908915
if (args.This()
909916
->Set(env->context(),
910917
env->source_map_url_string(),
@@ -1373,6 +1380,15 @@ Local<Object> ContextifyContext::CompileFunctionAndCacheResult(
13731380
Local<Object> result = Object::New(isolate);
13741381
if (result->Set(parsing_context, env->function_string(), fn).IsNothing())
13751382
return Object::New(env->isolate());
1383+
1384+
// ScriptOrigin::ResourceName() returns SourceURL magic comment content if
1385+
// present.
1386+
if (result
1387+
->Set(parsing_context,
1388+
env->source_url_string(),
1389+
fn->GetScriptOrigin().ResourceName())
1390+
.IsNothing())
1391+
return Object::New(env->isolate());
13761392
if (result
13771393
->Set(parsing_context,
13781394
env->source_map_url_string(),
@@ -1650,11 +1666,15 @@ static void CompileFunctionForCJSLoader(
16501666
std::vector<Local<Name>> names = {
16511667
env->cached_data_rejected_string(),
16521668
env->source_map_url_string(),
1669+
env->source_url_string(),
16531670
env->function_string(),
16541671
};
16551672
std::vector<Local<Value>> values = {
16561673
Boolean::New(isolate, cache_rejected),
16571674
fn->GetScriptOrigin().SourceMapUrl(),
1675+
// ScriptOrigin::ResourceName() returns SourceURL magic comment content if
1676+
// present.
1677+
fn->GetScriptOrigin().ResourceName(),
16581678
fn,
16591679
};
16601680
Local<Object> result = Object::New(

src/node_errors.cc

+16-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,20 @@ static std::string GetSourceMapErrorSource(Isolate* isolate,
5757
bool* added_exception_line) {
5858
v8::TryCatch try_catch(isolate);
5959
HandleScope handle_scope(isolate);
60-
Environment* env = Environment::GetCurrent(context);
60+
Realm* realm = Realm::GetCurrent(context);
61+
62+
Local<Function> get_source;
63+
if (realm != nullptr) {
64+
// If we are in a Realm, call the realm specific getSourceMapErrorSource
65+
// callback to avoid passing the JS objects (the exception and trace) across
66+
// the realm boundary.
67+
get_source = realm->get_source_map_error_source();
68+
} else {
69+
Environment* env = Environment::GetCurrent(context);
70+
// The context is created with ContextifyContext, call the principal
71+
// realm's getSourceMapErrorSource callback.
72+
get_source = env->principal_realm()->get_source_map_error_source();
73+
}
6174

6275
// The ScriptResourceName of the message may be different from the one we use
6376
// to compile the script. V8 replaces it when it detects magic comments in
@@ -69,8 +82,8 @@ static std::string GetSourceMapErrorSource(Isolate* isolate,
6982
Local<Value> argv[] = {script_resource_name,
7083
v8::Int32::New(isolate, linenum),
7184
v8::Int32::New(isolate, columnum)};
72-
MaybeLocal<Value> maybe_ret = env->get_source_map_error_source()->Call(
73-
context, Undefined(isolate), arraysize(argv), argv);
85+
MaybeLocal<Value> maybe_ret =
86+
get_source->Call(context, Undefined(isolate), arraysize(argv), argv);
7487
Local<Value> ret;
7588
if (!maybe_ret.ToLocal(&ret)) {
7689
// Ignore the caught exceptions.

test/common/assertSnapshot.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const assert = require('node:assert/strict');
88
const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g;
99
const windowNewlineRegexp = /\r/g;
1010

11+
function replaceExperimentalWarning(str) {
12+
return str.replace(/\(node:\d+\) ExperimentalWarning: (.*)\n/g, '')
13+
.replace('(Use `node --trace-warnings ...` to show where the warning was created)\n', '');
14+
}
15+
1116
function replaceNodeVersion(str) {
1217
return str.replaceAll(process.version, '*');
1318
}
@@ -16,6 +21,10 @@ function replaceStackTrace(str, replacement = '$1*$7$8\n') {
1621
return str.replace(stackFramesRegexp, replacement);
1722
}
1823

24+
function replaceInternalStackTrace(str) {
25+
return str.replaceAll(/(\W+).*node:internal.*/g, '$1*');
26+
}
27+
1928
function replaceWindowsLineEndings(str) {
2029
return str.replace(windowNewlineRegexp, '');
2130
}
@@ -24,8 +33,20 @@ function replaceWindowsPaths(str) {
2433
return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str;
2534
}
2635

27-
function replaceFullPaths(str) {
28-
return str.replaceAll(process.cwd(), '');
36+
function replaceWindowsDriveLetter(str) {
37+
if (!common.isWindows) {
38+
return str;
39+
}
40+
const currentDriveLetter = path.parse(process.cwd()).root.substring(0, 1).toLowerCase();
41+
const regex = new RegExp(`${currentDriveLetter}:`, 'gi');
42+
return str.replaceAll(regex, '');
43+
}
44+
45+
function transformCwd(replacement = '') {
46+
const cwd = process.cwd();
47+
return (str) => {
48+
return str.replaceAll(cwd, replacement);
49+
};
2950
}
3051

3152
function transform(...args) {
@@ -87,11 +108,14 @@ async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...
87108
module.exports = {
88109
assertSnapshot,
89110
getSnapshotPath,
90-
replaceFullPaths,
111+
replaceExperimentalWarning,
91112
replaceNodeVersion,
92113
replaceStackTrace,
114+
replaceInternalStackTrace,
93115
replaceWindowsLineEndings,
94116
replaceWindowsPaths,
117+
replaceWindowsDriveLetter,
95118
spawnAndAssert,
96119
transform,
120+
transformCwd,
97121
};
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
Error: an error!
2-
at functionD (*enclosing-call-site-min.js:1:156)
3-
at functionC (*enclosing-call-site-min.js:1:97)
4-
at functionB (*enclosing-call-site-min.js:1:60)
5-
at functionA (*enclosing-call-site-min.js:1:26)
6-
at Object.<anonymous> (*enclosing-call-site-min.js:1:199)
2+
at functionD (*/test/fixtures/source-map/enclosing-call-site-min.js:1:156)
3+
at functionC (*/test/fixtures/source-map/enclosing-call-site-min.js:1:97)
4+
at functionB (*/test/fixtures/source-map/enclosing-call-site-min.js:1:60)
5+
at functionA (*/test/fixtures/source-map/enclosing-call-site-min.js:1:26)
6+
at Object.<anonymous> (*/test/fixtures/source-map/enclosing-call-site-min.js:1:199)
77
Error: an error!
8-
at functionD (*enclosing-call-site.js:16:17)
9-
at functionC (*enclosing-call-site.js:10:3)
10-
at functionB (*enclosing-call-site.js:6:3)
11-
at functionA (*enclosing-call-site.js:2:3)
12-
at Object.<anonymous> (*enclosing-call-site.js:24:3)
8+
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
9+
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)
10+
at functionB (*/test/fixtures/source-map/enclosing-call-site.js:6:3)
11+
at functionA (*/test/fixtures/source-map/enclosing-call-site.js:2:3)
12+
at Object.<anonymous> (*/test/fixtures/source-map/enclosing-call-site.js:24:3)
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
Error: an error!
2-
at functionD (*enclosing-call-site.js:16:17)
3-
at functionC (*enclosing-call-site.js:10:3)
4-
at functionB (*enclosing-call-site.js:6:3)
5-
at functionA (*enclosing-call-site.js:2:3)
6-
at Object.<anonymous> (*enclosing-call-site.js:24:3)
2+
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
3+
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)
4+
at functionB (*/test/fixtures/source-map/enclosing-call-site.js:6:3)
5+
at functionA (*/test/fixtures/source-map/enclosing-call-site.js:2:3)
6+
at Object.<anonymous> (*/test/fixtures/source-map/enclosing-call-site.js:24:3)
77
Error: an error!
8-
at functionD (*enclosing-call-site-min.js:1:156)
9-
at functionC (*enclosing-call-site-min.js:1:97)
10-
at functionB (*enclosing-call-site-min.js:1:60)
11-
at functionA (*enclosing-call-site-min.js:1:26)
12-
at Object.<anonymous> (*enclosing-call-site-min.js:1:199)
8+
at functionD (*/test/fixtures/source-map/enclosing-call-site-min.js:1:156)
9+
at functionC (*/test/fixtures/source-map/enclosing-call-site-min.js:1:97)
10+
at functionB (*/test/fixtures/source-map/enclosing-call-site-min.js:1:60)
11+
at functionA (*/test/fixtures/source-map/enclosing-call-site-min.js:1:26)
12+
at Object.<anonymous> (*/test/fixtures/source-map/enclosing-call-site-min.js:1:199)
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
*enclosing-call-site.js:26
1+
*/test/fixtures/source-map/enclosing-call-site.js:26
22
throw err
33
^
44

55

66
Error: an error!
7-
at functionD (*enclosing-call-site.js:16:17)
8-
at functionC (*enclosing-call-site.js:10:3)
9-
at functionB (*enclosing-call-site.js:6:3)
10-
at functionA (*enclosing-call-site.js:2:3)
11-
at Object.<anonymous> (*enclosing-call-site.js:24:3)
7+
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
8+
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)
9+
at functionB (*/test/fixtures/source-map/enclosing-call-site.js:6:3)
10+
at functionA (*/test/fixtures/source-map/enclosing-call-site.js:2:3)
11+
at Object.<anonymous> (*/test/fixtures/source-map/enclosing-call-site.js:24:3)
1212

1313
Node.js *

test/fixtures/source-map/output/source_map_eval.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ Error.stackTraceLimit = 3;
77
const fs = require('fs');
88

99
const content = fs.readFileSync(require.resolve('../tabs.js'), 'utf8');
10+
// SourceURL magic comment is hardcoded in the source content.
1011
eval(content);
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
*tabs.coffee:26
1+
/synthethized/workspace/tabs.coffee:26
22
alert "I knew it!"
33
^
44

55

66
ReferenceError: alert is not defined
7-
at Object.eval (*tabs.coffee:26:2)
8-
at eval (*tabs.coffee:1:14)
9-
at Object.<anonymous> (*output*source_map_eval.js:10:1)
7+
at Object.eval (/synthethized/workspace/tabs.coffee:26:2)
8+
at eval (/synthethized/workspace/tabs.coffee:1:14)
9+
at Object.<anonymous> (*/test/fixtures/source-map/output/source_map_eval.js:11:1)
1010

1111
Node.js *
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
*no-source.js:2
1+
*/test/fixtures/source-map/no-source.js:2
22
throw new Error('foo');
33
^
44

55
Error: foo
6-
at Throw (*file-not-exists.ts:2:9)
7-
at Object.<anonymous> (*file-not-exists.ts:5:1)
6+
at Throw (*/test/fixtures/source-map/file-not-exists.ts:2:9)
7+
at Object.<anonymous> (*/test/fixtures/source-map/file-not-exists.ts:5:1)
88

99
Node.js *

0 commit comments

Comments
 (0)