261020.1 · 1d7e0126f3e601103d93586a6f872144f2789dad
chore: self-maintain Node.js features
reading declarations
Changes to test/nodejs.test.js. All files
@@ -1,12 +1,39 @@ const assert = require("node:assert");+const fs = require("node:fs");+const path = require("node:path");+const repl = require("node:repl");+const { PassThrough } = require("node:stream"); const { test } = require("node:test"); const {+ findTopLevelAwaits, getFirstExpression, isRecoverableError, isValidSyntax,+ tokenize, transformModuleSyntax, } = require("../dist/internal.js"); +const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;++function validatedImport(specifier, requiredExports) {+ const checks = requiredExports+ .map(+ (name) =>+ `if (!(${JSON.stringify(name)} in m)) throw new SyntaxError(${JSON.stringify(+ `The requested module '${specifier}' does not provide an export named '${name}'`,+ )}); `,+ )+ .join("");+ return `__nodeREPLDynamicImport(${JSON.stringify(specifier)}).then((m) => { ${checks}return m; })`;+}++function execute(source, namespace) {+ return new AsyncFunction(+ "__nodeREPLDynamicImport",+ transformModuleSyntax(source).code,+ )(async () => namespace);+}+ test("should transform module syntax for the Node.js REPL", () => { const result = transformModuleSyntax( 'import { readFile } from "node:fs/promises";\nreadFile("file");',@@ -17,6 +44,125 @@ test("should transform module syntax for the Node.js REPL", () => { assert.match(result.code, /readFile/); }); +test("should declare imported bindings instead of rewriting references", () => {+ assert.deepStrictEqual(transformModuleSyntax('import fs from "node:fs";'), {+ code: `const { default: fs } = await ${validatedImport("node:fs", ["default"])};`,+ hadModuleSyntax: true,+ });+ assert.deepStrictEqual(+ transformModuleSyntax('import { readFile as rf } from "node:fs";\nrf();'),+ {+ code: `const { readFile: rf } = await ${validatedImport("node:fs", ["readFile"])};\nrf();`,+ hadModuleSyntax: true,+ },+ );+ assert.deepStrictEqual(+ transformModuleSyntax('import def, * as ns from "mod";\ndef; ns;'),+ {+ code: `const ns = await ${validatedImport("mod", ["default"])}; const def = ns.default;\ndef; ns;`,+ hadModuleSyntax: true,+ },+ );+ assert.deepStrictEqual(+ transformModuleSyntax('import { "a-b" as c } from "mod";\nc;'),+ {+ code: `const { "a-b": c } = await ${validatedImport("mod", ["a-b"])};\nc;`,+ hadModuleSyntax: true,+ },+ );+});++test("should retain imported bindings across REPL inputs", async () => {+ const server = repl.start({+ input: new PassThrough(),+ output: new PassThrough(),+ terminal: false,+ prompt: "",+ });+ const namespace = { default: 7, value: 11, "a-b": 13 };+ server.context.__nodeREPLDynamicImport = async () => namespace;+ // Arrays come from the REPL's vm context, so copy them into this realm+ // before comparing prototypes with deepStrictEqual.+ const evaluate = (source) =>+ new Promise((resolve, reject) => {+ server.eval(+ `${transformModuleSyntax(source).code}\n`,+ server.context,+ "repl-import-test",+ (error, value) =>+ error+ ? reject(error)+ : resolve(Array.isArray(value) ? [...value] : value),+ );+ });+ try {+ await evaluate('import first from "m";');+ assert.strictEqual(await evaluate("first"), 7);+ await evaluate('import { value as second, "a-b" as third } from "m";');+ assert.deepStrictEqual(+ await evaluate("[first, second, third]"),+ [7, 11, 13],+ );+ await evaluate('import another, * as ns from "m";');+ assert.deepStrictEqual(await evaluate("[another, ns.value]"), [7, 11]);+ namespace.value = 17;+ assert.deepStrictEqual(await evaluate("[second, ns.value]"), [11, 17]);+ } finally {+ server.close();+ }+});++test("should preserve bare-call this and lexical shadowing", async () => {+ // A bare call of a destructured binding must not receive the namespace+ // object as `this`, unlike the former `__nodeREPLImport0.call()` rewrite.+ const namespace = {+ call() {+ return this === namespace;+ },+ value: 1,+ };+ const { code } = transformModuleSyntax(+ 'import { call, value } from "m";\nconst observed = [call(), (() => { const value = 2; return value; })(), value];',+ );+ const observed = await new AsyncFunction(+ "__nodeREPLDynamicImport",+ `${code}\nreturn observed;`,+ )(async () => namespace);+ assert.deepStrictEqual(observed, [false, 2, 1]);+});++test("should let JavaScript enforce const assignment semantics", async () => {+ for (const assignment of [+ "value = 2",+ "value++",+ "({ value } = { value: 2 })",+ ]) {+ await assert.rejects(+ execute(`import { value } from "m";\n${assignment};`, { value: 1 }),+ /Assignment to constant variable\./,+ );+ }+});++test("should keep missing-export validation and handle special export names", async () => {+ await assert.rejects(+ execute('import missing from "m";', {}),+ /does not provide an export named 'default'/,+ );+ await assert.rejects(+ execute('import { missing } from "m";', {}),+ /does not provide an export named 'missing'/,+ );+ const { code } = transformModuleSyntax(+ 'import { "a-b" as value, __proto__ as proto } from "m";',+ );+ const result = await new AsyncFunction(+ "__nodeREPLDynamicImport",+ `${code}\nreturn [value, proto];`,+ )(async () => ({ "a-b": 1, ["__proto__"]: 2 }));+ assert.deepStrictEqual(result, [1, 2]);+});+ test("should find the first expression at an error column", () => { assert.strictEqual( getFirstExpression("a(); assert.ok(value); b()", 13),@@ -33,3 +179,41 @@ test("should detect recoverable syntax errors", () => { assert.strictEqual(isRecoverableError("function foo() {"), true); assert.strictEqual(isRecoverableError("const value: number = 1"), false); });++test("should tokenize input for syntax highlighting", () => {+ assert.deepStrictEqual(tokenize("const x = 1; // done"), [+ { kind: "keyword", start: 0, end: 5 },+ { kind: "identifier", start: 6, end: 7 },+ { kind: "punctuator", start: 8, end: 9 },+ { kind: "number", start: 10, end: 11 },+ { kind: "punctuator", start: 11, end: 12 },+ { kind: "comment", start: 13, end: 20 },+ ]);+});++test("should find top-level awaits", () => {+ assert.deepStrictEqual(+ findTopLevelAwaits("await 1;\nasync function f() { await 2; }\nawait 3;"),+ [+ { line: 1, column: 0 },+ { line: 3, column: 0 },+ ],+ );+});++const fixturesDir = path.join(__dirname, "fixtures", "nodejs");+for (const [suite, api] of [+ ["tokenize", tokenize],+ ["awaits", findTopLevelAwaits],+ ["recoverable", isRecoverableError],+]) {+ const dir = path.join(fixturesDir, suite);+ for (const file of fs.readdirSync(dir)) {+ const cases = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8"));+ for (const { source, expected } of cases) {+ test(`${suite}/${file}: ${JSON.stringify(source)}`, () => {+ assert.deepStrictEqual(api(source), expected);+ });+ }+ }+}