Skip to content

Commit

Permalink
Merge pull request #6 from hildjj/line-col-offsets
Browse files Browse the repository at this point in the history
Add support for line and column offsets.  Up coverage.
  • Loading branch information
hildjj authored Feb 26, 2024
2 parents fbbe9f9 + c5f9b1a commit ebbcae4
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ jobs:
run: npm run build
- name: Test
run: npm run test
- uses: codecov/codecov-action@v3
with:
files: coverage/lcov.info
3 changes: 3 additions & 0 deletions .ncurc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dep": ["prod", "dev", "packageManager"]
}
9 changes: 5 additions & 4 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
.c8rc
.github/
.ncurc
.vscode/
coverage/
eslint.config.js
pnpm-lock.yaml
test/
coverage/
.github/
.vscode/
tsconfig.json
.c8rc
15 changes: 13 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export = fromMem;
*/
declare function fromMem(code: string, options: FromMemOptions): Promise<unknown>;
declare namespace fromMem {
export { guessModuleType, FromMemOptions, ModuleType };
export { guessModuleType, SourceFormat, FromMemOptions, ModuleType };
}
/**
* Options for how to process code.
Expand All @@ -19,7 +19,7 @@ type FromMemOptions = {
* What format does the code have? "guess" means to read the closest
* package.json file looking for the "type" key.
*/
format?: "amd" | "bare" | "commonjs" | "es" | "globals" | "guess" | "umd" | undefined;
format?: SourceFormat | undefined;
/**
* What is the fully-qualified synthetic
* filename for the code? Most important is the directory, which is used to
Expand All @@ -41,6 +41,16 @@ type FromMemOptions = {
* exported from the module?
*/
globalExport?: string | undefined;
/**
* Specifies the line number offset that is
* displayed in stack traces produced by this script.
*/
lineOffset?: number | undefined;
/**
* Specifies the first-line column number
* offset that is displayed in stack traces produced by this script.
*/
columnOffset?: number | undefined;
};
/**
* Figure out the module type for the given file. If no package.json is
Expand All @@ -54,4 +64,5 @@ declare function guessModuleType(filename: string): Promise<ModuleType>;
declare namespace guessModuleType {
function clearCache(): void;
}
type SourceFormat = "amd" | "bare" | "cjs" | "commonjs" | "es" | "es6" | "esm" | "globals" | "guess" | "mjs" | "module" | "umd";
type ModuleType = "commonjs" | "es";
42 changes: 37 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// Ideas taken from the "module-from-string" and "eval" modules, neither of
// which were situated correctly to be used as-is.

const fs = require("node:fs/promises");
const vm = require("node:vm");
const { Module } = require("node:module");
const fs = require("node:fs/promises");
const path = require("node:path");
const url = require("node:url");
const semver = require("semver");
const url = require("node:url");
const vm = require("node:vm");

// These already exist in a new, blank VM. Date, JSON, NaN, etc.
// Things from the core language.
Expand All @@ -37,11 +37,26 @@ const globalContext = Object.fromEntries(
// In node <15, console is in vmGlobals.
globalContext.console = console;

/**
* @typedef {"amd"
* | "bare"
* | "cjs"
* | "commonjs"
* | "es"
* | "es6"
* | "esm"
* | "globals"
* | "guess"
* | "mjs"
* | "module"
* | "umd" } SourceFormat
*/

/**
* Options for how to process code.
*
* @typedef {object} FromMemOptions
* @property {"amd"|"bare"|"commonjs"|"es"|"globals"|"guess"|"umd"} [format="commonjs"]
* @property {SourceFormat} [format="commonjs"]
* What format does the code have? "guess" means to read the closest
* package.json file looking for the "type" key.
* @property {string} filename What is the fully-qualified synthetic
Expand All @@ -53,6 +68,10 @@ globalContext.console = console;
* properties that node gives to all modules. (e.g. Buffer, process).
* @property {string} [globalExport=null] For type "globals", what name is
* exported from the module?
* @property {number} [lineOffset=0] Specifies the line number offset that is
* displayed in stack traces produced by this script.
* @property {number} [columnOffset=0] Specifies the first-line column number
* offset that is displayed in stack traces produced by this script.
*/

/**
Expand All @@ -69,7 +88,11 @@ function requireString(code, dirname, options) {
const m = new Module(options.filename, module); // Current module is parent.
// This is the function that will be called by `require()` in the parser.
m.require = Module.createRequire(options.filename);
const script = new vm.Script(code, { filename: options.filename });
const script = new vm.Script(code, {
filename: options.filename,
lineOffset: options.lineOffset,
columnOffset: options.columnOffset,
});
return script.runInNewContext({
module: m,
exports: m.exports,
Expand Down Expand Up @@ -123,6 +146,8 @@ async function importString(code, dirname, options) {

const mod = new vm.SourceTextModule(code, {
identifier: fileUrl,
lineOffset: options.lineOffset,
columnOffset: options.columnOffset,
context: vm.createContext(options.context),
initializeImportMeta(meta) {
meta.url = fileUrl;
Expand Down Expand Up @@ -226,6 +251,8 @@ async function fromMem(code, options) {
context: {},
includeGlobals: true,
globalExport: undefined,
lineOffset: 0,
columnOffset: 0,
...options,
};

Expand Down Expand Up @@ -255,10 +282,15 @@ async function fromMem(code, options) {
}
switch (options.format) {
case "bare":
case "cjs":
case "commonjs":
case "umd":
return requireString(code, dirname, options);
case "es":
case "es6":
case "esm":
case "module":
case "mjs":
// Returns promise
return importString(code, dirname, options);
// I don't care enough about amd and globals to figure out how to load them.
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
],
"author": "Joe Hildebrand <[email protected]>",
"license": "MIT",
"dependencies": {
"semver": "7.6.0"
},
"devDependencies": {
"@peggyjs/eslint-config": "3.2.3",
"@types/node": "20.11.20",
Expand All @@ -24,11 +27,8 @@
"eslint": "8.57.0",
"typescript": "5.3.3"
},
"packageManager": "[email protected].3",
"packageManager": "[email protected].4",
"engines": {
"node": ">=20.8"
},
"dependencies": {
"semver": "7.6.0"
}
}
52 changes: 51 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const fromMem = require("../index.js");
const { join, parse } = require("node:path");
const { pathToFileURL } = require("node:url");
const test = require("node:test");
const vm = require("node:vm");

test("options", async() => {
assert.equal(typeof fromMem, "function");
Expand Down Expand Up @@ -32,7 +33,17 @@ module.exports = foo() + 2`, {
filename: join(__dirname, "test3.js"),
format: "bare",
}), (/** @type {Error} */ err) => {
assert(/test3\.js/.test(err.stack), err.stack);
assert.match(err.stack, /test3\.js/);
return true;
});

await assert.rejects(() => fromMem("throw new Error('foo')", {
filename: join(__dirname, "test4.js"),
format: "cjs",
lineOffset: 13,
columnOffset: 43,
}), (/** @type {Error} */ err) => {
assert.match(err.stack, /test4\.js:14:50/);
return true;
});
});
Expand Down Expand Up @@ -121,4 +132,43 @@ export default 8`, {
format: "es",
});
assert.equal(mjs8.default, 8);

await assert.rejects(() => fromMem("throw new Error('foo')", {
filename: join(__dirname, "test9.js"),
format: "mjs",
lineOffset: 13,
columnOffset: 43,
}), (/** @type {Error} */ err) => {
assert.match(err.stack, /test9\.js:14:50/);
return true;
});
});

test("version", async() => {
const ver = process.version;
Object.defineProperty(process, "version", {
value: "v18.0.0",
});
await assert.rejects(() => fromMem("43", {
filename: join(__dirname, "test10.js"),
format: "es6",
}), /Requires node.js 20.8\+ or 21\./);

// Reset
Object.defineProperty(process, "version", {
value: ver,
});
});

test("no SourceTextModule", async() => {
const stm = vm.SourceTextModule;
delete vm.SourceTextModule;

await assert.rejects(() => fromMem("44", {
filename: join(__dirname, "test11.js"),
format: "module",
}), /Start node with --experimental-vm-modules for this to work/);

// Reset
vm.SourceTextModule = stm;
});

0 comments on commit ebbcae4

Please sign in to comment.