From 57e89f2e129f8455fe0968aec58b93ee00dbba00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:18:17 +0000 Subject: [PATCH 1/3] Initial plan From 8af7d3b51eb8caaa362a795b5968bdee7efe136a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:24:47 +0000 Subject: [PATCH 2/3] Extract replLikeEval/checkSyntax into packages/jsEval workspace with tests Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- .github/workflows/node.js.yml | 14 ++ app/terminal/worker/jsEval.worker.ts | 62 +------- package-lock.json | 16 +++ package.json | 3 + packages/jsEval/package.json | 16 +++ packages/jsEval/src/index.test.ts | 205 +++++++++++++++++++++++++++ packages/jsEval/src/index.ts | 65 +++++++++ packages/jsEval/tsconfig.json | 11 ++ 8 files changed, 331 insertions(+), 61 deletions(-) create mode 100644 packages/jsEval/package.json create mode 100644 packages/jsEval/src/index.test.ts create mode 100644 packages/jsEval/src/index.ts create mode 100644 packages/jsEval/tsconfig.json 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..74830297 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", @@ -3477,6 +3480,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", @@ -8059,6 +8066,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -15613,6 +15621,14 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/jsEval": { + "name": "@my-code/js-eval", + "version": "0.1.0", + "devDependencies": { + "tsx": "*", + "typescript": "*" + } } } } 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..f4048e78 --- /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 src/index.test.ts" + }, + "devDependencies": { + "tsx": "*", + "typescript": "*" + } +} diff --git a/packages/jsEval/src/index.test.ts b/packages/jsEval/src/index.test.ts new file mode 100644 index 00000000..233a085a --- /dev/null +++ b/packages/jsEval/src/index.test.ts @@ -0,0 +1,205 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { replLikeEval, checkSyntax } from "./index.ts"; + +// --------------------------------------------------------------------------- +// replLikeEval +// --------------------------------------------------------------------------- + +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 + ); + }); + }); +}); + +// --------------------------------------------------------------------------- +// checkSyntax +// --------------------------------------------------------------------------- + +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/src/index.ts b/packages/jsEval/src/index.ts new file mode 100644 index 00000000..fe062460 --- /dev/null +++ b/packages/jsEval/src/index.ts @@ -0,0 +1,65 @@ +// 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. +// eslint-disable-next-line no-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); + } +} + +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/tsconfig.json b/packages/jsEval/tsconfig.json new file mode 100644 index 00000000..437cdfe8 --- /dev/null +++ b/packages/jsEval/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"] +} From 459a5e552d83026bac7f98afcfdf63f3d12e8330 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:49:49 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 39 ++------ packages/jsEval/package.json | 6 +- packages/jsEval/src/eval.ts | 43 +++++++++ packages/jsEval/src/index.ts | 67 +------------- packages/jsEval/src/syntax.ts | 24 +++++ .../{src/index.test.ts => tests/eval.spec.ts} | 89 +------------------ packages/jsEval/tests/syntax.spec.ts | 81 +++++++++++++++++ packages/jsEval/tsconfig.json | 5 +- 8 files changed, 163 insertions(+), 191 deletions(-) create mode 100644 packages/jsEval/src/eval.ts create mode 100644 packages/jsEval/src/syntax.ts rename packages/jsEval/{src/index.test.ts => tests/eval.spec.ts} (58%) create mode 100644 packages/jsEval/tests/syntax.spec.ts diff --git a/package-lock.json b/package-lock.json index 74830297..53b95448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1500,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" @@ -1530,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", @@ -1735,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" }, @@ -3646,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" }, @@ -5297,7 +5292,6 @@ "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -5315,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" } @@ -5391,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", @@ -5939,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", @@ -5978,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" }, @@ -6462,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", @@ -7811,7 +7800,6 @@ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7892,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", @@ -8066,7 +8053,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10078,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" } @@ -10205,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" } @@ -10959,7 +10943,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -11550,8 +11533,7 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -11944,7 +11926,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -11986,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", @@ -12511,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", @@ -14178,7 +14157,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14402,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" @@ -14450,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" } @@ -14460,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", @@ -14807,7 +14782,6 @@ "integrity": "sha512-EhLJGptSGFi8AEErLiamO3PoGpbRqL+v4Ve36H2B38VxmDgFOSmDhfepBnA14sCQzGf1AEaoZX2DCwZsmO74yQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -14833,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", @@ -15626,8 +15599,8 @@ "name": "@my-code/js-eval", "version": "0.1.0", "devDependencies": { - "tsx": "*", - "typescript": "*" + "tsx": "^4", + "typescript": "^5" } } } diff --git a/packages/jsEval/package.json b/packages/jsEval/package.json index f4048e78..c62edc57 100644 --- a/packages/jsEval/package.json +++ b/packages/jsEval/package.json @@ -7,10 +7,10 @@ ".": "./src/index.ts" }, "scripts": { - "test": "node --import tsx/esm --test src/index.test.ts" + "test": "node --import tsx/esm --test tests/*" }, "devDependencies": { - "tsx": "*", - "typescript": "*" + "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 index fe062460..6a98a229 100644 --- a/packages/jsEval/src/index.ts +++ b/packages/jsEval/src/index.ts @@ -1,65 +1,2 @@ -// 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. -// eslint-disable-next-line no-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); - } -} - -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" }; - } - } -} +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/src/index.test.ts b/packages/jsEval/tests/eval.spec.ts similarity index 58% rename from packages/jsEval/src/index.test.ts rename to packages/jsEval/tests/eval.spec.ts index 233a085a..0d0e2830 100644 --- a/packages/jsEval/src/index.test.ts +++ b/packages/jsEval/tests/eval.spec.ts @@ -1,10 +1,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { replLikeEval, checkSyntax } from "./index.ts"; - -// --------------------------------------------------------------------------- -// replLikeEval -// --------------------------------------------------------------------------- +import { replLikeEval } from "../src/index.js"; describe("replLikeEval", () => { describe("var declaration", () => { @@ -120,86 +116,3 @@ describe("replLikeEval", () => { }); }); }); - -// --------------------------------------------------------------------------- -// checkSyntax -// --------------------------------------------------------------------------- - -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/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 index 437cdfe8..9cae755c 100644 --- a/packages/jsEval/tsconfig.json +++ b/packages/jsEval/tsconfig.json @@ -2,10 +2,11 @@ "compilerOptions": { "target": "ES2022", "module": "NodeNext", - "moduleResolution": "NodeNext", + "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "noEmit": true }, "include": ["src"] }