diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index caa07b8e..b68ddafa 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -32,3 +32,17 @@ jobs: cache: 'npm' - run: npm ci - run: npm run tsc + + test-js-eval: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm test --workspace=packages/jsEval diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 2f777ea9..712a8b8b 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -4,6 +4,7 @@ import { expose } from "comlink"; import type { ReplOutput } from "../repl"; import type { WorkerCapabilities } from "./runtime"; import inspect from "object-inspect"; +import { replLikeEval, checkSyntax } from "@my-code/js-eval"; function format(...args: unknown[]): string { // TODO: console.logの第1引数はフォーマット指定文字列を取ることができる @@ -34,41 +35,6 @@ async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ return { capabilities: { interrupt: "restart" } }; } -async function replLikeEval(code: string): Promise { - // eval()の中でconst,letを使って変数を作成した場合、 - // 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、 - // varに置き換えている - if (code.trim().startsWith("const ")) { - code = "var " + code.trim().slice(6); - } else if (code.trim().startsWith("let ")) { - code = "var " + code.trim().slice(4); - } - // eval()の中でclassを作成した場合も同様 - const classRegExp = /^\s*class\s+(\w+)/; - if (classRegExp.test(code)) { - code = code.replace(classRegExp, "var $1 = class $1"); - } - - if (code.trim().startsWith("{") && code.trim().endsWith("}")) { - // オブジェクトは ( ) で囲わなければならない - try { - return self.eval(`(${code})`); - } catch (e) { - if (e instanceof SyntaxError) { - // オブジェクトではなくブロックだった場合、再度普通に実行 - return self.eval(code); - } else { - throw e; - } - } - } else if (/^\s*await\W/.test(code)) { - // promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする - return await self.eval(code.trim().slice(5)); - } else { - return self.eval(code); - } -} - async function runCode( code: string, onOutput: (output: ReplOutput) => void @@ -129,32 +95,6 @@ function runFile( return { updatedFiles: {} as Record }; } -async function checkSyntax( - code: string -): Promise<{ status: "complete" | "incomplete" | "invalid" }> { - try { - // Try to create a Function to check syntax - // new Function(code); // <- not working - self.eval(`() => {${code}}`); - return { status: "complete" }; - } catch (e) { - // Check if it's a syntax error or if more input is expected - if (e instanceof SyntaxError) { - // Simple heuristic: check for "Unexpected end of input" - if ( - e.message.includes("Unexpected token '}'") || - e.message.includes("Unexpected end of input") - ) { - return { status: "incomplete" }; - } else { - return { status: "invalid" }; - } - } else { - return { status: "invalid" }; - } - } -} - async function restoreState(commands: string[]): Promise { // Re-execute all previously successful commands to restore state for (const command of commands) { diff --git a/package-lock.json b/package-lock.json index a02abb5e..53b95448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "my-code", "version": "0.1.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@fontsource-variable/inconsolata": "^5.2.7", "@fontsource/m-plus-rounded-1c": "^5.2.9", @@ -1497,7 +1500,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.5.tgz", "integrity": "sha512-dQ3hZOkUJzeBXfVEPTm2LVbzmWwka1nqd9KyWmB2OMlMfjr7IdUeBX4T7qJctF67d7QDhlX95jMoxu6JG0Eucw==", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" @@ -1527,14 +1529,12 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@better-fetch/fetch": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", - "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==", - "peer": true + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" }, "node_modules/@bjorn3/browser_wasi_shim": { "version": "0.3.0", @@ -1732,7 +1732,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3477,6 +3476,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@my-code/js-eval": { + "resolved": "packages/jsEval", + "link": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3639,7 +3642,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -5290,7 +5292,6 @@ "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -5308,7 +5309,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5384,7 +5384,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5932,8 +5931,7 @@ "version": "5.6.0-beta.115", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.115.tgz", "integrity": "sha512-EJXAW6dbxPuwQnLfTmPB5R3M5uu8qp24ltHdjCcfwGpudKxQRoDEbq1IeGrVLIuRc/8TbnT1U07dXUX7kyGYEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -5971,7 +5969,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6455,7 +6452,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.4.tgz", "integrity": "sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", @@ -7804,7 +7800,6 @@ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7885,7 +7880,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10070,7 +10064,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -10197,7 +10190,6 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.8.tgz", "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -10951,7 +10943,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -11542,8 +11533,7 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -11936,7 +11926,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -11978,7 +11967,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.11.tgz", "integrity": "sha512-L2KPiKmqTDpRdeVDdPjhf43g2/VPe0NCNndq7OKDCgOLWtxe1kbr/zXGIZtYY7kZEAjRf7Bj/mwUFSr+tYC2Yg==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.11", "@swc/helpers": "0.5.15", @@ -12503,7 +12491,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -14170,7 +14157,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14394,7 +14380,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14442,7 +14427,6 @@ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -14452,7 +14436,6 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -14799,7 +14782,6 @@ "integrity": "sha512-EhLJGptSGFi8AEErLiamO3PoGpbRqL+v4Ve36H2B38VxmDgFOSmDhfepBnA14sCQzGf1AEaoZX2DCwZsmO74yQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -14825,7 +14807,6 @@ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.61.1.tgz", "integrity": "sha512-hfYQ16VLPkNi8xE1/V3052S2stM5e+vq3Idpt83sXoDC3R7R1CLgMkK6M6+Qp3G+9GVDNyHCkvohMPdfFTaD4Q==", "license": "MIT OR Apache-2.0", - "peer": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.12.0", @@ -15613,6 +15594,14 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/jsEval": { + "name": "@my-code/js-eval", + "version": "0.1.0", + "devDependencies": { + "tsx": "^4", + "typescript": "^5" + } } } } diff --git a/package.json b/package.json index 29e08269..963fbb91 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "private": true, "type": "module", + "workspaces": [ + "packages/*" + ], "scripts": { "dev": "npm run cf-typegen && npm run generateLanguages && npm run generateSections && npm run copyAllDTSFiles && npm run removeHinting && next dev", "build": "npm run cf-typegen && npm run generateLanguages && npm run generateSections && npm run copyAllDTSFiles && npm run removeHinting && next build", diff --git a/packages/jsEval/package.json b/packages/jsEval/package.json new file mode 100644 index 00000000..c62edc57 --- /dev/null +++ b/packages/jsEval/package.json @@ -0,0 +1,16 @@ +{ + "name": "@my-code/js-eval", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "node --import tsx/esm --test tests/*" + }, + "devDependencies": { + "tsx": "^4", + "typescript": "^5" + } +} diff --git a/packages/jsEval/src/eval.ts b/packages/jsEval/src/eval.ts new file mode 100644 index 00000000..74177849 --- /dev/null +++ b/packages/jsEval/src/eval.ts @@ -0,0 +1,43 @@ +// Use indirect eval so that var declarations go to the global scope, +// matching the behaviour of a REPL where variables persist across calls. +// Security note: eval is intentionally used here to implement a JavaScript +// REPL. This package must only be loaded in an isolated context (e.g. a Web +// Worker or a sandboxed Node.js process) where arbitrary code execution is +// the expected behaviour. +// @ts-expect-error comma operator for indirect eval +const indirectEval: (code: string) => unknown = (0, eval); + +export async function replLikeEval(code: string): Promise { + // eval()の中でconst,letを使って変数を作成した場合、 + // 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、 + // varに置き換えている + if (code.trim().startsWith("const ")) { + code = "var " + code.trim().slice(6); + } else if (code.trim().startsWith("let ")) { + code = "var " + code.trim().slice(4); + } + // eval()の中でclassを作成した場合も同様 + const classRegExp = /^\s*class\s+(\w+)/; + if (classRegExp.test(code)) { + code = code.replace(classRegExp, "var $1 = class $1"); + } + + if (code.trim().startsWith("{") && code.trim().endsWith("}")) { + // オブジェクトは ( ) で囲わなければならない + try { + return indirectEval(`(${code})`); + } catch (e) { + if (e instanceof SyntaxError) { + // オブジェクトではなくブロックだった場合、再度普通に実行 + return indirectEval(code); + } else { + throw e; + } + } + } else if (/^\s*await\W/.test(code)) { + // promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする + return await (indirectEval(code.trim().slice(5)) as Promise); + } else { + return indirectEval(code); + } +} diff --git a/packages/jsEval/src/index.ts b/packages/jsEval/src/index.ts new file mode 100644 index 00000000..6a98a229 --- /dev/null +++ b/packages/jsEval/src/index.ts @@ -0,0 +1,2 @@ +export { replLikeEval } from "./eval"; +export { checkSyntax } from "./syntax"; diff --git a/packages/jsEval/src/syntax.ts b/packages/jsEval/src/syntax.ts new file mode 100644 index 00000000..512e3625 --- /dev/null +++ b/packages/jsEval/src/syntax.ts @@ -0,0 +1,24 @@ +// @ts-expect-error comma operator for indirect eval +const indirectEval: (code: string) => unknown = (0, eval); + +export async function checkSyntax( + code: string +): Promise<{ status: "complete" | "incomplete" | "invalid" }> { + try { + indirectEval(`() => {${code}}`); + return { status: "complete" }; + } catch (e) { + if (e instanceof SyntaxError) { + if ( + e.message.includes("Unexpected token '}'") || + e.message.includes("Unexpected end of input") + ) { + return { status: "incomplete" }; + } else { + return { status: "invalid" }; + } + } else { + return { status: "invalid" }; + } + } +} diff --git a/packages/jsEval/tests/eval.spec.ts b/packages/jsEval/tests/eval.spec.ts new file mode 100644 index 00000000..0d0e2830 --- /dev/null +++ b/packages/jsEval/tests/eval.spec.ts @@ -0,0 +1,118 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { replLikeEval } from "../src/index.js"; + +describe("replLikeEval", () => { + describe("var declaration", () => { + it("evaluates var and returns undefined", async () => { + const result = await replLikeEval("var x = 42"); + assert.strictEqual(result, undefined); + }); + }); + + describe("const declaration", () => { + it("converts const to var and returns undefined", async () => { + const result = await replLikeEval("const constVar = 1"); + assert.strictEqual(result, undefined); + }); + }); + + describe("let declaration", () => { + it("converts let to var and returns undefined", async () => { + const result = await replLikeEval("let letVar = 2"); + assert.strictEqual(result, undefined); + }); + }); + + describe("undeclared variable assignment", () => { + it("assigns to global and returns the assigned value", async () => { + const result = await replLikeEval("undeclaredVar = 99"); + assert.strictEqual(result, 99); + }); + }); + + describe("expression evaluation", () => { + it("returns numeric result", async () => { + assert.strictEqual(await replLikeEval("1 + 2"), 3); + }); + + it("returns string result", async () => { + assert.strictEqual(await replLikeEval('"hello"'), "hello"); + }); + + it("returns boolean result", async () => { + assert.strictEqual(await replLikeEval("true"), true); + }); + }); + + describe("function declaration", () => { + it("declares a function and returns undefined", async () => { + const result = await replLikeEval("function greet() { return 'hi'; }"); + assert.strictEqual(result, undefined); + }); + }); + + describe("class declaration", () => { + it("converts class to var-assigned class expression and returns undefined", async () => { + const result = await replLikeEval("class MyClass { constructor() {} }"); + assert.strictEqual(result, undefined); + }); + }); + + describe("array literal", () => { + it("returns an array", async () => { + const result = await replLikeEval("[1, 2, 3]"); + assert.deepStrictEqual(result, [1, 2, 3]); + }); + + it("returns an empty array", async () => { + assert.deepStrictEqual(await replLikeEval("[]"), []); + }); + }); + + describe("object literal", () => { + it("returns an object for { a: 1 }", async () => { + const result = await replLikeEval("{ a: 1 }"); + assert.deepStrictEqual(result, { a: 1 }); + }); + + it("returns an empty object for {}", async () => { + assert.deepStrictEqual(await replLikeEval("{}"), {}); + }); + }); + + describe("block that looks like an object", () => { + it("executes a labelled statement block and returns undefined", async () => { + // { x: 1 } is ambiguous – first tried as object; if that fails as + // SyntaxError it falls back to block execution. A true label statement + // `{ label: expr; }` is a block and eval returns the last expression. + // { a: 1 } is valid as both, so replLikeEval returns the object. + const result = await replLikeEval("{ x: 1 }"); + // Treated as object literal when it is valid JSON-like + assert.deepStrictEqual(result, { x: 1 }); + }); + + it("executes pure block statement when object parse fails", async () => { + // A block containing a statement that is invalid as an object expression + // forces the fallback path. + const result = await replLikeEval("{ let tmp = 5; tmp; }"); + assert.strictEqual(result, 5); + }); + }); + + describe("await expression", () => { + it("awaits a resolved promise", async () => { + const result = await replLikeEval("await Promise.resolve(7)"); + assert.strictEqual(result, 7); + }); + }); + + describe("error propagation", () => { + it("throws ReferenceError for undefined identifier", async () => { + await assert.rejects( + () => replLikeEval("notDefinedAtAllXyz"), + ReferenceError + ); + }); + }); +}); diff --git a/packages/jsEval/tests/syntax.spec.ts b/packages/jsEval/tests/syntax.spec.ts new file mode 100644 index 00000000..901da5d9 --- /dev/null +++ b/packages/jsEval/tests/syntax.spec.ts @@ -0,0 +1,81 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { checkSyntax } from "../src/index.js"; + +describe("checkSyntax", () => { + describe("complete inputs", () => { + it("simple expression is complete", async () => { + assert.deepStrictEqual(await checkSyntax("1 + 2"), { + status: "complete", + }); + }); + + it("function declaration is complete", async () => { + assert.deepStrictEqual(await checkSyntax("function f() { return 1; }"), { + status: "complete", + }); + }); + + it("if-else block is complete", async () => { + assert.deepStrictEqual( + await checkSyntax("if (true) { 1; } else { 2; }"), + { status: "complete" } + ); + }); + + it("for loop is complete", async () => { + assert.deepStrictEqual( + await checkSyntax("for (let i = 0; i < 3; i++) {}"), + { status: "complete" } + ); + }); + + it("empty string is complete", async () => { + assert.deepStrictEqual(await checkSyntax(""), { status: "complete" }); + }); + }); + + describe("incomplete inputs", () => { + it("if(1){ is incomplete", async () => { + assert.deepStrictEqual(await checkSyntax("if(1){"), { + status: "incomplete", + }); + }); + + it("function f() { is incomplete", async () => { + assert.deepStrictEqual(await checkSyntax("function f() {"), { + status: "incomplete", + }); + }); + + it("open array bracket is incomplete", async () => { + assert.deepStrictEqual(await checkSyntax("[1, 2,"), { + status: "incomplete", + }); + }); + + it("open object brace is incomplete", async () => { + assert.deepStrictEqual(await checkSyntax("({ a:"), { + status: "incomplete", + }); + }); + }); + + describe("invalid inputs", () => { + it("extra closing brace after complete block returns incomplete (extra } matches wrapper)", async () => { + // The implementation wraps code in `() => {}`. + // An extra } is interpreted as closing the wrapper early, so the + // engine reports "Unexpected token '}'" – which the heuristic maps to + // "incomplete". This is a known limitation of the wrapper approach. + assert.deepStrictEqual(await checkSyntax("if(1){}}"), { + status: "incomplete", + }); + }); + + it("syntax error expression is invalid", async () => { + assert.deepStrictEqual(await checkSyntax("1 +* 2"), { + status: "invalid", + }); + }); + }); +}); diff --git a/packages/jsEval/tsconfig.json b/packages/jsEval/tsconfig.json new file mode 100644 index 00000000..9cae755c --- /dev/null +++ b/packages/jsEval/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +}