Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src,lib: retrieve parsed source map url from v8 #44798

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions doc/api/vm.md
Expand Up @@ -344,6 +344,29 @@ console.log(globalVar);
// 1000
```

### `script.sourceMapURL`

<!-- YAML
added: REPLACEME
-->

* {string|undefined}

When the script is compiled from a source that contains a source map magic
comment, this property will be set to the URL of the source map.

```js
legendecas marked this conversation as resolved.
Show resolved Hide resolved
const vm = require('node:vm');
legendecas marked this conversation as resolved.
Show resolved Hide resolved

const script = new vm.Script(`
function myFunc() {}
//# sourceMappingURL=sourcemap.json
`);

console.log(script.sourceMapURL);
// Prints: sourcemap.json
```

## Class: `vm.Module`

<!-- YAML
Expand Down
20 changes: 15 additions & 5 deletions lib/internal/modules/cjs/loader.js
Expand Up @@ -86,7 +86,8 @@ const {
filterOwnProperties,
setOwnProperty,
} = require('internal/util');
const vm = require('vm');
const { Script } = require('vm');
const { internalCompileFunction } = require('internal/vm');
const assert = require('internal/assert');
const fs = require('fs');
const internalFS = require('internal/fs/utils');
Expand Down Expand Up @@ -1073,19 +1074,25 @@ let hasPausedEntry = false;
function wrapSafe(filename, content, cjsModuleInstance) {
if (patched) {
const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {
const script = new Script(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});

maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);

return script.runInThisContext({
displayErrors: true,
});
}

try {
return vm.compileFunction(content, [
const result = internalCompileFunction(content, [
'exports',
'require',
'module',
Expand All @@ -1099,6 +1106,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
importAssertions);
},
});

maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);

return result.function;
} catch (err) {
if (process.mainModule === cjsModuleInstance)
enrichCJSError(err, content);
Expand All @@ -1119,7 +1130,6 @@ Module.prototype._compile = function(content, filename) {
policy.manifest.assertIntegrity(moduleURL, content);
}

maybeCacheSourceMap(filename, content, this);
const compiledWrapper = wrapSafe(filename, content, this);

let inspectorWrapper = null;
Expand Down
31 changes: 21 additions & 10 deletions lib/internal/source_map/source_map_cache.js
Expand Up @@ -97,7 +97,21 @@ function extractSourceURLMagicComment(content) {
return sourceURL;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL) {
function extractSourceMapURLMagicComment(content) {
let match;
let lastMatch;
// A while loop is used here to get the last occurrence of sourceMappingURL.
// This is needed so that we don't match sourceMappingURL in string literals.
while ((match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content))) {
lastMatch = match;
}
if (lastMatch == null) {
return null;
}
return lastMatch.groups.sourceMappingURL;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
try {
Expand All @@ -108,20 +122,17 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
return;
}

let match;
let lastMatch;
// A while loop is used here to get the last occurrence of sourceMappingURL.
// This is needed so that we don't match sourceMappingURL in string literals.
while ((match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content))) {
lastMatch = match;
if (sourceMapURL === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it by design that we still do the regex parse when V8 told us that there is no source map? If so, should we add a test case, e.g.:

checkSourceMapUrl(`
function myFunc() {}
`
//# sourceMappingURL=sourcemap.json
`;
`, 'sourcemap.json');

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this fallback is kept here to handle the ES modules?

Copy link
Member Author

@legendecas legendecas Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkrems @joyeecheung thanks for the suggestion. We don't need to apply the regex again when the parse result of the script indicates that no source mapping URL is available. I've updated the code to eliminate the duplicated scans.

It is true that the fallback is left here to handle the ES modules.

sourceMapURL = extractSourceMapURLMagicComment(content);
}

if (sourceURL === undefined) {
sourceURL = extractSourceURLMagicComment(content);
}
if (lastMatch) {
const data = dataFromUrl(filename, lastMatch.groups.sourceMappingURL);
const url = data ? null : lastMatch.groups.sourceMappingURL;

if (sourceMapURL) {
const data = dataFromUrl(filename, sourceMapURL);
const url = data ? null : sourceMapURL;
if (cjsModuleInstance) {
cjsSourceMapCache.set(cjsModuleInstance, {
filename,
Expand Down
113 changes: 113 additions & 0 deletions lib/internal/vm.js
@@ -0,0 +1,113 @@
'use strict';

const {
ArrayPrototypeForEach,
} = primordials;

const {
compileFunction,
isContext: _isContext,
} = internalBinding('contextify');
const {
validateArray,
validateBoolean,
validateBuffer,
validateFunction,
validateObject,
validateString,
validateUint32,
} = require('internal/validators');
const {
ERR_INVALID_ARG_TYPE,
} = require('internal/errors').codes;

function isContext(object) {
validateObject(object, 'object', { allowArray: true });
legendecas marked this conversation as resolved.
Show resolved Hide resolved

return _isContext(object);
}

function internalCompileFunction(code, params, options) {
validateString(code, 'code');
if (params !== undefined) {
validateArray(params, 'params');
ArrayPrototypeForEach(params,
(param, i) => validateString(param, `params[${i}]`));
}

const {
filename = '',
columnOffset = 0,
lineOffset = 0,
cachedData = undefined,
produceCachedData = false,
parsingContext = undefined,
contextExtensions = [],
importModuleDynamically,
} = options;

validateString(filename, 'options.filename');
validateUint32(columnOffset, 'options.columnOffset');
validateUint32(lineOffset, 'options.lineOffset');
if (cachedData !== undefined)
validateBuffer(cachedData, 'options.cachedData');
validateBoolean(produceCachedData, 'options.produceCachedData');
if (parsingContext !== undefined) {
if (
typeof parsingContext !== 'object' ||
parsingContext === null ||
!isContext(parsingContext)
) {
throw new ERR_INVALID_ARG_TYPE(
'options.parsingContext',
'Context',
parsingContext
);
}
}
validateArray(contextExtensions, 'options.contextExtensions');
ArrayPrototypeForEach(contextExtensions, (extension, i) => {
const name = `options.contextExtensions[${i}]`;
validateObject(extension, name, { nullable: true });
legendecas marked this conversation as resolved.
Show resolved Hide resolved
});

const result = compileFunction(
code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext,
contextExtensions,
params
);

if (produceCachedData) {
result.function.cachedDataProduced = result.cachedDataProduced;
}

if (result.cachedData) {
result.function.cachedData = result.cachedData;
}

if (importModuleDynamically !== undefined) {
validateFunction(importModuleDynamically,
'options.importModuleDynamically');
const { importModuleDynamicallyWrap } =
require('internal/vm/module');
legendecas marked this conversation as resolved.
Show resolved Hide resolved
const { callbackMap } = internalBinding('module_wrap');
const wrapped = importModuleDynamicallyWrap(importModuleDynamically);
const func = result.function;
callbackMap.set(result.cacheKey, {
importModuleDynamically: (s, _k, i) => wrapped(s, func, i),
});
}

return result;
}

module.exports = {
internalCompileFunction,
isContext,
};
92 changes: 5 additions & 87 deletions lib/vm.js
Expand Up @@ -32,9 +32,7 @@ const {
ContextifyScript,
MicrotaskQueue,
makeContext,
isContext: _isContext,
constants,
compileFunction: _compileFunction,
measureMemory: _measureMemory,
} = internalBinding('contextify');
const {
Expand All @@ -45,9 +43,7 @@ const {
isArrayBufferView,
} = require('internal/util/types');
const {
validateArray,
validateBoolean,
validateBuffer,
validateFunction,
validateInt32,
validateObject,
Expand All @@ -60,6 +56,10 @@ const {
kEmptyObject,
kVmBreakFirstLineSymbol,
} = require('internal/util');
const {
internalCompileFunction,
isContext,
} = require('internal/vm');
const kParsingContext = Symbol('script parsing context');

class Script extends ContextifyScript {
Expand Down Expand Up @@ -213,12 +213,6 @@ function getContextOptions(options) {
return contextOptions;
}

function isContext(object) {
validateObject(object, 'object', { allowArray: true });

return _isContext(object);
}

let defaultContextNameIndex = 1;
function createContext(contextObject = {}, options = kEmptyObject) {
if (isContext(contextObject)) {
Expand Down Expand Up @@ -314,83 +308,7 @@ function runInThisContext(code, options) {
}

function compileFunction(code, params, options = kEmptyObject) {
validateString(code, 'code');
if (params !== undefined) {
validateArray(params, 'params');
ArrayPrototypeForEach(params,
(param, i) => validateString(param, `params[${i}]`));
}

const {
filename = '',
columnOffset = 0,
lineOffset = 0,
cachedData = undefined,
produceCachedData = false,
parsingContext = undefined,
contextExtensions = [],
importModuleDynamically,
} = options;

validateString(filename, 'options.filename');
validateUint32(columnOffset, 'options.columnOffset');
validateUint32(lineOffset, 'options.lineOffset');
if (cachedData !== undefined)
validateBuffer(cachedData, 'options.cachedData');
validateBoolean(produceCachedData, 'options.produceCachedData');
if (parsingContext !== undefined) {
if (
typeof parsingContext !== 'object' ||
parsingContext === null ||
!isContext(parsingContext)
) {
throw new ERR_INVALID_ARG_TYPE(
'options.parsingContext',
'Context',
parsingContext
);
}
}
validateArray(contextExtensions, 'options.contextExtensions');
ArrayPrototypeForEach(contextExtensions, (extension, i) => {
const name = `options.contextExtensions[${i}]`;
validateObject(extension, name, { nullable: true });
});

const result = _compileFunction(
code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext,
contextExtensions,
params
);

if (produceCachedData) {
result.function.cachedDataProduced = result.cachedDataProduced;
}

if (result.cachedData) {
result.function.cachedData = result.cachedData;
}

if (importModuleDynamically !== undefined) {
validateFunction(importModuleDynamically,
'options.importModuleDynamically');
const { importModuleDynamicallyWrap } =
require('internal/vm/module');
const { callbackMap } = internalBinding('module_wrap');
const wrapped = importModuleDynamicallyWrap(importModuleDynamically);
const func = result.function;
callbackMap.set(result.cacheKey, {
importModuleDynamically: (s, _k, i) => wrapped(s, func, i),
});
}

return result.function;
return internalCompileFunction(code, params, options).function;
}

const measureMemoryModes = {
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Expand Up @@ -276,6 +276,7 @@
V(sni_context_err_string, "Invalid SNI context") \
V(sni_context_string, "sni_context") \
V(source_string, "source") \
V(source_map_url_string, "sourceMapURL") \
V(stack_string, "stack") \
V(standard_name_string, "standardName") \
V(start_time_string, "startTime") \
Expand Down