Skip to content

Commit

Permalink
src,lib: retrieve parsed source map url from v8
Browse files Browse the repository at this point in the history
V8 already parses the source map magic comments. Currently, only scripts
and functions expose the parsed source map URLs. It is unnecessary to
parse the source map magic comments again when the parsed information is
available.
  • Loading branch information
legendecas committed Sep 26, 2022
1 parent e6018e2 commit 936cc1e
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 110 deletions.
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
const vm = require('node:vm');

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) {
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 });

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 });
});

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;
}

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

0 comments on commit 936cc1e

Please sign in to comment.