From 69704df6b3ddd8ff6aa23a33b6b56a582e391762 Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Sun, 25 Feb 2024 23:11:12 -0700 Subject: [PATCH 1/2] Add support for line and column offsets. Up coverage. --- .ncurc | 3 +++ .npmignore | 9 ++++---- index.d.ts | 15 +++++++++++-- index.js | 42 ++++++++++++++++++++++++++++++++----- package.json | 8 +++---- test/index.test.js | 52 +++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 .ncurc diff --git a/.ncurc b/.ncurc new file mode 100644 index 0000000..7f738d6 --- /dev/null +++ b/.ncurc @@ -0,0 +1,3 @@ +{ + "dep": ["prod", "dev", "packageManager"] +} diff --git a/.npmignore b/.npmignore index d3c1602..459d6fa 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,9 @@ +.c8rc +.github/ +.ncurc +.vscode/ +coverage/ eslint.config.js pnpm-lock.yaml test/ -coverage/ -.github/ -.vscode/ tsconfig.json -.c8rc diff --git a/index.d.ts b/index.d.ts index f409b29..f2d7013 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,7 @@ export = fromMem; */ declare function fromMem(code: string, options: FromMemOptions): Promise; declare namespace fromMem { - export { guessModuleType, FromMemOptions, ModuleType }; + export { guessModuleType, SourceFormat, FromMemOptions, ModuleType }; } /** * Options for how to process code. @@ -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 @@ -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 @@ -54,4 +64,5 @@ declare function guessModuleType(filename: string): Promise; declare namespace guessModuleType { function clearCache(): void; } +type SourceFormat = "amd" | "bare" | "cjs" | "commonjs" | "es" | "es6" | "esm" | "globals" | "guess" | "mjs" | "module" | "umd"; type ModuleType = "commonjs" | "es"; diff --git a/index.js b/index.js index 609c375..162a232 100644 --- a/index.js +++ b/index.js @@ -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. @@ -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 @@ -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. */ /** @@ -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, @@ -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; @@ -226,6 +251,8 @@ async function fromMem(code, options) { context: {}, includeGlobals: true, globalExport: undefined, + lineOffset: 0, + columnOffset: 0, ...options, }; @@ -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. diff --git a/package.json b/package.json index ab0d120..22121b5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ ], "author": "Joe Hildebrand ", "license": "MIT", + "dependencies": { + "semver": "7.6.0" + }, "devDependencies": { "@peggyjs/eslint-config": "3.2.3", "@types/node": "20.11.20", @@ -24,11 +27,8 @@ "eslint": "8.57.0", "typescript": "5.3.3" }, - "packageManager": "pnpm@8.15.3", + "packageManager": "pnpm@8.15.4", "engines": { "node": ">=20.8" - }, - "dependencies": { - "semver": "7.6.0" } } diff --git a/test/index.test.js b/test/index.test.js index df84683..384538e 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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"); @@ -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; }); }); @@ -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; }); From c5f9b1ab8d27fdf04262e7cec395836e1708cccb Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Sun, 25 Feb 2024 23:14:02 -0700 Subject: [PATCH 2/2] add codecov --- .github/workflows/node.js.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ea57a4a..7c4fec5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -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