mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-21 08:07:34 +00:00
Compare commits
105
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7d831d21 | ||
|
|
1233a112d2 | ||
|
|
cbdd1f66da | ||
|
|
788eb0fa29 | ||
|
|
58fcad77a8 | ||
|
|
c555559ac1 | ||
|
|
e3a3fa7108 | ||
|
|
6238af397e | ||
|
|
7ffd75faf6 | ||
|
|
1f8ab95695 | ||
|
|
62dc1f7696 | ||
|
|
66f10ab7bf | ||
|
|
1ca8f63a79 | ||
|
|
03bcdd580d | ||
|
|
cc502f7e5f | ||
|
|
64ce8771c0 | ||
|
|
702a73d91a | ||
|
|
991b727eb8 | ||
|
|
ba342ce227 | ||
|
|
1464545665 | ||
|
|
fecacc9e68 | ||
|
|
1f73b4806b | ||
|
|
da2ce02596 | ||
|
|
25f35dcfb8 | ||
|
|
717f81ce08 | ||
|
|
5fdfcc7a80 | ||
|
|
0530c8e512 | ||
|
|
1d8cf4564b | ||
|
|
7e88f6bb18 | ||
|
|
af592fb779 | ||
|
|
7fc3f68007 | ||
|
|
cdcbb0047e | ||
|
|
a1956a7522 | ||
|
|
55bc7fd403 | ||
|
|
3049b1e684 | ||
|
|
1f36a7aff8 | ||
|
|
eb0e26b974 | ||
|
|
6f2b0e7833 | ||
|
|
f153255942 | ||
|
|
fef2fad76f | ||
|
|
f30d06ea34 | ||
|
|
dfa44e94e8 | ||
|
|
b81e10a461 | ||
|
|
65c93b69ed | ||
|
|
b073b052d3 | ||
|
|
728b2b6052 | ||
|
|
cc3ce20ab4 | ||
|
|
4ad5001be2 | ||
|
|
14b3c2ea4b | ||
|
|
558bd54c9b | ||
|
|
7e70f7e1ab | ||
|
|
cb6d95b7ef | ||
|
|
7020944359 | ||
|
|
4b00dd2713 | ||
|
|
b447627f5f | ||
|
|
5848ee0d24 | ||
|
|
839aa25c3e | ||
|
|
c42f1c9232 | ||
|
|
417f6d234d | ||
|
|
45a13af0ee | ||
|
|
e50d845451 | ||
|
|
56816621f8 | ||
|
|
7125f5f8b5 | ||
|
|
01a6ed8d97 | ||
|
|
004583d598 | ||
|
|
cbd911368e | ||
|
|
8bfb247854 | ||
|
|
dd71cdbd84 | ||
|
|
1c90e8f01c | ||
|
|
9843d13c29 | ||
|
|
871db0025e | ||
|
|
7b7a67080e | ||
|
|
3361953fe1 | ||
|
|
e9201ce933 | ||
|
|
b38d72e174 | ||
|
|
e1e2efe8ec | ||
|
|
c2b171d7c4 | ||
|
|
c3bf8864b9 | ||
|
|
3693f56374 | ||
|
|
971f4e558e | ||
|
|
dd95e3f2ed | ||
|
|
dfde4628e3 | ||
|
|
e755fce13d | ||
|
|
5b565e8923 | ||
|
|
ec8d95f1f2 | ||
|
|
ea52d18ec5 | ||
|
|
0f9504d6c5 | ||
|
|
f6353a48b5 | ||
|
|
4facc03371 | ||
|
|
485a027c1a | ||
|
|
988a66ca4f | ||
|
|
98c6b41573 | ||
|
|
4ee9bf0213 | ||
|
|
681338c533 | ||
|
|
5464aa4a74 | ||
|
|
36083c9520 | ||
|
|
168aba6183 | ||
|
|
ec19dc09c7 | ||
|
|
fca4701688 | ||
|
|
c720ca20ed | ||
|
|
dae1e8abbd | ||
|
|
ffe4d1522c | ||
|
|
b1860465cd | ||
|
|
cab11795e5 | ||
|
|
810e79a6d4 |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -87,7 +87,6 @@
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
||||
"luxon": "catalog:",
|
||||
"qr-scanner": "1.4.2",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
@@ -100,7 +99,6 @@
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"diff": "catalog:",
|
||||
@@ -113,7 +111,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -177,7 +175,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -203,11 +201,10 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
@@ -217,7 +214,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -253,7 +250,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -280,7 +277,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -297,7 +294,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -321,7 +318,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -341,7 +338,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -409,14 +406,12 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
"electron-log": "^5",
|
||||
"electron-store": "11.0.2",
|
||||
"electron-updater": "6.8.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"lighthouse": "13.4.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -460,7 +455,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -497,7 +492,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -513,7 +508,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -532,7 +527,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -545,7 +540,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -559,7 +554,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -574,7 +569,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -613,7 +608,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -643,7 +638,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -658,7 +653,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -682,7 +677,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -703,7 +698,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -725,7 +720,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -760,7 +755,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -780,7 +775,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -814,7 +809,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -833,7 +828,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -879,7 +874,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -893,7 +888,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -928,7 +923,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -963,7 +958,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -996,7 +991,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1037,7 +1032,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
@@ -3343,8 +3338,6 @@
|
||||
|
||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
|
||||
|
||||
"am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="],
|
||||
@@ -3639,8 +3632,6 @@
|
||||
|
||||
"condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="],
|
||||
|
||||
"conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="],
|
||||
|
||||
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
||||
|
||||
"configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="],
|
||||
@@ -3779,8 +3770,6 @@
|
||||
|
||||
"db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="],
|
||||
|
||||
"debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
@@ -3907,16 +3896,12 @@
|
||||
|
||||
"electron-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="],
|
||||
|
||||
"electron-store": ["electron-store@11.0.2", "", { "dependencies": { "conf": "^15.0.2", "type-fest": "^5.0.1" } }, "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="],
|
||||
|
||||
"electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="],
|
||||
|
||||
"electron-vite": ["electron-vite@6.0.0-beta.1", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^7.0.0", "esbuild": "^0.25.11", "magic-string": "^0.30.21", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-jltST77AwNxIeTTDtYhnIEA8ZM0RW9jmSJcqS8x/dQcgeeLlxlcJoYNNJ1tv7/Swor0AbXMYteBGdezoAOt+Nw=="],
|
||||
|
||||
"electron-window-state": ["electron-window-state@5.0.3", "", { "dependencies": { "jsonfile": "^4.0.0", "mkdirp": "^0.5.1" } }, "sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg=="],
|
||||
|
||||
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
|
||||
|
||||
"emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
|
||||
@@ -4461,8 +4446,6 @@
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
@@ -4473,7 +4456,7 @@
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
|
||||
|
||||
@@ -4735,8 +4718,6 @@
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
@@ -5491,8 +5472,6 @@
|
||||
|
||||
"svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
@@ -5617,8 +5596,6 @@
|
||||
|
||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
|
||||
@@ -6445,10 +6422,6 @@
|
||||
|
||||
"condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
|
||||
|
||||
"conf/dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="],
|
||||
|
||||
"conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"configstore/dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
|
||||
@@ -6489,8 +6462,6 @@
|
||||
|
||||
"electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||
|
||||
"electron-store/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="],
|
||||
|
||||
"electron-updater/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
||||
|
||||
"electron-updater/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
@@ -6521,8 +6492,6 @@
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
|
||||
@@ -6925,18 +6894,12 @@
|
||||
|
||||
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
||||
|
||||
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
"@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
|
||||
"@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
|
||||
|
||||
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
|
||||
@@ -7013,8 +6976,6 @@
|
||||
|
||||
"@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
|
||||
|
||||
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="],
|
||||
@@ -7393,8 +7354,6 @@
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"conf/dot-prop/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
@@ -7419,6 +7378,8 @@
|
||||
|
||||
"electron-updater/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
"electron/@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
@@ -7567,8 +7528,6 @@
|
||||
|
||||
"tw-to-css/tailwindcss/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="],
|
||||
@@ -7577,8 +7536,6 @@
|
||||
|
||||
"vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"workbox-build/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-TlCPtuIvTNF01fLI0yHCLzVkx53yF9xxvAObDMGeo8M=",
|
||||
"aarch64-linux": "sha256-aEjyPIXHeA5w1iZXbPKJAOIJQBfXg+LAx/y7C5uFz4g=",
|
||||
"aarch64-darwin": "sha256-lthMTio2qNy+H2tL9cAPmqC41O+wdSADI61cnW4dIb0=",
|
||||
"x86_64-darwin": "sha256-ONFbRQnwzOYHo0Pyh327QfJG2uzqzNEn/SS2vbYYMdM="
|
||||
"x86_64-linux": "sha256-8mOzCscBAuogG4tm8CroqjTX5E5yzCvTobbhKvxQP0U=",
|
||||
"aarch64-linux": "sha256-drrRSpzxC8bfaTXBpOyaN0QAyBVV7WJzm0NstJ+8sAE=",
|
||||
"aarch64-darwin": "sha256-YY5A/zxLPONvgnI+DZlzcD2K5Q9PIw4F2YbDxKbT4UU=",
|
||||
"x86_64-darwin": "sha256-/c/Ew4onA+9+l6GRKXz3zWq3QFESM7j5RyGLCWRPQFw="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, and `LLM.generateObject`. Use `LLMRequest.update(...)` when deriving canonical request data; do not add a duplicate `LLM.updateRequest(...)` path. Two ways to construct the same thing is one too many.
|
||||
|
||||
- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
|
||||
- Prefer forward compatibility for provider-defined options that OpenCode only passes through. For pass-through string enums, expose known values for autocomplete while accepting future values with `Known | (string & {})`, and accept any string at runtime. Closed literals are appropriate when OpenCode branches on a value, transforms its associated structure, or otherwise cannot correctly handle an unknown variant. New options whose shape or behavior requires implementation remain unsupported until they are handled; do not blindly forward unknown structures.
|
||||
- Order reasoning-effort values from lowest to highest: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Provider-specific subsets follow the same relative order in types, schemas, option lists, and tests.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -83,7 +83,7 @@ const AnthropicCacheControl = Schema.Struct({
|
||||
type: Schema.tag("ephemeral"),
|
||||
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
|
||||
})
|
||||
const AnthropicServiceTier = Schema.Literals(["auto", "standard_only"])
|
||||
const AnthropicServiceTier = knownString<"auto" | "standard_only">()
|
||||
|
||||
const AnthropicTextBlock = Schema.Struct({
|
||||
type: Schema.tag("text"),
|
||||
@@ -276,7 +276,7 @@ const AnthropicThinkingBlockBinding = Schema.Struct({
|
||||
})
|
||||
|
||||
const AnthropicThinkingFields = {
|
||||
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
|
||||
display: Schema.optional(knownString<"summarized" | "omitted">()),
|
||||
block_binding: Schema.optional(AnthropicThinkingBlockBinding),
|
||||
}
|
||||
const AnthropicThinkingEnabled = Schema.Struct({
|
||||
|
||||
@@ -73,11 +73,6 @@ const driver = (options: Options, body: string): WebSocketChannelDriver => {
|
||||
)
|
||||
if (event.type === "error") {
|
||||
terminal = true
|
||||
yield* OpenResponses.decodeKnownErrorEvent(event).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame, cause),
|
||||
),
|
||||
)
|
||||
return {
|
||||
type: "provider-failure",
|
||||
error: OpenResponses.providerFailure(event, `${options.name} stream error`, frame),
|
||||
|
||||
@@ -108,7 +108,7 @@ const incremental = (
|
||||
return input.slice(baseline.length)
|
||||
}
|
||||
|
||||
const code = (event: OpenResponses.Event) => event.code || event.error?.code || event.response?.error?.code || undefined
|
||||
const code = (event: OpenResponses.Event) => OpenResponses.errorDetail(event).code
|
||||
|
||||
const rejected = (
|
||||
observation: Extract<ChannelObservation, { readonly type: "provider-failure" }>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Option, Schema, SchemaGetter } from "effect"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import type { Content } from "@opencode/schema/tool"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
@@ -333,53 +333,13 @@ export const StreamItem = Schema.StructWithRest(
|
||||
export type StreamItem = Schema.Schema.Type<typeof StreamItem>
|
||||
export type OutputItem = StreamItem & { readonly id: string }
|
||||
|
||||
// Responses-compatible providers put streaming error details at the top level or
|
||||
// under `error`, and response failures under `response.error`. Accept all three shapes.
|
||||
// Responses-compatible providers put error details at the top level, under `error`, or under
|
||||
// `response.error`, and gateways reshape them freely: strings, numeric codes, extra fields. Those
|
||||
// fields decode as opaque values and `errorDetail` reads them defensively, so an error frame can
|
||||
// only fail on invalid JSON and otherwise always classifies with the raw body as the fallback.
|
||||
// https://www.openresponses.org/specification
|
||||
const OpenResponsesErrorObject = Schema.Struct({
|
||||
type: optionalNull(Schema.String),
|
||||
code: optionalNull(Schema.String),
|
||||
message: optionalNull(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
})
|
||||
const OpenResponsesErrorPayload = Schema.Union([Schema.String, OpenResponsesErrorObject]).pipe(
|
||||
Schema.decodeTo(OpenResponsesErrorObject, {
|
||||
decode: SchemaGetter.transform((error) => (typeof error === "string" ? { message: error } : error)),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
type OpenResponsesErrorPayload = Schema.Schema.Type<typeof OpenResponsesErrorPayload>
|
||||
|
||||
const WebSocketErrorHeader = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
|
||||
export const WebSocketErrorEvent = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
type: Schema.tag("error"),
|
||||
status: Schema.optional(Schema.Number),
|
||||
status_code: Schema.optional(Schema.Number),
|
||||
code: optionalNull(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, WebSocketErrorHeader)),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const decodeWebSocketErrorEvent = Schema.decodeUnknownEffect(WebSocketErrorEvent)
|
||||
|
||||
export const decodeKnownErrorEvent = (event: Event) =>
|
||||
decodeWebSocketErrorEvent({
|
||||
...event,
|
||||
status: typeof event.status === "number" ? event.status : undefined,
|
||||
status_code: typeof event.status_code === "number" ? event.status_code : undefined,
|
||||
headers: ProviderShared.isRecord(event.headers)
|
||||
? Object.fromEntries(
|
||||
Object.entries(event.headers).filter(
|
||||
(entry): entry is [string, string | number | boolean] =>
|
||||
typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean",
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
})
|
||||
const asText = (value: unknown) =>
|
||||
typeof value === "string" && value.length > 0 ? value : typeof value === "number" ? String(value) : undefined
|
||||
|
||||
export const Event = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
@@ -400,31 +360,18 @@ export const Event = Schema.StructWithRest(
|
||||
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
|
||||
output: Schema.optional(Schema.Array(StreamItem)),
|
||||
usage: optionalNull(OpenResponsesUsage),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
),
|
||||
code: optionalNull(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
code: Schema.optional(Schema.Unknown),
|
||||
message: Schema.optional(Schema.Unknown),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
status: Schema.optional(Schema.Unknown),
|
||||
status_code: Schema.optional(Schema.Unknown),
|
||||
headers: Schema.optional(Schema.Unknown),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((event) => {
|
||||
if (event.type !== "error" || event.error != null) return event
|
||||
const { code, message, param, ...rest } = event
|
||||
if (code === undefined && message === undefined && param === undefined) return event
|
||||
// Flat errors (for example, Meta's) can also arrive through generic Responses endpoints.
|
||||
return { ...rest, error: { code, message, param } }
|
||||
}),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
|
||||
@@ -433,16 +380,15 @@ const decodeEventValue = Schema.decodeUnknownEffect(Event)
|
||||
const decodeFrame = Schema.decodeUnknownEffect(ProviderShared.Json)
|
||||
|
||||
/**
|
||||
* Decodes one WebSocket frame. xAI answers a rejected `response.create` with `{ "error": { "message", "type" } }` and no
|
||||
* event type; that envelope reads as an error event so the failure classifies instead of failing decoding.
|
||||
* Decodes one WebSocket frame. Some providers and gateways answer a rejected `response.create` with a bare
|
||||
* `{ "error": ... }` envelope and no event type; that reads as an error event so it classifies instead of
|
||||
* failing decoding.
|
||||
*/
|
||||
export const decodeChannelEvent = (frame: string) =>
|
||||
decodeFrame(frame).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeEventValue(
|
||||
ProviderShared.isRecord(value) &&
|
||||
value.type === undefined &&
|
||||
(typeof value.error === "string" || ProviderShared.isRecord(value.error))
|
||||
ProviderShared.isRecord(value) && value.type === undefined && value.error != null
|
||||
? { ...value, type: "error" }
|
||||
: value,
|
||||
),
|
||||
@@ -1422,22 +1368,21 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
|
||||
return [{ ...current, lifecycle }, events] satisfies StepResult
|
||||
})
|
||||
|
||||
// Build the prettiest summary available from whatever the provider supplied.
|
||||
// When both code and message are present, prefix the code so consumers see
|
||||
// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
|
||||
// the bare message — production rate limits and context-length failures used
|
||||
// to be indistinguishable from generic stream drops. Returns undefined when
|
||||
// the payload carries no usable summary.
|
||||
const providerErrorMessage = (event: Event, nested: OpenResponsesErrorPayload | undefined): string | undefined => {
|
||||
const message = event.message || nested?.message || undefined
|
||||
const code = event.code || nested?.code || undefined
|
||||
if (message && code) return `${code}: ${message}`
|
||||
return message || code
|
||||
/** Error code and message from wherever the frame put them; top-level fields win over nested ones. */
|
||||
export const errorDetail = (event: Event) => {
|
||||
const raw = event.error ?? event.response?.error
|
||||
const nested = typeof raw === "string" ? { message: raw } : ProviderShared.isRecord(raw) ? raw : undefined
|
||||
return {
|
||||
message: asText(event.message) ?? asText(nested?.message),
|
||||
code: asText(event.code) ?? asText(nested?.code),
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix the code when both are present (`rate_limit_exceeded: Slow down`) so the failure mode is
|
||||
// visible; fall back to the raw frame rather than a generic message when neither decodes.
|
||||
export const providerFailure = (event: Event, fallback: string, body = ProviderShared.encodeJson(event)) => {
|
||||
const nested = event.error ?? event.response?.error ?? undefined
|
||||
const summary = providerErrorMessage(event, nested)
|
||||
const detail = errorDetail(event)
|
||||
const summary = detail.message && detail.code ? `${detail.code}: ${detail.message}` : (detail.message ?? detail.code)
|
||||
const message = summary ?? (body === "{}" ? fallback : body)
|
||||
const status =
|
||||
typeof event.status === "number"
|
||||
@@ -1520,18 +1465,7 @@ export const step = (state: ParserState, event: NormalizedEvent) => {
|
||||
if (event.type === "response.output_item.done") return onOutputItemDone(state, event.item)
|
||||
if (event.type === "response.completed" || event.type === "response.incomplete") return onResponseFinish(state, event)
|
||||
if (event.type === "response.failed") return providerFailure(event, `${state.name} response failed`)
|
||||
if (event.type === "error")
|
||||
return decodeKnownErrorEvent(event).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
state.id,
|
||||
`${state.name} returned a malformed error event`,
|
||||
ProviderShared.encodeJson(event),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
Effect.flatMap(() => providerFailure(event, `${state.name} stream error`)),
|
||||
)
|
||||
if (event.type === "error") return providerFailure(event, `${state.name} stream error`)
|
||||
return Effect.succeed<StepResult>([state, NO_EVENTS])
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,9 @@ export const fromWebSocket = (
|
||||
): Effect.Effect<WebSocketConnection, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
yield* waitOpen(ws, input)
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, AIError | Cause.Done<void>>(128)
|
||||
// The socket pushes frames synchronously and cannot be paused, so the hand-off to the consumer
|
||||
// fiber must absorb whole read buffers. Bun delivers over a thousand small frames in one tick.
|
||||
const messages = yield* Queue.unbounded<string | Uint8Array, AIError | Cause.Done<void>>()
|
||||
|
||||
const oversized = (message: string | Uint8Array) =>
|
||||
typeof message === "string" ? new Blob([message]).size > MAX_FRAME_BYTES : message.byteLength > MAX_FRAME_BYTES
|
||||
@@ -238,19 +240,7 @@ export const fromWebSocket = (
|
||||
}
|
||||
const offer = (message: string | Uint8Array) => {
|
||||
if (rejectOversized(message)) return
|
||||
if (Queue.offerUnsafe(messages, message)) return
|
||||
Queue.failCauseUnsafe(
|
||||
messages,
|
||||
Cause.fail(
|
||||
transportError("WebSocket inbound queue overflow", {
|
||||
body: typeof message === "string" ? message : new TextDecoder().decode(message),
|
||||
url: input.url,
|
||||
operation: "read",
|
||||
code: "queue-overflow",
|
||||
phase: "receive",
|
||||
}),
|
||||
),
|
||||
)
|
||||
Queue.offerUnsafe(messages, message)
|
||||
}
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
|
||||
@@ -4,6 +4,22 @@ import { Anthropic } from "../../src/providers.js"
|
||||
const model = Anthropic.provider.model("claude-sonnet-4-5")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { thinking: { type: "adaptive" } } })
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
providerOptions: {
|
||||
serviceTier: "future-tier",
|
||||
thinking: { type: "adaptive", display: "future-display" },
|
||||
},
|
||||
})
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
providerOptions: {
|
||||
// @ts-expect-error Anthropic cache TTL values are protocol constraints.
|
||||
cacheControl: { type: "ephemeral", ttl: "future-ttl" },
|
||||
},
|
||||
})
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
|
||||
@@ -215,13 +215,31 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("forwards unknown values for pass-through string enums", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: {
|
||||
service_tier: "future-tier",
|
||||
thinking: { type: "adaptive", display: "future-display" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
service_tier: "future-tier",
|
||||
thinking: { type: "adaptive", display: "future-display" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores unknown provider options and rejects malformed known ones", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(LLMRequest.update(request, { providerOptions: { unknownOption: true } }))
|
||||
const malformed = [
|
||||
{ service_tier: "fast" },
|
||||
{ service_tier: 1 },
|
||||
{ metadata: { user_id: 42 } },
|
||||
{ cache_control: { type: "ephemeral", ttl: "2h" } },
|
||||
{ cache_control: { type: "ephemeral", ttl: "future-ttl" } },
|
||||
{ output_config: { format: { type: "text" } } },
|
||||
{ thinking: { type: "automatic" } },
|
||||
]
|
||||
|
||||
@@ -11,66 +11,78 @@ import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
|
||||
|
||||
it.effect("normalizes flat errors in shared SSE and WebSocket decoding", () =>
|
||||
it.effect("decodes error frames verbatim in shared SSE and WebSocket decoding", () =>
|
||||
Effect.gen(function* () {
|
||||
const frame = {
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
code: "server_shutting_down",
|
||||
message: "Server is shutting down. Please retry your request.",
|
||||
param: null,
|
||||
}
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
const event = yield* decode(JSON.stringify(frame))
|
||||
expect(event).toEqual({
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
error: { code: frame.code, message: frame.message, param: null },
|
||||
})
|
||||
|
||||
for (const unchanged of [
|
||||
event,
|
||||
for (const frame of [
|
||||
{ type: "error", sequence_number: 4, code: "server_shutting_down", message: "Shutting down", param: null },
|
||||
{ type: "error" },
|
||||
{ type: "error", error: "Gateway failed" },
|
||||
{ type: "error", error: { code: 429, message: "slow down" } },
|
||||
{ type: "error", error: 42 },
|
||||
{ type: "error", code: 500, message: ["not", "a", "string"] },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: ["weird"] } },
|
||||
{
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { code: "server_error", message: "Internal server error" } },
|
||||
},
|
||||
{ type: "response.output_text.delta", item_id: "msg_text", delta: "Hello" },
|
||||
]) {
|
||||
expect(yield* decode(JSON.stringify(unchanged))).toEqual(unchanged)
|
||||
expect(yield* decode(JSON.stringify(frame))).toEqual(frame)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("continues to normalize untyped xAI WebSocket errors", () =>
|
||||
it.effect("reads bare WebSocket error envelopes as error events", () =>
|
||||
Effect.gen(function* () {
|
||||
const message = "gRPC error: Response with id=resp_missing not found"
|
||||
for (const error of [{ type: "api_error", message }, message]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify({ error }))).toEqual({
|
||||
type: "error",
|
||||
error: typeof error === "string" ? { message } : error,
|
||||
})
|
||||
for (const error of [{ type: "api_error", message }, message, 42]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify({ error }))).toEqual({ type: "error", error })
|
||||
}
|
||||
for (const frame of [{ error: null }, { message }]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame)).pipe(Effect.flip)).toBeDefined()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("normalizes string errors in shared SSE and WebSocket decoding", () =>
|
||||
it.effect("extracts error details from every shape and falls back to the raw frame", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
expect(yield* decode(JSON.stringify({ type: "error", error: "Gateway failed" }))).toEqual({
|
||||
type: "error",
|
||||
error: { message: "Gateway failed" },
|
||||
})
|
||||
expect(
|
||||
yield* decode(
|
||||
JSON.stringify({ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } }),
|
||||
),
|
||||
).toEqual({
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { message: "Gateway failed" } },
|
||||
})
|
||||
const cases: Array<[frame: Record<string, unknown>, message: string, tag: string]> = [
|
||||
[
|
||||
{ type: "error", code: "server_shutting_down", message: "Shutting down" },
|
||||
"server_shutting_down: Shutting down",
|
||||
"UnknownProvider",
|
||||
],
|
||||
[{ type: "error", error: "Gateway failed" }, "Gateway failed", "UnknownProvider"],
|
||||
[{ type: "error", error: { code: 429, message: "slow down" } }, "429: slow down", "UnknownProvider"],
|
||||
[{ type: "error", error: { message: "slow down" }, status: 429 }, "slow down", "RateLimit"],
|
||||
[{ type: "error", code: 500, message: ["not", "a", "string"] }, "500", "UnknownProvider"],
|
||||
[
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } },
|
||||
"Gateway failed",
|
||||
"UnknownProvider",
|
||||
],
|
||||
]
|
||||
for (const [frame, message, tag] of cases) {
|
||||
const event = yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))
|
||||
const error = OpenResponses.providerFailure(event, "fallback", JSON.stringify(frame))
|
||||
expect(error.message).toBe(message)
|
||||
expect(error.reason._tag).toBe(tag)
|
||||
expect(error.reason.body).toBe(JSON.stringify(frame))
|
||||
}
|
||||
for (const frame of [
|
||||
{ type: "error", error: 42 },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: ["weird"] } },
|
||||
]) {
|
||||
const event = yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))
|
||||
const error = OpenResponses.providerFailure(event, "fallback", JSON.stringify(frame))
|
||||
expect(error.message).toBe(JSON.stringify(frame))
|
||||
expect(error.reason._tag).toBe("UnknownProvider")
|
||||
}
|
||||
expect(OpenResponses.providerFailure({ type: "error" }, "fallback", "{}").message).toBe("fallback")
|
||||
expect(OpenResponses.providerFailure({ type: "error" }, "fallback", "{}").reason._tag).toBe("ProviderInternal")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ export function mountBrowserPane() {
|
||||
session: "Alpha",
|
||||
mounted: true,
|
||||
visible: true,
|
||||
url: undefined as string | undefined,
|
||||
loading: false,
|
||||
generation: 0,
|
||||
delayNavigation: false,
|
||||
pendingURL: undefined as string | undefined,
|
||||
loadErrors: {} as Record<string, string | undefined>,
|
||||
error: undefined as string | undefined,
|
||||
layouts: {} as Record<string, BrowserPaneLayout | undefined>,
|
||||
})
|
||||
const tabs = ["Alpha", "Beta"].map((name) => ({
|
||||
@@ -47,13 +54,33 @@ export function mountBrowserPane() {
|
||||
opened: () => !!registrations.get(store.session),
|
||||
state: () => ({ tabs: tabs.filter((tab) => tab.title === store.session), focusedTabID: null }),
|
||||
tabs: () => tabs.filter((tab) => tab.title === store.session),
|
||||
active: () => tabs.find((tab) => tab.title === store.session) ?? tabs[0],
|
||||
active: () => {
|
||||
const tab = tabs.find((tab) => tab.title === store.session) ?? tabs[0]
|
||||
return {
|
||||
...tab,
|
||||
url: store.url ?? tab.url,
|
||||
loading: store.loading,
|
||||
generation: store.generation,
|
||||
loadError: store.loadErrors[store.session],
|
||||
}
|
||||
},
|
||||
registration: () => registrations.get(store.session),
|
||||
error: () => undefined,
|
||||
error: () => store.error ?? (store.loadErrors[store.session] ? "Request failed" : undefined),
|
||||
suspended: () => false,
|
||||
close: () => undefined,
|
||||
open: () => undefined,
|
||||
command: () => undefined,
|
||||
command: (command) => {
|
||||
setStore("error", undefined)
|
||||
if (command.type === "navigate" || command.type === "reload") setStore("loadErrors", store.session, undefined)
|
||||
if (command.type === "navigate") {
|
||||
if (store.delayNavigation) {
|
||||
setStore("pendingURL", command.url)
|
||||
return
|
||||
}
|
||||
setStore({ url: command.url, loading: false, generation: store.generation + 1 })
|
||||
}
|
||||
if (command.type === "stop") setStore("loading", false)
|
||||
},
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -64,6 +91,32 @@ export function mountBrowserPane() {
|
||||
{(name) => <button onClick={() => setStore({ session: name, mounted: name !== "Empty" })}>{name}</button>}
|
||||
</For>
|
||||
<button onClick={() => setStore("mounted", false)}>Unmount pane</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: false })}>Blank page</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: true })}>Loading page</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({ loading: true, generation: store.generation + 1, loadErrors: { [store.session]: undefined } })
|
||||
}
|
||||
>
|
||||
Load current page
|
||||
</button>
|
||||
<button onClick={() => setStore("loadErrors", store.session, "ERR_CONNECTION_REFUSED")}>Failed page</button>
|
||||
<button onClick={() => setStore("delayNavigation", true)}>Delay navigation</button>
|
||||
<button onClick={() => setStore({ error: "ERR_BLOCKED_BY_CLIENT", pendingURL: undefined })}>
|
||||
Block navigation
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({
|
||||
url: store.pendingURL,
|
||||
pendingURL: undefined,
|
||||
loading: false,
|
||||
generation: store.generation + 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Complete navigation
|
||||
</button>
|
||||
<button onClick={() => setStore("visible", (visible) => !visible)}>Toggle Review tab</button>
|
||||
</nav>
|
||||
<div style={{ width: "640px", height: "360px", border: "1px solid #555" }}>
|
||||
|
||||
@@ -57,3 +57,153 @@ story("hides the native view immediately while the pane stays mounted", async ({
|
||||
await toggle.click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText('Or prompt "Open in the app browser"', { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
story("keeps Stop available and hides the empty state while a blank page loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Loading page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await root.getByRole("button", { name: "Stop", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("keeps the submitted URL visible until the browser reports navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await address.fill("https://unsubmitted.example/")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("restores the current URL when a submitted navigation is blocked", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://blocked.example/")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByText("ERR_BLOCKED_BY_CLIENT", { exact: true })).toBeVisible()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Check the URL and your connection, then try again.", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Request failed", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("textbox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
"https://alpha.example/",
|
||||
)
|
||||
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Alpha", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
|
||||
await root.getByRole("button", { name: "Reload", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("returns a failed tab to the empty state when an empty URL is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("")
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("selects the full URL when the address field gains focus", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
await address.pressSequentially("https://example.com/")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await address.focus()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
await address.press("ArrowRight")
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
})
|
||||
|
||||
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("keeps the current page and restores its URL when an empty address is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
|
||||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
project: fixture.project,
|
||||
pageMessages,
|
||||
})
|
||||
await page.addInitScript(
|
||||
({ directory, server, sessions }) => {
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] } }),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify(sessions.map((session) => ({ type: "session", server, sessionId: session.id }))),
|
||||
)
|
||||
},
|
||||
{ directory: fixture.directory, server: fixture.serverKey, sessions: fixture.sessions },
|
||||
)
|
||||
await page.goto("/")
|
||||
await page.locator('[data-slot="mobile-tabs-trigger"]').click()
|
||||
await expect(page.locator('[data-slot="mobile-tabs-drawer"]')).toBeVisible()
|
||||
const drawer = page.locator('[data-slot="mobile-drawer-content"]')
|
||||
await expect
|
||||
.poll(() => drawer.evaluate((element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).m42))
|
||||
.toBe(0)
|
||||
await expect(drawer).not.toHaveAttribute("data-transitioning")
|
||||
})
|
||||
|
||||
test("reorders session tabs with touch", async ({ page }) => {
|
||||
const tabs = page.locator('[data-slot="vertical-tabs"] a')
|
||||
await expect(tabs).toContainText([fixture.expected.sourceTitle, fixture.expected.targetTitle])
|
||||
|
||||
const target = tabs.filter({ hasText: fixture.expected.targetTitle })
|
||||
const source = tabs.filter({ hasText: fixture.expected.sourceTitle })
|
||||
const targetBox = await target.boundingBox()
|
||||
const sourceBox = await source.boundingBox()
|
||||
expect(targetBox).not.toBeNull()
|
||||
expect(sourceBox).not.toBeNull()
|
||||
|
||||
await touchDrag(page, source, {
|
||||
from: { x: sourceBox!.x + sourceBox!.width / 2, y: sourceBox!.y + sourceBox!.height / 2 },
|
||||
to: { x: targetBox!.x + targetBox!.width / 2, y: targetBox!.y + targetBox!.height / 2 },
|
||||
})
|
||||
|
||||
await expect(tabs).toContainText([fixture.expected.targetTitle, fixture.expected.sourceTitle])
|
||||
})
|
||||
|
||||
test("dismisses a touch context menu by tapping outside", async ({ page }) => {
|
||||
const drawer = page.locator('[data-slot="mobile-tabs-drawer"]')
|
||||
const tab = drawer.locator('[data-slot="tab-link"]').filter({ hasText: fixture.expected.sourceTitle })
|
||||
const home = drawer.getByRole("button", { name: "Home", exact: true })
|
||||
const homeBox = await home.boundingBox()
|
||||
const box = await tab.boundingBox()
|
||||
expect(homeBox).not.toBeNull()
|
||||
expect(box).not.toBeNull()
|
||||
const point = { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 }
|
||||
|
||||
await tab.dispatchEvent("pointerdown", {
|
||||
pointerType: "touch",
|
||||
pointerId: 1,
|
||||
isPrimary: true,
|
||||
clientX: point.x,
|
||||
clientY: point.y,
|
||||
})
|
||||
const rename = page.getByRole("menuitem", { name: "Rename", exact: true })
|
||||
await expect(rename).toBeVisible()
|
||||
await expect(drawer).toBeVisible()
|
||||
await tab.filter({ visible: true }).dispatchEvent("pointerup", {
|
||||
pointerType: "touch",
|
||||
pointerId: 1,
|
||||
isPrimary: true,
|
||||
clientX: point.x,
|
||||
clientY: point.y,
|
||||
})
|
||||
|
||||
await page.touchscreen.tap(homeBox!.x + homeBox!.width / 2, homeBox!.y + homeBox!.height / 2)
|
||||
await expect(rename).toBeHidden()
|
||||
await expect(drawer).toBeVisible()
|
||||
})
|
||||
|
||||
async function touchDrag(
|
||||
page: Page,
|
||||
source: Locator,
|
||||
input: { from: { x: number; y: number }; to: { x: number; y: number } },
|
||||
) {
|
||||
const client = await page.context().newCDPSession(page)
|
||||
await client.send("Input.dispatchTouchEvent", {
|
||||
type: "touchStart",
|
||||
touchPoints: [{ ...input.from, id: 1 }],
|
||||
})
|
||||
await client.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ x: (input.from.x + input.to.x) / 2, y: (input.from.y + input.to.y) / 2, id: 1 }],
|
||||
})
|
||||
await expect(source).toHaveCount(2)
|
||||
await client.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ ...input.to, id: 1 }],
|
||||
})
|
||||
await client.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] })
|
||||
}
|
||||
@@ -166,6 +166,45 @@ test.describe("session timeline projection", () => {
|
||||
await expect(longNotice.locator("[title]")).toHaveAttribute("title", `Switched to ${longName}`)
|
||||
await expect.poll(() => longNotice.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true)
|
||||
})
|
||||
|
||||
test("does not repeat a review comment file as an attachment", async ({ page }) => {
|
||||
const message = userMessage([
|
||||
userText("what's goin on here", { id: "prt_user_review_comment" }),
|
||||
{
|
||||
id: "prt_user_review_file",
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename: "LiveRuntime.ts",
|
||||
url: "file:///repo/LiveRuntime.ts?start=14&end=32",
|
||||
},
|
||||
{
|
||||
id: "prt_user_unrelated_file",
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename: "notes.txt",
|
||||
url: "data:text/plain;base64,bm90ZXM=",
|
||||
},
|
||||
])
|
||||
message.metadata = {
|
||||
displayText: "what's goin on here",
|
||||
comments: [
|
||||
{
|
||||
path: "LiveRuntime.ts",
|
||||
comment: "what's goin on here",
|
||||
selection: { startLine: 14, startChar: 0, endLine: 32, endChar: 0 },
|
||||
origin: "review",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await setupTimeline(page, { messages: [message, assistantMessage()] })
|
||||
|
||||
const user = page.locator('[data-component="user-message"]')
|
||||
await expect(user.getByText("LiveRuntime.ts:14-32", { exact: true })).toBeVisible()
|
||||
const attachments = user.locator('[data-slot="user-message-attachments"]')
|
||||
await expect(attachments.getByText("LiveRuntime.ts", { exact: true })).toHaveCount(0)
|
||||
await expect(attachments.getByText("notes.txt", { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
function patchFile(file: string, status: "added" | "modified" | "deleted") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -46,7 +46,6 @@
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"diff": "catalog:",
|
||||
@@ -88,7 +87,6 @@
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
||||
"luxon": "catalog:",
|
||||
"qr-scanner": "1.4.2",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Prompt } from "@/composer/state"
|
||||
import { prependHistoryEntry, type PromptHistoryComment } from "./entry"
|
||||
import { prependHistoryEntry, removeHistoryEntry, type PromptHistoryComment } from "./entry"
|
||||
import { Schema } from "effect"
|
||||
import { PromptHistoryState } from "../schema"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
@@ -36,6 +36,24 @@ describe("Composer history", () => {
|
||||
expect(dedupedComments).toBe(commentsOnly)
|
||||
})
|
||||
|
||||
test("removeHistoryEntry drops the entry recorded for a prompt and leaves others alone", () => {
|
||||
const image: Prompt = [
|
||||
{ type: "text", content: "look", start: 0, end: 4 },
|
||||
{ type: "image", id: "img", filename: "big.png", mime: "image/png", blob: { id: "hash", url: "" } },
|
||||
]
|
||||
const entries = prependHistoryEntry(prependHistoryEntry([], text("earlier")), image, [comment("c1")])
|
||||
expect(entries).toHaveLength(2)
|
||||
|
||||
const untouched = removeHistoryEntry(entries, text("never sent"))
|
||||
expect(untouched).toBe(entries)
|
||||
|
||||
const withoutComments = removeHistoryEntry(entries, image)
|
||||
expect(withoutComments).toBe(entries)
|
||||
|
||||
const removed = removeHistoryEntry(entries, image, [comment("c1")])
|
||||
expect(removed).toEqual(prependHistoryEntry([], text("earlier")))
|
||||
})
|
||||
|
||||
test("insertion isolates canonical entries from source mutations", () => {
|
||||
const prompt: Prompt = [
|
||||
{
|
||||
|
||||
@@ -48,6 +48,18 @@ export function prependHistoryEntry(
|
||||
return [entry, ...entries].slice(0, max)
|
||||
}
|
||||
|
||||
// A send that failed puts its prompt back in the composer, so the entry recorded for it would
|
||||
// only duplicate the draft and keep its attachments referenced for as long as history holds it.
|
||||
export function removeHistoryEntry(
|
||||
entries: PromptHistoryStoredEntry[],
|
||||
prompt: Prompt,
|
||||
comments: PromptHistoryComment[] = [],
|
||||
) {
|
||||
const entry = { prompt, comments } satisfies PromptHistoryEntry
|
||||
const next = entries.filter((item) => !isPromptEqual(item, entry))
|
||||
return next.length === entries.length ? entries : next
|
||||
}
|
||||
|
||||
function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) {
|
||||
return (
|
||||
commentA.path === commentB.path &&
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Persist, persisted } from "@/runtime/persistence/storage"
|
||||
import {
|
||||
clonePromptHistoryComments,
|
||||
prependHistoryEntry,
|
||||
removeHistoryEntry,
|
||||
type PromptHistoryComment,
|
||||
type PromptHistoryStoredEntry,
|
||||
} from "./entry"
|
||||
@@ -13,6 +14,7 @@ import { PromptHistoryState } from "../schema"
|
||||
export type ComposerHistoryStore = {
|
||||
entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[]
|
||||
add: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void
|
||||
remove: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void
|
||||
}
|
||||
|
||||
type PromptHistoryState = typeof PromptHistoryState.Type
|
||||
@@ -32,6 +34,13 @@ function createComposerHistoryStore(
|
||||
if (next === current.entries) return
|
||||
setCurrent("entries", next)
|
||||
},
|
||||
remove(prompt, mode, comments) {
|
||||
const current = mode === "shell" ? shell : normal
|
||||
const setCurrent = mode === "shell" ? setShell : setNormal
|
||||
const next = removeHistoryEntry(current.entries, prompt, comments)
|
||||
if (next === current.entries) return
|
||||
setCurrent("entries", next)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,5 +65,12 @@ export function createComposerHistory() {
|
||||
const metadata = clonePromptHistoryComments(comments)
|
||||
void ready.then(() => history.add(saved, mode, metadata))
|
||||
},
|
||||
remove(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) {
|
||||
const ready = mode === "shell" ? shellInit : normalInit
|
||||
if (!(ready instanceof Promise)) return history.remove(prompt, mode, comments)
|
||||
const saved = clonePrompt(prompt)
|
||||
const metadata = clonePromptHistoryComments(comments)
|
||||
void ready.then(() => history.remove(saved, mode, metadata))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
editor: () => editor,
|
||||
queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })),
|
||||
addToHistory: (value, mode) => controller.addHistory(value, mode),
|
||||
removeFromHistory: (value, mode, comments) => history.remove(value, mode, mode === "shell" ? [] : comments),
|
||||
resetHistory: () => controller.resetHistory(),
|
||||
setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }),
|
||||
closePopover: () => controller.dispatch({ type: "popover.close" }),
|
||||
|
||||
@@ -53,6 +53,7 @@ function submitInput(
|
||||
notify = { missingSelection() {}, failed(_kind: "shell" | "command" | "prompt", _error: unknown) {} },
|
||||
mode: "normal" | "shell" = "normal",
|
||||
commands: () => readonly { name: string }[] | undefined = () => [],
|
||||
history: string[] = [],
|
||||
) {
|
||||
return createComposerSubmit({
|
||||
adapter,
|
||||
@@ -60,7 +61,9 @@ function submitInput(
|
||||
commands,
|
||||
editor: () => undefined,
|
||||
queueScroll() {},
|
||||
addToHistory() {},
|
||||
addToHistory: (prompt) => history.push(`add:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
|
||||
removeFromHistory: (prompt) =>
|
||||
history.push(`remove:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
|
||||
resetHistory() {},
|
||||
setMode() {},
|
||||
closePopover() {},
|
||||
@@ -533,12 +536,8 @@ describe("Composer submission", () => {
|
||||
missingSelection() {},
|
||||
failed: () => (attempts.length === 2 ? first.resolve() : second.resolve()),
|
||||
}
|
||||
const submission = submitInput(
|
||||
adapter,
|
||||
notify,
|
||||
"normal",
|
||||
() => [],
|
||||
)
|
||||
const history: string[] = []
|
||||
const submission = submitInput(adapter, notify, "normal", () => [], history)
|
||||
|
||||
await submission.submit(new Event("submit"))
|
||||
await first.promise
|
||||
@@ -549,6 +548,8 @@ describe("Composer submission", () => {
|
||||
expect(new Set(attempts).size).toBe(1)
|
||||
expect(statuses).toEqual(["running", "idle", "running", "idle"])
|
||||
expect(state.current()).toMatchObject([{ type: "text", content: text }])
|
||||
// The restored prompt is the draft again, so history does not also keep it (and its attachments).
|
||||
expect(history).toEqual([`add:${text}`, `remove:${text}`, `add:${text}`, `remove:${text}`])
|
||||
})
|
||||
|
||||
test("forwards structured mentions to custom commands", async () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ type ComposerSubmitInput = {
|
||||
editor: () => HTMLDivElement | undefined
|
||||
queueScroll: () => void
|
||||
addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void
|
||||
removeFromHistory: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void
|
||||
resetHistory: () => void
|
||||
setMode: (mode: "normal" | "shell") => void
|
||||
closePopover: () => void
|
||||
@@ -250,6 +251,8 @@ function restoreSubmission(
|
||||
) {
|
||||
const restored = submission.restore()
|
||||
if (!restored) return false
|
||||
// The prompt is back in the composer; its history entry would only keep attachments referenced.
|
||||
input.removeFromHistory(value.prompt, value.mode, comments)
|
||||
restored.target.set(restored.prompt, promptLength(restored.prompt))
|
||||
restored.target.mode.set(value.mode)
|
||||
restored.target.context.replaceComments(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { DialogFooter, DialogHeader, DialogTitleGroup, Dialog } from "@opencode/ui/dialog"
|
||||
import { skipToken, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||
import { DateTime } from "luxon"
|
||||
import { type Accessor, createEffect, createMemo, type JSX, startTransition, untrack } from "solid-js"
|
||||
import { notifySessionTabsRemoved } from "@/shell/titlebar/session-events"
|
||||
import { useCommand } from "@/shell/commands/command"
|
||||
@@ -327,19 +326,19 @@ export function homeSessionSearchKey(record: HomeSessionRecord) {
|
||||
return `${pathKey(record.session.location.directory)}:${record.session.id}`
|
||||
}
|
||||
|
||||
// Calendar day in the local time zone, comparable as a number.
|
||||
function localDay(date: Date) {
|
||||
return date.getFullYear() * 10_000 + date.getMonth() * 100 + date.getDate()
|
||||
}
|
||||
|
||||
function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof useLanguage>): HomeSessionGroup[] {
|
||||
const now = DateTime.local()
|
||||
const yesterday = now.minus({ days: 1 })
|
||||
const todaySessions = records.filter((record) =>
|
||||
DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(now, "day"),
|
||||
)
|
||||
const yesterdaySessions = records.filter((record) =>
|
||||
DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(yesterday, "day"),
|
||||
)
|
||||
const olderSessions = records.filter((record) => {
|
||||
const time = DateTime.fromMillis(record.session.time.updated ?? record.session.time.created)
|
||||
return !time.hasSame(now, "day") && !time.hasSame(yesterday, "day")
|
||||
})
|
||||
const now = new Date()
|
||||
const today = localDay(now)
|
||||
const yesterday = localDay(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))
|
||||
const day = (record: HomeSessionRecord) => localDay(new Date(record.session.time.updated ?? record.session.time.created))
|
||||
const todaySessions = records.filter((record) => day(record) === today)
|
||||
const yesterdaySessions = records.filter((record) => day(record) === yesterday)
|
||||
const olderSessions = records.filter((record) => day(record) !== today && day(record) !== yesterday)
|
||||
const olderTitle =
|
||||
todaySessions.length === 0 && yesterdaySessions.length === 0
|
||||
? language.t("sidebar.project.recentSessions")
|
||||
|
||||
@@ -65,14 +65,14 @@
|
||||
}
|
||||
|
||||
/* Keep the 1000px conversation intact, with 320px for the summary and its gutters.
|
||||
Move only as far as needed; wider panels already have enough space in the margin. */
|
||||
@container session-chat (min-width: 1320px) {
|
||||
When they overlap, center the conversation in the space that remains beside the summary. */
|
||||
@container session-chat (min-width: 1320px) and (width < 1640px) {
|
||||
[data-slot="session-chat-panel"][data-summary-open="true"]
|
||||
:is([data-timeline-virtual-content], [data-component="session-composer-dock"] > div) {
|
||||
--session-summary-translate: min(0px, calc(50cqi - 820px));
|
||||
--session-summary-translate: -160px;
|
||||
|
||||
&:dir(rtl) {
|
||||
--session-summary-translate: max(0px, calc(820px - 50cqi));
|
||||
--session-summary-translate: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type Accessor, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import { createSimpleContext } from "@opencode/ui/context"
|
||||
import { useProviders } from "@/providers/catalog/providers"
|
||||
@@ -9,6 +8,8 @@ export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
type Visibility = "show" | "hide"
|
||||
const RECENT_LIMIT = 5
|
||||
// luxon's diffNow().as("months") used an average month; keep the same window.
|
||||
const sixMonths = 6 * 30.436875 * 24 * 60 * 60 * 1000
|
||||
|
||||
function modelKey(model: ModelKey) {
|
||||
return `${model.providerID}:${model.modelID}`
|
||||
@@ -29,27 +30,23 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
|
||||
),
|
||||
)
|
||||
|
||||
// Release dates as epoch ms; an unparseable date is NaN and never counts as recent.
|
||||
const release = createMemo(
|
||||
() =>
|
||||
new Map(
|
||||
available().map((model) => {
|
||||
const parsed = DateTime.fromISO(model.release_date)
|
||||
return [modelKey({ providerID: model.provider.id, modelID: model.id }), parsed] as const
|
||||
}),
|
||||
available().map(
|
||||
(model) => [modelKey({ providerID: model.provider.id, modelID: model.id }), Date.parse(model.release_date)] as const,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter(
|
||||
(x) =>
|
||||
Math.abs(
|
||||
(release().get(modelKey({ providerID: x.provider.id, modelID: x.id })) ?? DateTime.invalid("invalid"))
|
||||
.diffNow()
|
||||
.as("months"),
|
||||
) < 6,
|
||||
),
|
||||
filter((x) => {
|
||||
const released = release().get(modelKey({ providerID: x.provider.id, modelID: x.id })) ?? NaN
|
||||
return Math.abs(Date.now() - released) < sixMonths
|
||||
}),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
@@ -101,9 +98,8 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
|
||||
if (state === "hide") return false
|
||||
if (state === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
const date = release().get(key)
|
||||
if (!date?.isValid) return true
|
||||
return false
|
||||
// Models without a parseable release date stay visible.
|
||||
return !Number.isFinite(release().get(key) ?? NaN)
|
||||
}
|
||||
|
||||
const setVisibility = (model: ModelKey, state: boolean) => {
|
||||
|
||||
@@ -906,6 +906,10 @@ export const dict = {
|
||||
"session.browser.replaced": "Browser control moved to another desktop window.",
|
||||
"session.browser.suspended": "Browser suspended. Interact with this session to reconnect.",
|
||||
"session.browser.address.placeholder": "Enter URL",
|
||||
"session.browser.empty.title": "Enter URL",
|
||||
"session.browser.empty.description": 'Or prompt "Open in the app browser"',
|
||||
"session.browser.failed.title": "URL can't be reached",
|
||||
"session.browser.failed.description": "Check the URL and your connection, then try again.",
|
||||
|
||||
"titlebar.update": "Update",
|
||||
"titlebar.tabs": "Tabs",
|
||||
|
||||
@@ -20,13 +20,18 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const command = useCommand()
|
||||
const state = props.browser.active
|
||||
const address = () => (state()?.url === "about:blank" ? "" : (state()?.url ?? ""))
|
||||
const failed = () => !!state()?.loadError
|
||||
const registration = props.browser.registration
|
||||
const button = { variant: "ghost", size: "large" } as const
|
||||
const [store, setStore] = createStore({
|
||||
address: "",
|
||||
editing: false,
|
||||
submitted: false,
|
||||
// A submitted navigation the browser has not reported yet; keeps the empty state hidden meanwhile.
|
||||
navigating: false,
|
||||
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||
})
|
||||
const empty = () => !address() && !state()?.loading && !store.navigating
|
||||
let surface: HTMLDivElement | undefined
|
||||
let addressDisplay: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
@@ -43,7 +48,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
title: language.t("command.browser.reload"),
|
||||
category: language.t("command.category.view"),
|
||||
keybind: "f5",
|
||||
disabled: !props.visible || !state(),
|
||||
disabled: !props.visible || !address(),
|
||||
onSelect: () => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: "reload", tabID: tab.id })
|
||||
@@ -71,7 +76,9 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const top = Math.round(rect.top * zoom)
|
||||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
const visible = props.visible && store.visible && !dialog.active && !covered(rect)
|
||||
// The desktop page hides blank and loading documents itself; only hide here
|
||||
// while the pane shows its own empty or failed state over the surface.
|
||||
const visible = props.visible && store.visible && !empty() && !failed() && !dialog.active && !covered(rect)
|
||||
// The cutout exposes the app backdrop outside the rounded Review card,
|
||||
// not the browser surface inside it.
|
||||
const color = getComputedStyle(
|
||||
@@ -107,7 +114,25 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
if (frame === undefined) frame = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
createEffect(() => !store.editing && setStore("address", address()))
|
||||
createEffect(on([() => state()?.id, address], () => !store.editing && setStore("address", address())))
|
||||
// Any reported movement, including a rejected or blocked request, ends the submitted navigation.
|
||||
createEffect(
|
||||
on(
|
||||
[() => state()?.id, () => state()?.generation, () => state()?.loading, () => props.browser.error()],
|
||||
() => setStore("navigating", false),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
// A blocked or rejected submission leaves the page where it was; show that page's URL again.
|
||||
createEffect(
|
||||
on(
|
||||
() => props.browser.error(),
|
||||
(error) => {
|
||||
if (error && !store.editing) setStore("address", address())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(
|
||||
on(registration, (current) => {
|
||||
// Session routes can change before this pane unmounts. Hide the registration
|
||||
@@ -123,6 +148,8 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
() => store.visible,
|
||||
() => props.visible,
|
||||
() => state()?.id,
|
||||
empty,
|
||||
failed,
|
||||
registration,
|
||||
],
|
||||
() => {
|
||||
@@ -157,7 +184,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
|
||||
return (
|
||||
<aside id="browser-panel" class="relative size-full min-w-0 overflow-hidden bg-v2-background-bg-base flex flex-col">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-2 border-b border-v2-border-border-muted">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-3 border-b border-v2-border-border-muted">
|
||||
<For each={["back", "forward"] as const}>
|
||||
{(direction) => (
|
||||
<Tooltip placement="top" value={language.t(direction === "back" ? "common.goBack" : "common.goForward")}>
|
||||
@@ -193,7 +220,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
>
|
||||
<IconButton
|
||||
{...button}
|
||||
disabled={!state()}
|
||||
disabled={!state()?.loading && !address()}
|
||||
aria-label={language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
@@ -212,8 +239,13 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const tab = state()
|
||||
if (tab && store.address.trim())
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: store.address })
|
||||
const url = store.address.trim()
|
||||
if (!tab) return
|
||||
if (url || failed()) {
|
||||
setStore({ submitted: true, address: url, navigating: true })
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: url || "about:blank" })
|
||||
}
|
||||
event.currentTarget.querySelector("input")?.blur()
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -224,8 +256,14 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
disabled={!state()}
|
||||
placeholder={language.t("session.browser.address.placeholder")}
|
||||
aria-label={language.t("session.browser.address")}
|
||||
onFocus={() => setStore("editing", true)}
|
||||
onBlur={() => setStore({ editing: false, address: address() })}
|
||||
onFocus={(event) => {
|
||||
setStore("editing", true)
|
||||
event.currentTarget.select()
|
||||
}}
|
||||
onClick={(event) => event.currentTarget.select()}
|
||||
onBlur={() =>
|
||||
setStore({ editing: false, address: store.submitted ? store.address : address(), submitted: false })
|
||||
}
|
||||
onInput={(event) => setStore("address", event.currentTarget.value)}
|
||||
onScroll={(event) => {
|
||||
if (addressDisplay) addressDisplay.scrollLeft = event.currentTarget.scrollLeft
|
||||
@@ -243,12 +281,27 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Show when={props.browser.error()}>
|
||||
<Show when={props.browser.error() && !failed()}>
|
||||
<div class="shrink-0 px-3 py-1.5 text-12-regular text-text-danger-base border-b border-v2-border-border-muted">
|
||||
{props.browser.error()}
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={surface} class="min-h-0 flex-1 bg-v2-background-bg-base flex items-center justify-center">
|
||||
<Show when={(empty() || failed()) && !props.browser.suspended()}>
|
||||
{/* Add the 40px toolbar to the file empty state's 160px bottom padding to align their centers. */}
|
||||
<div
|
||||
dir="auto"
|
||||
class="flex size-full flex-col items-center justify-center gap-2 p-6 pb-[200px] text-center text-text-weak"
|
||||
>
|
||||
<Icon name="globe" size="large" class="mb-2 shrink-0" />
|
||||
<div class="text-[13px] font-medium leading-[var(--line-height-compact)] text-text-strong">
|
||||
{language.t(failed() ? "session.browser.failed.title" : "session.browser.empty.title")}
|
||||
</div>
|
||||
<div class="text-13-regular leading-[var(--line-height-base)]">
|
||||
{language.t(failed() ? "session.browser.failed.description" : "session.browser.empty.description")}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.browser.suspended()}>
|
||||
<p class="px-6 text-center text-13-regular text-v2-text-text-subtle" role="status">
|
||||
{language.t("session.browser.suspended")}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function createSessionContextFormatter(locale: string) {
|
||||
// The fields luxon's DATETIME_MED preset passed to Intl; output is identical.
|
||||
const dateTime = new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
return {
|
||||
number(value: number | null | undefined) {
|
||||
if (value === undefined) return "—"
|
||||
@@ -14,7 +20,7 @@ export function createSessionContextFormatter(locale: string) {
|
||||
},
|
||||
time(value: number | undefined) {
|
||||
if (!value) return "—"
|
||||
return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED)
|
||||
return dateTime.format(value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { children, createMemo, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Keybind } from "@opencode/ui/keybind"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { Tabs } from "@opencode/ui/tabs"
|
||||
@@ -69,9 +71,19 @@ export function SortableTab(props: {
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<Tabs.CloseButton
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.onTabClose(props.tab)}
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.onTabClose(props.tab)
|
||||
}}
|
||||
icon={<Icon name="xmark-small" />}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { For, Show, createEffect, createMemo, lazy, on, onCleanup } from "solid-js"
|
||||
import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
@@ -17,8 +18,6 @@ import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
|
||||
const Icon = lazy(() => import("@opencode/ui/icon").then((module) => ({ default: module.Icon })))
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
const PALETTE_ID = "command.palette"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { canOpenTabRename, canStartTabDrag, forwardTabRef, isTabCloseTarget } from "./tab-gesture"
|
||||
import { canOpenTabRename, forwardTabRef, isTabCloseTarget } from "./tab-gesture"
|
||||
|
||||
describe("titlebar tab gestures", () => {
|
||||
test("excludes close controls from tab gestures", () => {
|
||||
@@ -24,10 +24,4 @@ describe("titlebar tab gestures", () => {
|
||||
expect(canOpenTabRename(false, false, false)).toBe(true)
|
||||
expect(canOpenTabRename(false, false, true)).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves native panning for touch pointers", () => {
|
||||
expect(canStartTabDrag("mouse")).toBe(true)
|
||||
expect(canStartTabDrag("pen")).toBe(true)
|
||||
expect(canStartTabDrag("touch")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,10 +4,6 @@ export function isTabCloseTarget(target: EventTarget | null) {
|
||||
return target instanceof Element && !!target.closest('[data-slot="tab-close"]')
|
||||
}
|
||||
|
||||
export function canStartTabDrag(pointerType: string) {
|
||||
return pointerType !== "touch"
|
||||
}
|
||||
|
||||
export function forwardTabRef(ref: Ref<HTMLDivElement> | undefined, element: HTMLDivElement) {
|
||||
if (typeof ref === "function") ref(element)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-titlebar-tab] {
|
||||
[data-titlebar-tab],
|
||||
[data-titlebar-tab-action] {
|
||||
--tab-base: var(--v2-background-bg-deep);
|
||||
--tab-overlay: transparent;
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
@@ -68,6 +69,14 @@
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:hover, :focus-visible) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:active, [data-state="pressed"]) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] {
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
@@ -328,6 +328,7 @@ export function TabNavItem(props: {
|
||||
|
||||
return (
|
||||
<Menu.Context
|
||||
modal={false}
|
||||
onOpenChange={(open) => {
|
||||
setMenu("open", open)
|
||||
if (open) setPopoverOpen(false)
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { createTabComposerState } from "@/composer/persistence"
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { canStartTabDrag, isTabCloseTarget } from "./tab-gesture"
|
||||
import { isTabCloseTarget } from "./tab-gesture"
|
||||
import { adjacentTabKey, mergeVisibleTabOrder } from "./tab-order"
|
||||
import type { SessionInfo } from "@opencode/client/promise"
|
||||
|
||||
@@ -292,9 +292,11 @@ export function TitlebarTabStrip(props: {
|
||||
<DragDropProvider
|
||||
sensors={[
|
||||
PointerSensor.configure({
|
||||
activationConstraints: [new PointerActivationConstraints.Distance({ value: 4 })],
|
||||
activationConstraints: (event) =>
|
||||
event.pointerType === "touch"
|
||||
? [new PointerActivationConstraints.Distance({ value: 8 })]
|
||||
: [new PointerActivationConstraints.Distance({ value: 4 })],
|
||||
preventActivation: (event) =>
|
||||
!canStartTabDrag(event.pointerType) ||
|
||||
isTabCloseTarget(event.target) ||
|
||||
(event.target instanceof Element && !!event.target.closest('[contenteditable="true"]')),
|
||||
}),
|
||||
@@ -313,6 +315,7 @@ export function TitlebarTabStrip(props: {
|
||||
if (!source) return
|
||||
const tab = props.tabs.find((item) => tabKey(item) === source.id.toString())
|
||||
if (!tab) return
|
||||
if (vertical()) return
|
||||
const tabEl = source.element?.querySelector<HTMLDivElement>("[data-titlebar-tab]")
|
||||
props.onNavigate(tab, tabEl ?? undefined)
|
||||
}}
|
||||
|
||||
@@ -351,9 +351,10 @@ export function Titlebar(props: {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-home"
|
||||
data-state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base data-[state=pressed]:bg-v2-background-bg-layer-02 data-[state=pressed]:text-v2-text-text-base"
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base data-[state=pressed]:text-v2-text-text-base"
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
@@ -650,8 +651,9 @@ export function Titlebar(props: {
|
||||
{homeButton(true)}
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base"
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
|
||||
@@ -36,17 +36,17 @@ export function ErrorOverlay(props: { component: string; error: unknown; onClose
|
||||
<Dialog centered onClose={props.onClose}>
|
||||
<box maxHeight={Math.max(1, dimensions().height - 3)} paddingX={2} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between" flexShrink={0}>
|
||||
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
|
||||
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
|
||||
Error while hot reloading
|
||||
</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={props.onClose}>
|
||||
<text fg={theme.text.muted} onMouseUp={props.onClose}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.default}>
|
||||
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.base}>
|
||||
{props.error instanceof Error ? props.error.message : String(props.error)}
|
||||
</text>
|
||||
<text flexShrink={0} fg={theme.text.subdued}>
|
||||
<text flexShrink={0} fg={theme.text.muted}>
|
||||
{props.component} · Fix the component and save to retry.
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Script } from "@opencode/script"
|
||||
import { $ } from "bun"
|
||||
import { mkdir, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { UpdateArtifact } from "../../../script/update-artifact"
|
||||
|
||||
if (Script.channel !== "beta" && Script.channel !== "latest") {
|
||||
throw new Error("Homebrew publishing requires the beta or latest channel")
|
||||
}
|
||||
const valid =
|
||||
Script.channel === "beta"
|
||||
? /^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/.test(Script.version)
|
||||
: /^\d+\.\d+\.\d+$/.test(Script.version)
|
||||
if (!valid) throw new Error(`Expected a ${Script.channel} release version`)
|
||||
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
const root = path.resolve(process.env.OPENCODE_CLI_DIST ?? path.join(dir, "dist"))
|
||||
const outdir = path.join(root, "homebrew-tap")
|
||||
const dryRun = process.argv.includes("--dry-run")
|
||||
const name = Script.channel === "beta" ? "opencode-beta" : "opencode-v2"
|
||||
const formulaClass = Script.channel === "beta" ? "OpencodeBeta" : "OpencodeV2"
|
||||
|
||||
const targets = await Promise.all(
|
||||
[
|
||||
{ name: "darwin-arm64", archive: "zip" },
|
||||
{ name: "darwin-x64-baseline", archive: "zip" },
|
||||
{ name: "linux-arm64", archive: "tar.gz" },
|
||||
{ name: "linux-x64-baseline", archive: "tar.gz" },
|
||||
].map(async (target) => {
|
||||
const filename = `opencode-${target.name}.${target.archive}`
|
||||
const file = Bun.file(path.join(root, filename))
|
||||
if (!(await file.exists()) || !file.size) throw new Error(`Missing Homebrew archive: ${filename}`)
|
||||
const sha256 = new Bun.CryptoHasher("sha256")
|
||||
for await (const chunk of file.stream()) sha256.update(chunk)
|
||||
return {
|
||||
...target,
|
||||
url: `https://opencode.ai/files/bin/${encodeURIComponent(Script.version)}/${filename}`,
|
||||
sha256: sha256.digest("hex"),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await rm(outdir, { recursive: true, force: true })
|
||||
if (dryRun) await mkdir(outdir, { recursive: true })
|
||||
if (!dryRun) {
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
if (!token) throw new Error("GITHUB_TOKEN is required to update the Homebrew tap")
|
||||
await $`git clone ${`https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`} ${outdir}`
|
||||
await $`git checkout -B master`.cwd(outdir)
|
||||
}
|
||||
|
||||
const target = (name: string) => {
|
||||
const result = targets.find((item) => item.name === name)
|
||||
if (!result) throw new Error(`Missing Homebrew target: ${name}`)
|
||||
return result
|
||||
}
|
||||
const macArm = target("darwin-arm64")
|
||||
const macIntel = target("darwin-x64-baseline")
|
||||
const linuxArm = target("linux-arm64")
|
||||
const linuxIntel = target("linux-x64-baseline")
|
||||
|
||||
await Bun.write(
|
||||
path.join(outdir, `${name}.rb`),
|
||||
[
|
||||
"# typed: false",
|
||||
"# frozen_string_literal: true",
|
||||
"",
|
||||
`class ${formulaClass} < Formula`,
|
||||
` desc "OpenCode V2${Script.channel === "beta" ? " beta" : ""} - the AI coding agent for the terminal"`,
|
||||
' homepage "https://github.com/anomalyco/opencode"',
|
||||
` version "${Script.version}"`,
|
||||
' license "MIT"',
|
||||
"",
|
||||
' depends_on "ripgrep"',
|
||||
' conflicts_with "opencode", because: "both install an opencode binary"',
|
||||
"",
|
||||
" on_macos do",
|
||||
" if Hardware::CPU.arm?",
|
||||
` url "${macArm.url}"`,
|
||||
` sha256 "${macArm.sha256}"`,
|
||||
" else",
|
||||
` url "${macIntel.url}"`,
|
||||
` sha256 "${macIntel.sha256}"`,
|
||||
" end",
|
||||
" end",
|
||||
"",
|
||||
" on_linux do",
|
||||
" if Hardware::CPU.arm?",
|
||||
` url "${linuxArm.url}"`,
|
||||
` sha256 "${linuxArm.sha256}"`,
|
||||
" else",
|
||||
` url "${linuxIntel.url}"`,
|
||||
` sha256 "${linuxIntel.sha256}"`,
|
||||
" end",
|
||||
" end",
|
||||
"",
|
||||
" def install",
|
||||
' bin.install "opencode"',
|
||||
" end",
|
||||
"end",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
console.log(`Prepared ${name} ${Script.version} in ${outdir}`)
|
||||
if (dryRun) process.exit(0)
|
||||
|
||||
await $`git add ${name + ".rb"}`.cwd(outdir)
|
||||
if ((await $`git diff --cached --quiet`.cwd(outdir).nothrow()).exitCode !== 0) {
|
||||
await $`git commit -m ${`chore: update ${name} to ${Script.version}`}`.cwd(outdir)
|
||||
await $`git push origin master`.cwd(outdir)
|
||||
}
|
||||
await UpdateArtifact.publish({
|
||||
channel: Script.channel,
|
||||
name: "cli",
|
||||
distribution: "homebrew",
|
||||
version: Script.version,
|
||||
metadata: { package: `anomalyco/tap/${name}` },
|
||||
})
|
||||
@@ -149,6 +149,10 @@ if (Script.channel === "latest" && Script.release && !dryRun) {
|
||||
|
||||
if ((Script.channel === "beta" || Script.channel === "latest") && Script.release) {
|
||||
await $`bun ./script/publish-aur.ts ${dryRun ? ["--dry-run"] : []}`.env({ ...process.env, OPENCODE_CLI_DIST: root })
|
||||
await $`bun ./script/publish-homebrew.ts ${dryRun ? ["--dry-run"] : []}`.env({
|
||||
...process.env,
|
||||
OPENCODE_CLI_DIST: root,
|
||||
})
|
||||
}
|
||||
|
||||
async function archive(bin: string, target: string, binary: string, directory: string) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
||||
import { Effect } from "effect"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { getErrorReported } from "effect/Runtime"
|
||||
import { Commands } from "./commands/commands"
|
||||
import { Runtime } from "./framework/runtime"
|
||||
import { Observability } from "@opencode/util/observability"
|
||||
@@ -130,5 +131,15 @@ Effect.gen(function* () {
|
||||
Effect.provide(NodeServices.layer),
|
||||
Effect.scoped,
|
||||
Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),
|
||||
NodeRuntime.runMain,
|
||||
// runMain's default reporter logs the fatal cause to stdout. Write it to stderr instead: the
|
||||
// desktop and `Service.ensure` only capture stderr from `serve --service`, so this is the only
|
||||
// channel through which a startup failure's reason reaches the user.
|
||||
Effect.tapCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return
|
||||
if (!getErrorReported(Cause.squash(cause))) return
|
||||
process.stderr.write(Cause.pretty(cause) + "\n")
|
||||
}),
|
||||
),
|
||||
NodeRuntime.runMain({ disableErrorReporting: true }),
|
||||
)
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Global } from "@opencode/util/global"
|
||||
import { AppProcess } from "@opencode/util/process"
|
||||
import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_LOCAL, OPENCODE_VERSION } from "../version"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Ref } from "effect"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Option, Ref, Schema } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
import { action, parseReleaseVersion, type Policy } from "./updater-action"
|
||||
|
||||
export const methods = ["curl", "npm", "pnpm", "bun", "yarn"] as const
|
||||
export const methods = ["curl", "npm", "pnpm", "bun", "yarn", "vp", "brew"] as const
|
||||
|
||||
export type Method = (typeof methods)[number]
|
||||
export type RunResult = { readonly type: "available" | "installed"; readonly version: string }
|
||||
export type CheckResult = RunResult | { readonly type: "unavailable"; readonly message: string }
|
||||
|
||||
const decodeVpPackages = Schema.decodeUnknownOption(
|
||||
Schema.fromJsonString(Schema.Array(Schema.Struct({ name: Schema.String }))),
|
||||
)
|
||||
|
||||
export interface Interface {
|
||||
readonly run: (onInstall?: (version: string) => void) => Effect.Effect<RunResult | undefined>
|
||||
readonly check: () => Effect.Effect<CheckResult | undefined, Error>
|
||||
@@ -96,6 +101,13 @@ const make = Effect.gen(function* () {
|
||||
process.platform === "win32" ? "opencode.exe" : "opencode",
|
||||
)
|
||||
if (path.resolve(process.execPath) === path.resolve(binary)) return "curl"
|
||||
const executable = yield* fs.realPath(process.execPath).pipe(Effect.orElseSucceed(() => process.execPath))
|
||||
if (
|
||||
["opencode-beta", "opencode-v2"].some((name) =>
|
||||
executable.includes(`${path.sep}Cellar${path.sep}${name}${path.sep}`),
|
||||
)
|
||||
)
|
||||
return "brew"
|
||||
if (!installedPackage) return
|
||||
|
||||
const checks: ReadonlyArray<{ method: Method; command: string[] }> = [
|
||||
@@ -103,22 +115,30 @@ const make = Effect.gen(function* () {
|
||||
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
||||
{ method: "yarn", command: ["yarn", "global", "list"] },
|
||||
{ method: "vp", command: ["vp", "list", "-g", "--json", installedPackage] },
|
||||
]
|
||||
const results = yield* Effect.forEach(
|
||||
checks,
|
||||
(check) => exec(check.command).pipe(Effect.map((result) => ({ check, result }))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return results.find((result) => result.result.stdout.includes(installedPackage))?.check.method
|
||||
return results.find((result) => {
|
||||
if (result.check.method !== "vp") return result.result.stdout.includes(installedPackage)
|
||||
// Vite+ repeats the filter in its successful no-match message, so substring detection would be a false positive.
|
||||
return Option.exists(decodeVpPackages(result.result.stdout), (packages) =>
|
||||
packages.some((item) => item.name === installedPackage),
|
||||
)
|
||||
})?.check.method
|
||||
})
|
||||
|
||||
const removal = (method: Method) => {
|
||||
if (method === "curl" || !installedPackage) return undefined
|
||||
if (method === "curl" || method === "brew" || !installedPackage) return undefined
|
||||
const commands = {
|
||||
npm: ["npm", "uninstall", "--global", installedPackage],
|
||||
pnpm: ["pnpm", "remove", "--global", installedPackage],
|
||||
bun: ["bun", "remove", "--global", installedPackage],
|
||||
yarn: ["yarn", "global", "remove", installedPackage],
|
||||
vp: ["vp", "uninstall", "-g", installedPackage],
|
||||
}
|
||||
const command = commands[method]
|
||||
return {
|
||||
@@ -133,11 +153,12 @@ const make = Effect.gen(function* () {
|
||||
}
|
||||
}
|
||||
|
||||
const release = Effect.fnUntraced(function* () {
|
||||
const release = Effect.fnUntraced(function* (method?: Method) {
|
||||
const distribution = method === "brew" ? "homebrew" : "npm"
|
||||
const response = yield* Effect.tryPromise({
|
||||
try: (signal) =>
|
||||
fetch(
|
||||
`https://opencode.ai/update/api/${encodeURIComponent(channel)}/${encodeURIComponent(OPENCODE_ARTIFACT)}/npm?current=${encodeURIComponent(OPENCODE_VERSION)}`,
|
||||
`https://opencode.ai/update/api/${encodeURIComponent(channel)}/${encodeURIComponent(OPENCODE_ARTIFACT)}/${distribution}?current=${encodeURIComponent(OPENCODE_VERSION)}`,
|
||||
{
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
||||
},
|
||||
@@ -153,7 +174,11 @@ const make = Effect.gen(function* () {
|
||||
return { package: data.metadata.package, version: data.version }
|
||||
})
|
||||
|
||||
const latest = () => release().pipe(Effect.map((data) => data.version))
|
||||
const latest = () =>
|
||||
method().pipe(
|
||||
Effect.flatMap(release),
|
||||
Effect.map((data) => data.version),
|
||||
)
|
||||
|
||||
const temporaryDirectory = (prefix: string) =>
|
||||
Effect.acquireRelease(fs.makeTempDirectory({ directory: global.cache, prefix }), (directory) =>
|
||||
@@ -163,12 +188,12 @@ const make = Effect.gen(function* () {
|
||||
const upgrade = Effect.fnUntraced(function* (method: Method, input: string) {
|
||||
if (!parseReleaseVersion(input)) return yield* Effect.fail(new Error(`Invalid version: ${input}`))
|
||||
const version = input.trim().replace(/^v/, "")
|
||||
const packageName = (yield* release()).package
|
||||
const packageName = (yield* release(method)).package
|
||||
const target = `${packageName}@${version}`
|
||||
if (installedPackage && packageName !== installedPackage && (method === "pnpm" || method === "yarn")) {
|
||||
return yield* Effect.fail(new Error(`Reinstall ${target} with ${method} to migrate from ${installedPackage}.`))
|
||||
}
|
||||
const commands: Record<Exclude<Method, "bun" | "curl">, string[]> = {
|
||||
const commands: Record<Exclude<Method, "bun" | "curl" | "brew">, string[]> = {
|
||||
// Keep the old package: uninstalling it can unlink the replacement command.
|
||||
npm: [
|
||||
"npm",
|
||||
@@ -182,6 +207,10 @@ const make = Effect.gen(function* () {
|
||||
],
|
||||
pnpm: ["pnpm", "add", "--global", `--allow-build=${packageName}`, target],
|
||||
yarn: ["yarn", "global", "add", target],
|
||||
vp:
|
||||
installedPackage && packageName !== installedPackage
|
||||
? ["vp", "install", "-g", "--force", target]
|
||||
: ["vp", "update", "-g", target],
|
||||
}
|
||||
const result = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
@@ -202,6 +231,7 @@ const make = Effect.gen(function* () {
|
||||
if (download.code !== 0) return download
|
||||
return yield* exec(["bash", installer, "--version", version, "--no-modify-path"], "5 minutes")
|
||||
}
|
||||
if (method === "brew") return yield* exec(["brew", "upgrade", packageName], "5 minutes")
|
||||
return yield* exec(commands[method], "5 minutes")
|
||||
}),
|
||||
).pipe(Effect.mapError((cause) => new Error(`Failed to update with ${method}`, { cause })))
|
||||
|
||||
@@ -19,6 +19,7 @@ function fixture(
|
||||
} = () => ({}),
|
||||
name = "@opencode/cli",
|
||||
failCleanup = false,
|
||||
releasePackage = name,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
@@ -34,7 +35,7 @@ function fixture(
|
||||
yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
spyOn(globalThis, "fetch").mockImplementation(
|
||||
Object.assign(async () => Response.json({ version: "2.3.4", metadata: { package: name } }), {
|
||||
Object.assign(async () => Response.json({ version: "2.3.4", metadata: { package: releasePackage } }), {
|
||||
preconnect: fetch.preconnect,
|
||||
}),
|
||||
),
|
||||
@@ -106,6 +107,7 @@ const installs = [
|
||||
command: ["pnpm", "add", "--global", "--allow-build=@opencode/cli", "@opencode/cli@2.3.4-beta.1"],
|
||||
},
|
||||
{ method: "yarn", command: ["yarn", "global", "add", "@opencode/cli@2.3.4-beta.1"] },
|
||||
{ method: "vp", command: ["vp", "update", "-g", "@opencode/cli@2.3.4-beta.1"] },
|
||||
] as const
|
||||
|
||||
installs.forEach(({ method, command }) => {
|
||||
@@ -117,6 +119,25 @@ installs.forEach(({ method, command }) => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.live("vp force-installs a renamed V2 package that replaces the existing binary owner", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture(() => ({}), "@opencode/cli", false, "@opencode/cli-node")
|
||||
yield* test.updater.upgrade("vp", "v2.3.4-beta.1")
|
||||
expect(test.commands).toEqual([["vp", "install", "-g", "--force", "@opencode/cli-node@2.3.4-beta.1"]])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("vp removes the package from its managed global store", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture()
|
||||
const removal = test.updater.removal("vp")
|
||||
if (!removal) return yield* Effect.die("Expected vp removal command")
|
||||
expect(removal.command).toEqual(["vp", "uninstall", "-g", "@opencode/cli"])
|
||||
yield* removal.run
|
||||
expect(test.commands).toEqual([["vp", "uninstall", "-g", "@opencode/cli"]])
|
||||
}),
|
||||
)
|
||||
;[0, 1].forEach((exitCode) => {
|
||||
it.live(`bun isolates and removes its install cache after exit ${exitCode}`, () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -200,11 +221,19 @@ it.live("install failures expose stderr and process errors do not report success
|
||||
expect(missing.commands).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
;(["npm", "pnpm", "bun", "yarn", undefined] as const).forEach((method) => {
|
||||
;(["npm", "pnpm", "bun", "yarn", "vp", undefined] as const).forEach((method) => {
|
||||
it.live(`method detection identifies ${method ?? "an unknown installation"} using the V2 package`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) => ({
|
||||
stdout: Buffer.from(command.command === method ? "@opencode/cli@2.3.4" : "opencode-ai@1.0.0"),
|
||||
stdout: Buffer.from(
|
||||
command.command === method
|
||||
? method === "vp"
|
||||
? JSON.stringify([{ name: "@opencode/cli", version: "2.3.4" }])
|
||||
: "@opencode/cli@2.3.4"
|
||||
: command.command === "vp"
|
||||
? "[]"
|
||||
: "opencode-ai@1.0.0",
|
||||
),
|
||||
}))
|
||||
expect(yield* test.updater.method()).toBe(method)
|
||||
expect(test.commands).toEqual([
|
||||
@@ -212,6 +241,7 @@ it.live("install failures expose stderr and process errors do not report success
|
||||
["pnpm", "list", "-g", "--depth=0", "@opencode/cli"],
|
||||
["bun", "pm", "ls", "-g"],
|
||||
["yarn", "global", "list"],
|
||||
["vp", "list", "-g", "--json", "@opencode/cli"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -225,7 +255,16 @@ it.live("method detection tolerates unavailable package managers", () =>
|
||||
: { error: new AppProcess.AppProcessError({ command: command.command }) },
|
||||
)
|
||||
expect(yield* test.updater.method()).toBe("yarn")
|
||||
expect(test.commands).toHaveLength(4)
|
||||
expect(test.commands).toHaveLength(5)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("vp detection ignores no-match output that repeats the package name", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) => ({
|
||||
stdout: Buffer.from(command.command === "vp" ? "No global packages matching '@opencode/cli'." : ""),
|
||||
}))
|
||||
expect(yield* test.updater.method()).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -265,13 +304,16 @@ if (typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-no
|
||||
expect(yield* test.updater.method()).toBe("npm")
|
||||
yield* test.updater.upgrade("npm", "v2.3.4")
|
||||
yield* test.updater.upgrade("pnpm", "v2.3.4")
|
||||
yield* test.updater.upgrade("vp", "v2.3.4")
|
||||
expect(test.commands).toEqual([
|
||||
["npm", "list", "-g", "--depth=0", "@opencode/cli-node"],
|
||||
["pnpm", "list", "-g", "--depth=0", "@opencode/cli-node"],
|
||||
["bun", "pm", "ls", "-g"],
|
||||
["yarn", "global", "list"],
|
||||
["vp", "list", "-g", "--json", "@opencode/cli-node"],
|
||||
["npm", "install", "--global", "@opencode/cli-node@2.3.4"],
|
||||
["pnpm", "add", "--global", "--allow-build=@opencode/cli-node", "@opencode/cli-node@2.3.4"],
|
||||
["vp", "update", "-g", "@opencode/cli-node@2.3.4"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -35,6 +35,12 @@ describe("upgrade command", () => {
|
||||
expect(result.events).toEqual([{ method: "bun", version: "2.0.0" }])
|
||||
})
|
||||
|
||||
test("accepts vp as an explicit installation method", async () => {
|
||||
const result = await cli(["2.0.0", "--method", "vp"])
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.events).toEqual([{ method: "vp", version: "2.0.0" }])
|
||||
})
|
||||
|
||||
test("skips the already installed version", async () => {
|
||||
const result = await cli(["v0.0.0-beta-old"])
|
||||
expect(result.exitCode).toBe(0)
|
||||
@@ -50,7 +56,7 @@ describe("upgrade command", () => {
|
||||
})
|
||||
|
||||
test("rejects unsupported methods before attempting an upgrade", async () => {
|
||||
const result = await cli(["--method", "brew"])
|
||||
const result = await cli(["--method", "apt"])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.events).toEqual([])
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||
|
||||
await $`rm -rf dist`
|
||||
await $`bun tsc -p tsconfig.build.json`
|
||||
const root = path.resolve("src")
|
||||
const result = await Bun.build({
|
||||
entrypoints: await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true })).then((files) =>
|
||||
files.filter((file) => !file.endsWith(".d.ts")),
|
||||
),
|
||||
root,
|
||||
outdir: "dist",
|
||||
target: "node",
|
||||
format: "esm",
|
||||
packages: "external",
|
||||
splitting: true,
|
||||
naming: {
|
||||
entry: "[dir]/[name].[ext]",
|
||||
chunk: "chunks/[name]-[hash].[ext]",
|
||||
},
|
||||
})
|
||||
if (!result.success) throw new AggregateError(result.logs, "Failed to build Client")
|
||||
|
||||
@@ -131,7 +131,9 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
}).pipe(
|
||||
Effect.repeat({
|
||||
until: Option.isSome,
|
||||
schedule: Schedule.max([Schedule.spaced(timing.pollInterval), Schedule.recurs(timing.attempts)]),
|
||||
// Probes run sequentially, so a slow probe stretches each iteration; bound the loop by wall clock
|
||||
// like the Promise variant rather than by attempt count.
|
||||
schedule: Schedule.spaced(timing.pollInterval).pipe(Schedule.upTo({ duration: timing.promiseTimeout })),
|
||||
}),
|
||||
Effect.ensuring(Effect.sync(() => contenders.forEach((contender) => contender.release()))),
|
||||
)
|
||||
|
||||
@@ -456,6 +456,10 @@ export type V2EventServerConnected = {
|
||||
data: {}
|
||||
}
|
||||
|
||||
export type ModelSettings = { compaction?: ProviderCompaction } & { [x: string]: any }
|
||||
|
||||
export type ConfigModelSettings = { compaction?: ProviderCompaction } & { [x: string]: JsonValue | null }
|
||||
|
||||
export type ProviderSettings = {
|
||||
timeout?: number | false
|
||||
chunkTimeout?: number
|
||||
@@ -1656,19 +1660,19 @@ export type SessionInboxMove = {
|
||||
payload: SessionInboxMovePayload
|
||||
}
|
||||
|
||||
export type ModelVariant = {
|
||||
id: string
|
||||
settings?: ModelSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
}
|
||||
|
||||
export type ProviderRequest = {
|
||||
settings: ProviderSettings
|
||||
headers: { [x: string]: string }
|
||||
body: { [x: string]: any }
|
||||
}
|
||||
|
||||
export type ModelVariant = {
|
||||
id: string
|
||||
settings?: ProviderSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
}
|
||||
|
||||
export type ProviderInfo = {
|
||||
id: string
|
||||
canonical?: string
|
||||
@@ -1898,7 +1902,7 @@ export type ModelInfo = {
|
||||
name: string
|
||||
compatibility?: ModelCompatibility
|
||||
package?: string
|
||||
settings?: ProviderSettings
|
||||
settings?: ModelSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
capabilities: ModelCapabilities
|
||||
@@ -2107,13 +2111,13 @@ export type ConfigEntry =
|
||||
name?: string
|
||||
compatibility?: ModelCompatibility
|
||||
package?: string
|
||||
settings?: ConfigProviderSettings
|
||||
settings?: ConfigModelSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: JsonValue }
|
||||
capabilities?: ModelCapabilities
|
||||
variants?: Array<{
|
||||
id: string
|
||||
settings?: ConfigProviderSettings
|
||||
settings?: ConfigModelSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: JsonValue }
|
||||
}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export type EnsureTiming = {
|
||||
readonly pollInterval: number
|
||||
readonly attempts: number
|
||||
readonly requestTimeout: number
|
||||
readonly spawnDelay: number
|
||||
readonly maxSpawnDelay: number
|
||||
@@ -11,9 +10,11 @@ export type EnsureTiming = {
|
||||
|
||||
const timings = new WeakMap<object, EnsureTiming>()
|
||||
|
||||
// A freshly spawned service registers in ~250 ms, so the poll cadence is a large share of the
|
||||
// time a client waits for it. Probes are sequential, so the wait between them only matters while
|
||||
// the connection is refused; both variants give up after promiseTimeout of wall-clock time.
|
||||
export const defaultEnsureTiming: EnsureTiming = {
|
||||
pollInterval: 100,
|
||||
attempts: 1_200,
|
||||
pollInterval: 25,
|
||||
requestTimeout: 2_000,
|
||||
spawnDelay: 5_000,
|
||||
maxSpawnDelay: 30_000,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { withEnsureTiming } from "../../src/service-timing"
|
||||
|
||||
const timing = {
|
||||
pollInterval: 20,
|
||||
attempts: 120,
|
||||
requestTimeout: 100,
|
||||
spawnDelay: 200,
|
||||
maxSpawnDelay: 1_200,
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.
|
||||
- State model-visible diagnostics, logs, tool descriptions, and instructions directly. The execution context is already clear; do not repeat `Code Mode` or `CodeMode` unless the distinction is necessary.
|
||||
- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR.
|
||||
- Program values are `Value` (`src/interpreter/objects.ts`); host values are `unknown` and are copied in at the boundaries (`fromHost`, `fromJson`). Do not widen program-facing signatures back to `unknown`.
|
||||
- A built-in kind of object is one `Obj` subclass (`Wrapper` for host-backed data such as Date or Map, `Opaque` for machinery such as functions and promises) that overrides `tag`, `toString`, `toPrimitive`, `inspect`, `toHost`, and `iterator` as needed. Do not add `instanceof` ladders over the built-in classes elsewhere; ask the object.
|
||||
|
||||
## OpenAPI
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ The idea of code mode was originally introduced by Cloudflare. See
|
||||
|
||||
## How it differs from JavaScript
|
||||
|
||||
- **Only supported APIs are available.** Programs can use the provided tools and supported JavaScript built-ins. APIs
|
||||
such as `fetch`, timers, `process`, filesystem access, imports, and modules are unavailable.
|
||||
- **Only supported APIs are available.** Programs can use the provided tools, supported JavaScript built-ins, and the
|
||||
globals the host adds through extensions. Timers, `process`, filesystem access, imports, and modules are unavailable.
|
||||
- **Unfinished work is interrupted.** Tool calls and async functions start when called. When the program finishes,
|
||||
anything still running is interrupted. Unhandled rejections from un-awaited promises are returned as warnings.
|
||||
- **REPL-style results.** Without an explicit `return`, the final top-level expression becomes the result. `undefined`
|
||||
@@ -94,11 +94,19 @@ receive `{ extension, name, args }`. An `after` hook also receives how the call
|
||||
`failure` with its error, or `interrupted`). A failing `before` hook denies the call, and the program catches the
|
||||
failure as a thrown error.
|
||||
|
||||
### `Values`
|
||||
### `Extension.make`
|
||||
|
||||
`Values` exports the runtime's non-JSON value classes: `Values.URL`, `Values.URLSearchParams`, `Values.Date`,
|
||||
`Values.RegExp`, `Values.Map`, `Values.Set`, and `Values.Promise`. The interpreter recognizes these by class; a
|
||||
program's `new URL(...)` is a `Values.URL` wrapping the host `URL`. `Values.isValue` narrows to the data-like kinds.
|
||||
Extensions are host functions a program calls directly as globals, such as `fetch`. Unlike tools they are not in the
|
||||
catalog, not counted against `maxToolCalls`, and not described to the model; the host decides what they mean.
|
||||
|
||||
```ts
|
||||
const web = Extension.make({ name: "web", globals: { fetch: (url: string) => globalThis.fetch(url) } })
|
||||
const runtime = CodeMode.make({ tools, extensions: [web] })
|
||||
```
|
||||
|
||||
Every value crossing in either direction is converted, never shared: arguments come in as copies, results go out as
|
||||
copies, and a function inside a result is callable the same way. A global that shadows a built-in or another
|
||||
extension throws at `CodeMode.make`.
|
||||
|
||||
### OpenAPI tools
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
## Source and execution model
|
||||
|
||||
- [x] JavaScript parsed with the latest syntax accepted by Acorn, then restricted by the interpreter allowlist.
|
||||
- [x] Erasable TypeScript syntax, including type annotations, type declarations, assertions, and non-null assertions.
|
||||
TypeScript is transpiled first; the emitted JavaScript must still use the supported subset.
|
||||
TypeScript-only syntax is rejected rather than stripped before execution.
|
||||
- [x] Top-level `await` and `return` through the program's implicit async-function scope.
|
||||
- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced.
|
||||
- [x] The host boundary is `JSON.stringify` plus a short table. The program result and tool arguments cross as
|
||||
@@ -25,11 +24,11 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
and Map, RegExp, and generators serialize as `{}`. A bare `undefined` result is `null`.
|
||||
Tool results come back the way `JSON.parse(JSON.stringify(result))` would. The table, where a value cannot
|
||||
be JSON but what the program meant is clear: a promise is awaited (a rejection fails the program), a Set
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own }`, a
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own enumerable }`, a
|
||||
Uint8Array is rejected with a hint to encode as text, and own `__proto__` keys are dropped so merging tool
|
||||
inputs or results cannot replace a prototype. In-program `JSON.stringify` keeps JS behavior except for the
|
||||
Error form and a promise, which is a `TypeError` with an await hint rather than a silent `{}`.
|
||||
- [x] Live Date, RegExp, Map, Set, URL, URLSearchParams, and Uint8Array values inside CodeMode.
|
||||
- [x] Live Date, RegExp, Map, Set, URL, URLSearchParams, Headers, and Uint8Array values inside CodeMode.
|
||||
- [x] Tool calls through the host-provided `tools` tree only.
|
||||
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
|
||||
shadowable by program declarations like other globals.
|
||||
@@ -39,16 +38,17 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
10,000,000 elements (`Array(n)`, `length =`, `Array.from`, `split`, `matchAll`, `concat`, `flat`; below the JS
|
||||
maximum of 2^32 - 1), and 10,000 pending promises at once. Exceeding one throws a `RangeError`. A single regular
|
||||
expression match can still run long on a pathological pattern; the host regex engine has no interrupt hook.
|
||||
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
|
||||
rest parameter are accepted unless the program itself begins with `"use strict"`.
|
||||
- [ ] Valid JavaScript rejected by TypeScript transpilation before interpretation, such as `in` inside a destructuring
|
||||
default in a `for...of` head and Unicode-escaped keywords.
|
||||
- [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`.
|
||||
- [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at
|
||||
parse time. Without it, `yield` is an ordinary binding.
|
||||
- [ ] Duplicate parameter names in non-strict code throw when the function is called, instead of binding the last
|
||||
parameter as JavaScript does.
|
||||
|
||||
## Values and literals
|
||||
|
||||
- [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings.
|
||||
- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, custom synchronous
|
||||
iterators, and synchronous generators.
|
||||
- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, Headers, custom
|
||||
synchronous iterators, and synchronous generators.
|
||||
- [x] Object literals with shorthand, computed string/number keys, and spread following ToObject: data objects and
|
||||
arrays copy own enumerable keys, strings copy index keys, and other values contribute nothing.
|
||||
- [x] Template literals with interpolation.
|
||||
@@ -56,7 +56,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `NaN` and `Infinity` globals.
|
||||
- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries.
|
||||
- [ ] Arbitrary Symbol primitive values and symbol-keyed properties. The confined `Symbol.iterator` and
|
||||
`Symbol.asyncIterator` keys are available only for custom iterator protocols.
|
||||
`Symbol.asyncIterator` keys are available only for the iterator protocols.
|
||||
- [ ] Tagged-template calls.
|
||||
- [ ] Getter and setter definitions in object literals.
|
||||
|
||||
@@ -95,9 +95,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `if`/`else` and conditional expressions.
|
||||
- [x] `switch`, including default clauses and fallthrough.
|
||||
- [x] `for`, `while`, and `do...while`.
|
||||
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and confined
|
||||
synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references; other values iterate nothing.
|
||||
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, Headers, Uint8Arrays, built-in iterators, custom
|
||||
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
|
||||
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
|
||||
- [x] Unlabeled `break` and `continue`.
|
||||
- [x] `try`, `catch`, optional catch bindings, and `finally`.
|
||||
- [x] `throw` with arbitrary values.
|
||||
@@ -111,7 +112,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
## Functions and callbacks
|
||||
|
||||
- [x] Function declarations, function expressions, and arrow functions.
|
||||
- [x] Synchronous and `async` functions.
|
||||
- [x] Synchronous and `async` functions. A line break between `function` and the name is allowed, as in JavaScript;
|
||||
a line break between `async` and `function` is not an async function.
|
||||
- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters.
|
||||
- [x] A call depth limit of 10000: deeper nesting throws a catchable `RangeError: Maximum call stack size exceeded`
|
||||
at the overflowing call instead of running until the timeout. Callbacks invoked by built-ins count below the
|
||||
@@ -127,7 +129,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
string). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
|
||||
because `includes` is called without a string `this`.
|
||||
- [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`),
|
||||
and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Promise`) throw a `TypeError`,
|
||||
and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Headers`, `Promise`) throw a `TypeError`,
|
||||
like JS.
|
||||
- [x] Tool references and detached `Promise` statics are rejected as callbacks with a hint to wrap them in an
|
||||
arrow function.
|
||||
@@ -146,7 +148,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`Array.prototype.push.name === "push"`).
|
||||
- [ ] A named function expression's name is not bound inside its own body.
|
||||
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
|
||||
- [ ] A line terminator between `async function` and the function name.
|
||||
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
|
||||
rather than at the call, so their errors are not thrown synchronously.
|
||||
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
|
||||
@@ -179,11 +180,12 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Sequence expressions (the comma operator).
|
||||
- [x] `await` for CodeMode promises and callable thenables; a plain value passes through unchanged, though every
|
||||
`await` still defers its continuation one reaction turn.
|
||||
- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. `new` on any
|
||||
other value throws a catchable `TypeError` naming the callee: other built-in functions such as `Number` say
|
||||
`new` is unsupported and point at the plain call, user-defined functions report the constructor gap below, and
|
||||
non-callable values are not constructors. Error constructors take the ES2022 options object, so
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present.
|
||||
- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, Headers, and Promise. `new`
|
||||
on any other value throws a catchable `TypeError` naming the callee: other built-in functions such as `Number`
|
||||
say `new` is unsupported and point at the plain call, user-defined functions report the constructor gap below,
|
||||
and non-callable values are not constructors. Error constructors take the ES2022 options object, so
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present. `Error.isError`
|
||||
is true for every Error value.
|
||||
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
|
||||
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
|
||||
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
|
||||
@@ -273,8 +275,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `Object.is` for supported data values.
|
||||
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
|
||||
and plain-object results.
|
||||
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString`, `valueOf`,
|
||||
`hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
|
||||
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`, `"[object Map]"`, `"[object Promise]"`, and so
|
||||
on for every built-in kind, as JS reports through `Symbol.toStringTag`), `toLocaleString` (calls the value's
|
||||
`toString`, as in JS), `valueOf`, `hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
|
||||
|
||||
## Arrays
|
||||
|
||||
@@ -288,9 +291,14 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`lastIndexOf`.
|
||||
- [x] Aggregation: `reduce` and `reduceRight`.
|
||||
- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, and `join`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, `join`, and `toLocaleString` (each element's
|
||||
`toLocaleString`, holes and nullish elements as empty strings).
|
||||
- [x] Mutation: `push`, `pop`, `shift`, `unshift`, `splice`, `fill`, and `copyWithin`.
|
||||
- [x] Materialized iteration helpers: `keys`, `values`, and `entries` return arrays rather than iterators.
|
||||
- [x] `keys`, `values`, `entries`, and `[Symbol.iterator]` (the same function as `values`) return live iterator objects
|
||||
with `next()` and `[Symbol.iterator]`, as in JS. Iterator objects are opaque references: they print as
|
||||
`[opaque reference]`, serialize to `{}`, and cannot be passed to extensions. Every built-in collection iterator
|
||||
shares one prototype. JavaScript gives each collection its own; the difference is not observable here because
|
||||
`Object.getPrototypeOf` is not exposed.
|
||||
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
||||
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
||||
- [x] `Array.prototype.toSpliced`.
|
||||
@@ -303,13 +311,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
form, like `JSON.stringify`.
|
||||
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
|
||||
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
|
||||
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
|
||||
`undefined`.
|
||||
- [ ] Iterator objects from `keys`, `values`, and `entries` with a live `next()`.
|
||||
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
|
||||
`undefined`; `includes()` still requires a value.
|
||||
|
||||
## Strings
|
||||
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`, and the `toLocaleLowerCase`/`toLocaleUpperCase`
|
||||
aliases, which ignore their locale argument and apply the default Unicode casing.
|
||||
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
|
||||
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
|
||||
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
|
||||
@@ -317,7 +325,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
|
||||
- [x] `localeCompare`; locale and options arguments are currently ignored.
|
||||
- [x] `isWellFormed` and `toWellFormed`.
|
||||
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
|
||||
- [x] `toString`, `length`, numeric indexing, spread, `for...of`, and `[Symbol.iterator]` by Unicode code point.
|
||||
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
|
||||
- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like
|
||||
native JS, `split(undefined)` returns the whole string, and `includes`/`startsWith`/`endsWith` reject regular
|
||||
@@ -333,7 +341,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Coercion functions: `Number`, `parseInt`, and `parseFloat`.
|
||||
- [x] Number predicates/parsers: `Number.isInteger`, `Number.isFinite`, `Number.isNaN`, `Number.isSafeInteger`,
|
||||
`Number.parseInt`, and `Number.parseFloat`.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, and `valueOf`.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, `valueOf`, and `toLocaleString`, which
|
||||
always formats as `en-US` (`"1,234.5"`) so output does not depend on the host.
|
||||
- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`,
|
||||
`POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`.
|
||||
- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`.
|
||||
@@ -346,7 +355,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
use their epoch time) and reject opaque runtime references as data errors.
|
||||
- [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined`
|
||||
for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
|
||||
example `Math.sum is not a function.` Unknown `Promise` statics keep their descriptive error.
|
||||
example `Math.sum is not a function.` or `search(...).catch is not a function.` Unknown `Promise` statics keep
|
||||
their descriptive error.
|
||||
- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number
|
||||
elements without coercion.
|
||||
- [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
|
||||
@@ -380,6 +390,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`TimeClip` behavior.
|
||||
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
|
||||
- [x] `toDateString` and `toTimeString` in the host's local timezone.
|
||||
- [x] `toLocaleString`, `toLocaleDateString`, and `toLocaleTimeString` always format as `en-US` in UTC
|
||||
(`"1/1/1970, 12:00:00 AM"`) so output does not depend on the host.
|
||||
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
|
||||
- [ ] Date setters and multi-argument construction coerce object arguments through `valueOf`/`toString` and surface
|
||||
their throws.
|
||||
@@ -405,10 +417,11 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
|
||||
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
|
||||
- [x] `new Map()` from synchronous iterables of entries.
|
||||
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`.
|
||||
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, `forEach`, `getOrInsert`, and `getOrInsertComputed`.
|
||||
- [x] `new Set()` from synchronous iterables.
|
||||
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
|
||||
- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set.
|
||||
- [x] Live `keys`, `values`, `entries`, and `[Symbol.iterator]` iterators for Map and Set; a Set-like operand's `keys()`
|
||||
may return a built-in iterator or an array.
|
||||
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
|
||||
- [x] Map and Set values serialize to `{}` at host/JSON boundaries.
|
||||
- [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
|
||||
@@ -424,8 +437,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Writable URL fields except `origin`.
|
||||
- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams.
|
||||
- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`,
|
||||
`entries`, `toString`, and `size`.
|
||||
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
|
||||
`entries`, `[Symbol.iterator]`, `toString`, and `size`.
|
||||
- [x] URL values are their href in `JSON.stringify` and at the host boundary. URLSearchParams are `{}` in
|
||||
`JSON.stringify` and their query string at the host boundary.
|
||||
|
||||
## Uint8Array
|
||||
|
||||
@@ -437,7 +451,8 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
- [x] Index reads and writes with JS byte semantics: values wrap modulo 256, out-of-range writes are ignored, indexes
|
||||
cannot be deleted. `length` is a prototype accessor, so `Object.keys` lists only indexes.
|
||||
- [x] `at`, `slice`, `subarray` (a view on the same bytes), `set`, `fill`, `reverse`, `indexOf`, `lastIndexOf`,
|
||||
`includes`, `join`, `toString`, `toBase64`, `toHex`, and materialized `keys`, `values`, and `entries` arrays.
|
||||
`includes`, `join`, `toString`, `toBase64`, `toHex`, and live `keys`, `values`, `entries`, and `[Symbol.iterator]`
|
||||
iterators.
|
||||
- [x] Spread, destructuring, `for...of`, `yield*`, `Array.from`, and `new Set(bytes)`. `Array.isArray` is false.
|
||||
- [x] String coercion joins with commas; `JSON.stringify` gives `{"0":1,...}`; `console.log` prints
|
||||
`Uint8Array(n) [...]`.
|
||||
@@ -451,7 +466,13 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
- [x] `crypto.randomUUID()` and `crypto.getRandomValues(uint8Array)`.
|
||||
- [x] `TextEncoder` and `TextDecoder` for UTF-8 only: any other label is a `RangeError`. `TextDecoder` accepts the
|
||||
`fatal` and `ignoreBOM` options; `decode` takes a Uint8Array or nothing.
|
||||
- [ ] `crypto.subtle`, `Blob`, and `TextDecoder` streaming or non-UTF-8 encodings.
|
||||
- [x] `new Headers()` from records, synchronous iterables of pairs, and Headers, wrapping the host's `Headers`: names
|
||||
fold to lowercase, values are normalized and combined, and invalid names or values throw a `TypeError`.
|
||||
- [x] Headers `append`, `delete`, `get`, `getSetCookie`, `has`, `set`, `forEach`, `keys`, `values`, `entries`, and
|
||||
`[Symbol.iterator]`; iteration is live and sorted by name, with `set-cookie` values kept apart.
|
||||
- [x] Headers serialize to a `{ name: value }` object in JSON, in results, and in tool arguments.
|
||||
- [ ] `Request`, `Response`, and `Blob`.
|
||||
- [ ] `crypto.subtle` and `TextDecoder` streaming or non-UTF-8 encodings.
|
||||
|
||||
## Extensions
|
||||
|
||||
@@ -461,9 +482,10 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] Each global is a function, callable but not constructible, run with `this` undefined. A global that shadows
|
||||
a built-in or another extension throws at `make`.
|
||||
- [x] Every value crossing in either direction is converted, never shared: plain objects and arrays are copied,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Map`, `Set`, and `Uint8Array` become fresh copies with their
|
||||
contents converted (a host `ArrayBuffer` comes in as a `Uint8Array`; other typed arrays cannot come out),
|
||||
errors cross as errors with their name and message, and a `__proto__` key is dropped. Functions, generators,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Headers`, `Map`, `Set`, and `Uint8Array` become fresh copies with
|
||||
their contents converted (a host `ArrayBuffer` comes in as a `Uint8Array`; other typed arrays cannot come out),
|
||||
errors cross as errors with their name, message, `cause`, and own enumerable data, and a `__proto__` key is
|
||||
dropped. Functions, generators,
|
||||
un-awaited promises, and symbols cannot be passed in; a class instance, a symbol, or a BigInt cannot come out.
|
||||
- [x] A host function inside a result becomes a program function whose calls cross the same way, so a result can
|
||||
carry methods (`res.json()`) whose host closures keep the host state. Diagnostics name it by its path
|
||||
@@ -488,19 +510,21 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
|
||||
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
|
||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. `message` is an own
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` while the host still
|
||||
receives `{ name, message }`. Errors have no `stack`; the diagnostic carries the source location instead.
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` for a plain error. The
|
||||
result boundary still emits `{ name, message, ...own enumerable }`, so a field such as `code` crosses. `cause` is
|
||||
non-enumerable: an extension Error carries it, and this JSON form does not. Errors have no `stack`; the diagnostic
|
||||
carries a 1-based line and column in the submitted source instead.
|
||||
- [x] `instanceof` against any constructor with a `prototype`, including every built-in and `Function`.
|
||||
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
|
||||
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
|
||||
`catch`.
|
||||
- [x] Source locations on unsupported-syntax diagnostics for JavaScript-shaped input; TypeScript transpilation may
|
||||
shift them. The diagnostic names the rejected node type and attaches a short orientation to the supported
|
||||
subset; this matrix is the full reference.
|
||||
- [x] Source locations on unsupported-syntax diagnostics. The diagnostic names the rejected node type and attaches a
|
||||
short orientation to the supported subset; this matrix is the full reference.
|
||||
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
|
||||
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
|
||||
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
|
||||
This is deliberate: the program should handle a failure the same way regardless of where it originated.
|
||||
Error-shaped value in `catch`, rejection handlers, and `Promise.allSettled` reasons. It always has `name` and
|
||||
`message`, plus `cause` and own data when the failure carried them. This is deliberate: the program should
|
||||
handle a failure the same way regardless of where it originated.
|
||||
- [x] Failures raised by the interpreter are `TypeError`s unless JavaScript names them otherwise (`RangeError`,
|
||||
`ReferenceError`, `SyntaxError`, `URIError`), so `e instanceof TypeError` and `e.constructor === TypeError`
|
||||
hold. Unsupported syntax reached at runtime is a `SyntaxError`; awaited tool failures stay plain `Error`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.11",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -19,12 +19,6 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"#transpile": {
|
||||
"workerd": "./src/interpreter/transpile.workerd.ts",
|
||||
"default": "./src/interpreter/transpile.node.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run script/build.ts",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -32,8 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
|
||||
@@ -15,7 +15,7 @@ const pkg = JSON.parse(originalText) as {
|
||||
name: string
|
||||
version: string
|
||||
exports: Record<string, string | { import: string; types: string }>
|
||||
imports: Record<string, Record<string, string>>
|
||||
imports?: Record<string, Record<string, string>>
|
||||
}
|
||||
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
|
||||
const output = (value: string, types = false) =>
|
||||
@@ -41,12 +41,14 @@ try {
|
||||
]
|
||||
}),
|
||||
)
|
||||
pkg.imports = Object.fromEntries(
|
||||
Object.entries(pkg.imports).map(([key, conditions]) => [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(conditions).map(([condition, value]) => [condition, output(value)])),
|
||||
]),
|
||||
)
|
||||
if (pkg.imports) {
|
||||
pkg.imports = Object.fromEntries(
|
||||
Object.entries(pkg.imports).map(([key, conditions]) => [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(conditions).map(([condition, value]) => [condition, output(value)])),
|
||||
]),
|
||||
)
|
||||
}
|
||||
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||
await rm(tarball, { force: true })
|
||||
await $`bun pm pack`
|
||||
|
||||
@@ -15,21 +15,23 @@ import {
|
||||
Obj,
|
||||
PromiseObj,
|
||||
record,
|
||||
type Value,
|
||||
SetObj,
|
||||
URLSearchParamsObj,
|
||||
HeadersObj,
|
||||
} from "./interpreter/objects.js"
|
||||
import { typeofValue } from "./interpreter/references.js"
|
||||
|
||||
export type Json = Schema.Json
|
||||
|
||||
type Replacer<R> = (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
type Replacer<R> = (args: Array<Value>) => Effect.Effect<Value, unknown, R>
|
||||
|
||||
/**
|
||||
* What `JSON.stringify` would serialize for a program value, as host JSON: `toJSON` is honored, functions and
|
||||
* `undefined` vanish, non-finite numbers become null, and everything else is copied. Two departures from JS
|
||||
* so a mistake is not a silent `{}`: an Error serializes as `{ name, message, ...own }`, and a promise throws.
|
||||
*/
|
||||
export const toJson = <R>(ctx: Interpreter<R>, value: unknown, replacer?: Replacer<R>) =>
|
||||
export const toJson = <R>(ctx: Interpreter<R>, value: Value, replacer?: Replacer<R>) =>
|
||||
walk(ctx, value, replacer, false)
|
||||
|
||||
/**
|
||||
@@ -37,11 +39,11 @@ export const toJson = <R>(ctx: Interpreter<R>, value: unknown, replacer?: Replac
|
||||
* awaited, a Set crosses as an array, a URLSearchParams as its query string, a Uint8Array asks to be encoded as
|
||||
* text first, and a `__proto__` key is dropped so host code can never receive one.
|
||||
*/
|
||||
export const toBoundary = <R>(ctx: Interpreter<R>, value: unknown) => walk(ctx, value, undefined, true)
|
||||
export const toBoundary = <R>(ctx: Interpreter<R>, value: Value) => walk(ctx, value, undefined, true)
|
||||
|
||||
const walk = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
replacer: Replacer<R> | undefined,
|
||||
boundary: boolean,
|
||||
): Effect.Effect<Json | undefined, unknown, R> => {
|
||||
@@ -69,6 +71,7 @@ const walk = <R>(
|
||||
)
|
||||
}
|
||||
if (boundary && value instanceof URLSearchParamsObj) return value.params.toString()
|
||||
if (value instanceof HeadersObj) return Object.fromEntries(value.headers)
|
||||
const target = boundary && value instanceof SetObj ? new Arr(ctx.builtins.Array, [...value.set]) : value
|
||||
if (stack.has(target)) throw typeError("Converting circular structure to JSON.")
|
||||
stack.add(target)
|
||||
@@ -102,7 +105,7 @@ const walk = <R>(
|
||||
}
|
||||
|
||||
/** Host JSON as program values: objects and arrays are copied, primitives pass through. */
|
||||
export const fromJson = <R>(ctx: Interpreter<R>, value: unknown): unknown => {
|
||||
export const fromJson = <R>(ctx: Interpreter<R>, value: Json | undefined): Value => {
|
||||
if (value === null || typeof value !== "object") return value
|
||||
if (Array.isArray(value))
|
||||
return new Arr(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Effect, Exit } from "effect"
|
||||
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { primitivePrototype } from "./intrinsics.js"
|
||||
import { typeError } from "./model.js"
|
||||
import { Callable, get, Native, DateObj, Obj } from "./objects.js"
|
||||
import { Callable, get, Native, DateObj, Obj, coerceToNumber, coerceToString, type Value } from "./objects.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
|
||||
export type IteratorCursor<R> = {
|
||||
readonly next: Effect.Effect<{ readonly done: boolean; readonly value: unknown }, unknown, R>
|
||||
readonly next: Effect.Effect<{ readonly done: boolean; readonly value: Value }, unknown, R>
|
||||
readonly close: Effect.Effect<void, unknown, R>
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ export const preserveConsumerError = <A, R>(
|
||||
*/
|
||||
export const toPrimitive = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
hint: "number" | "string" | "default",
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
): Effect.Effect<Value, unknown, R> => {
|
||||
if (!(value instanceof Obj)) return Effect.succeed(value)
|
||||
const asString = hint === "string" || (hint === "default" && value instanceof DateObj)
|
||||
const order = asString ? ["toString", "valueOf"] : ["valueOf", "toString"]
|
||||
@@ -43,24 +43,31 @@ export const toPrimitive = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
/** Invoke(value, name): calls the method the value would find through its prototype. */
|
||||
export const invoke = <R>(ctx: Interpreter<R>, value: Value, name: string, label: string) => {
|
||||
const target = value instanceof Obj ? value : primitivePrototype(ctx.builtins, value)
|
||||
if (target === undefined) throw typeError(`${label} called on null or undefined.`)
|
||||
return ctx.call(get(target, name), value, [])
|
||||
}
|
||||
|
||||
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: Value) =>
|
||||
Effect.map(toPrimitive(ctx, value, "string"), coerceToString)
|
||||
|
||||
export const toPrimitiveNumber = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
export const toPrimitiveNumber = <R>(ctx: Interpreter<R>, value: Value) =>
|
||||
Effect.map(toPrimitive(ctx, value, "number"), coerceToNumber)
|
||||
|
||||
// The single acceptance list for callbacks: collections, sort, string replacers,
|
||||
// Array.from mappers, and promise reactions all admit exactly these callables.
|
||||
// Admission means dispatchable, not necessarily invocable: new-requiring
|
||||
// constructors pass the gate and throw a TypeError on call, like JS.
|
||||
export const isSupportedCallback = (value: unknown): value is Callable =>
|
||||
export const isSupportedCallback = (value: Value): value is Callable =>
|
||||
value instanceof Callable && !(value instanceof Native && !value.callback)
|
||||
|
||||
export const applyCollectionCallback = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
callback: unknown,
|
||||
callback: Value,
|
||||
name: string,
|
||||
): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
|
||||
): ((args: Array<Value>) => Effect.Effect<Value, unknown, R>) => {
|
||||
if (!isSupportedCallback(callback)) {
|
||||
if (typeofValue(callback) === "function") {
|
||||
throw typeError(
|
||||
|
||||
@@ -6,10 +6,21 @@ import { type AstNode, formatLocation, PendingThrow, Throw, sourceLocation, type
|
||||
import { containsRuntimeReference } from "./references.js"
|
||||
import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js"
|
||||
import { constructor, methods, prototypeFrom, receiver } from "./native.js"
|
||||
import { type Callable, define, get, has, hidden, type Native, Arr, ErrorObj, Obj } from "./objects.js"
|
||||
import {
|
||||
type Callable,
|
||||
define,
|
||||
get,
|
||||
has,
|
||||
hidden,
|
||||
type Native,
|
||||
Arr,
|
||||
ErrorObj,
|
||||
Obj,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "./objects.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { formatValue } from "../stdlib/console.js"
|
||||
import { coerceToString } from "../stdlib/value.js"
|
||||
|
||||
export const normalizeError = (error: unknown): Diagnostic => {
|
||||
if (error instanceof PendingThrow) {
|
||||
@@ -86,7 +97,7 @@ export const locate = (error: unknown, node?: AstNode): unknown => {
|
||||
}
|
||||
|
||||
/** The program value a handler receives for a failure; one failure always yields the same value. */
|
||||
export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): unknown => {
|
||||
export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): Value => {
|
||||
if (thrown instanceof Throw) return thrown.value
|
||||
const builtins = ctx.builtins
|
||||
if (thrown instanceof PendingThrow) {
|
||||
@@ -113,7 +124,7 @@ const errorToString = (self: Obj): string => {
|
||||
|
||||
export const createAggregateErrorValue = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
errors: Array<unknown>,
|
||||
errors: Array<Value>,
|
||||
message: string,
|
||||
proto: Obj = ctx.builtins.AggregateError,
|
||||
) => {
|
||||
@@ -124,13 +135,13 @@ export const createAggregateErrorValue = <R>(
|
||||
|
||||
const constructAggregateErrorValue = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
args: Array<unknown>,
|
||||
args: Array<Value>,
|
||||
proto: Obj,
|
||||
): Effect.Effect<ErrorObj, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(args[0])
|
||||
if (cursor === undefined) throw typeError("new AggregateError(...) expects a synchronous iterable of errors.")
|
||||
const errors: Array<unknown> = []
|
||||
const errors: Array<Value> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) {
|
||||
@@ -144,7 +155,7 @@ const constructAggregateErrorValue = <R>(
|
||||
export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const prototype = builtins[type]
|
||||
const construct = (args: Array<unknown>, newTarget: Callable) => {
|
||||
const construct = (args: Array<Value>, newTarget: Callable) => {
|
||||
const proto = prototypeFrom(newTarget, prototype)
|
||||
const created =
|
||||
type === "AggregateError"
|
||||
@@ -168,6 +179,7 @@ export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
|
||||
methods(builtins, prototype, [
|
||||
["toString", 0, (thisValue) => errorToString(receiver(Obj, thisValue, "Error.prototype.toString"))],
|
||||
])
|
||||
methods(builtins, ctor, [["isError", 1, (_, args) => args[0] instanceof ErrorObj]])
|
||||
}
|
||||
return ctor
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { parse, type Program } from "acorn"
|
||||
import { Cause, Effect, Scope } from "effect"
|
||||
// #transpile: conditional import — full typescript on node/bun, an identity
|
||||
// pass-through on workerd (the compiler is ~11 MiB and can't init there).
|
||||
import { transpile } from "#transpile"
|
||||
import type { DataValue, Diagnostic, ResolvedExecutionLimits, Result } from "../codemode.js"
|
||||
import { toBoundary } from "../data.js"
|
||||
import { ToolRuntime } from "../tool-runtime.js"
|
||||
import { normalizeError } from "./errors.js"
|
||||
import type { Value } from "./objects.js"
|
||||
import { createBuiltins } from "./intrinsics.js"
|
||||
import { PendingThrow } from "./model.js"
|
||||
import { Pending } from "./promises.js"
|
||||
import { Interpreter } from "./interpreter.js"
|
||||
|
||||
@@ -17,7 +14,7 @@ export const executeProgram = <R>(
|
||||
prepared: ToolRuntime.Prepared<R>,
|
||||
limits: ResolvedExecutionLimits,
|
||||
hooks: ToolRuntime.Hooks<R>,
|
||||
globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>,
|
||||
globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, Value]>,
|
||||
): Effect.Effect<Result, never, R> => {
|
||||
if (code.trim().length === 0) {
|
||||
return Effect.succeed({
|
||||
@@ -110,16 +107,7 @@ export const executeProgram = <R>(
|
||||
}
|
||||
|
||||
const parseProgram = (code: string): Program => {
|
||||
const transpiled = transpile(`async function __codemode__() {\n${code}\n}`)
|
||||
|
||||
if (transpiled.error !== undefined) {
|
||||
throw new PendingThrow("SyntaxError", `Failed to parse TypeScript: ${transpiled.error}`, undefined, "ParseError")
|
||||
}
|
||||
|
||||
const bodyStart = transpiled.outputText.indexOf("{") + 1
|
||||
const bodyEnd = transpiled.outputText.lastIndexOf("}")
|
||||
const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd)
|
||||
return parse(executableCode, {
|
||||
return parse(code, {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "script",
|
||||
allowReturnOutsideFunction: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Extension } from "../extension.js"
|
||||
import { coerceToString } from "../stdlib/value.js"
|
||||
import { type ExtensionInvocation, hooked } from "../tool-runtime.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { createErrorValue, isErrorType } from "./intrinsics.js"
|
||||
@@ -8,9 +7,7 @@ import { MAX_VALUE_DEPTH } from "./limits.js"
|
||||
import { PendingThrow, Throw, typeError } from "./model.js"
|
||||
import { fn } from "./native.js"
|
||||
import {
|
||||
Callable,
|
||||
define,
|
||||
entries,
|
||||
get,
|
||||
has,
|
||||
hidden,
|
||||
@@ -19,16 +16,17 @@ import {
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
GeneratorObj,
|
||||
HeadersObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "./objects.js"
|
||||
import { describeValue } from "./references.js"
|
||||
import { describeValue, isOpaque } from "./references.js"
|
||||
|
||||
/**
|
||||
* The global bindings of one run's extensions. Everything crossing the boundary is converted: plain data and
|
||||
@@ -38,33 +36,18 @@ import { describeValue } from "./references.js"
|
||||
export const extensionGlobals = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
extensions: ReadonlyArray<Extension>,
|
||||
): ReadonlyArray<readonly [string, unknown]> => {
|
||||
): ReadonlyArray<readonly [string, Value]> => {
|
||||
const builtins = ctx.builtins
|
||||
|
||||
const toHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
|
||||
const toHost = (value: Value, label: string, depth = 0, seen = new Set<object>()): unknown => {
|
||||
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||
if (value === null || typeof value !== "object") {
|
||||
if (isPrimitive(value)) return value
|
||||
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
|
||||
}
|
||||
if (value instanceof Bytes) return new Uint8Array(value.bytes)
|
||||
if (value instanceof DateObj) return new Date(value.time)
|
||||
if (value instanceof RegExpObj) return new RegExp(value.regex.source, value.regex.flags)
|
||||
if (value instanceof URLObj) return new URL(value.url.href)
|
||||
if (value instanceof URLSearchParamsObj) return new URLSearchParams(value.params)
|
||||
const next = (item: unknown) => toHost(item, label, depth + 1, seen)
|
||||
if (value instanceof MapObj) return new Map([...value.map].map(([key, item]) => [next(key), next(item)]))
|
||||
if (value instanceof SetObj) return new Set([...value.set].map(next))
|
||||
if (
|
||||
!(value instanceof Obj) ||
|
||||
value instanceof Callable ||
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof PromiseObj
|
||||
) {
|
||||
if (isPrimitive(value)) return value
|
||||
if (!(value instanceof Obj) || isOpaque(value)) {
|
||||
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
|
||||
}
|
||||
if (seen.has(value)) throw typeError(`${label} contains a circular value.`)
|
||||
seen.add(value)
|
||||
const next = (item: Value) => toHost(item, label, depth + 1, seen)
|
||||
if (value instanceof ErrorObj) {
|
||||
const name = coerceToString(get(value, "name"))
|
||||
const message = get(value, "message")
|
||||
@@ -85,19 +68,12 @@ export const extensionGlobals = <R>(
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
const copied =
|
||||
value instanceof Arr
|
||||
? value.items.map(next)
|
||||
: Object.fromEntries(
|
||||
entries(value)
|
||||
.filter(([key]) => key !== "__proto__")
|
||||
.map(([key, item]) => [key, next(item)]),
|
||||
)
|
||||
const copied = value.toHost(next)
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
|
||||
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
|
||||
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): Value => {
|
||||
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||
if (isPrimitive(value)) return value
|
||||
if (typeof value === "function") return wrap(value, label)
|
||||
@@ -125,6 +101,7 @@ export const extensionGlobals = <R>(
|
||||
if (value instanceof URLSearchParams) {
|
||||
return new URLSearchParamsObj(builtins.URLSearchParams, new URLSearchParams(value))
|
||||
}
|
||||
if (value instanceof Headers) return new HeadersObj(builtins.Headers, new Headers(value))
|
||||
if (value instanceof Map) {
|
||||
const wrapped = new MapObj(builtins.Map)
|
||||
for (const [key, item] of value) wrapped.map.set(next(key, label), next(item, label))
|
||||
@@ -195,7 +172,7 @@ export const extensionGlobals = <R>(
|
||||
*/
|
||||
const uncrossed = new Set(["stack", "constructor", "toString", "__proto__"])
|
||||
const left = Symbol("left behind")
|
||||
const crossing = (convert: () => unknown): unknown => {
|
||||
const crossing = <T>(convert: () => T): T | typeof left => {
|
||||
try {
|
||||
return convert()
|
||||
} catch (reason) {
|
||||
@@ -214,7 +191,7 @@ const hostErrors = new Map<string, ErrorConstructor>([
|
||||
])
|
||||
|
||||
// The primitives the interpreter operates on; symbols and BigInts are not among them.
|
||||
const isPrimitive = (value: unknown): boolean =>
|
||||
const isPrimitive = (value: unknown): value is string | number | boolean | null | undefined =>
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
typeof value === "string" ||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { fn, type Method, methods, receiver } from "./native.js"
|
||||
import { AsyncIteratorSymbol, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
|
||||
import { define, hidden, GeneratorObj } from "./objects.js"
|
||||
import { define, hidden, GeneratorObj, type Value } from "./objects.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
|
||||
/** `next`/`return`/`throw` on the generator prototypes; async generators answer with promises. */
|
||||
@@ -13,9 +13,9 @@ export const generatorGlobals = <R>(ctx: Interpreter<R>): void => {
|
||||
const request = (kind: GeneratorRequestKind): Method => [
|
||||
kind,
|
||||
1,
|
||||
(thisValue: unknown, args: Array<unknown>) => {
|
||||
(thisValue: Value, args: Array<Value>) => {
|
||||
const generator = receiver(GeneratorObj, thisValue, `${label}.prototype.${kind}`)
|
||||
const requested = generator.request(kind, args[0]) as Effect.Effect<unknown, unknown, R>
|
||||
const requested = generator.request(kind, args[0]) as Effect.Effect<Value, unknown, R>
|
||||
return generator.asynchronous ? ctx.pending.create(requested) : requested
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Value } from "./objects.js"
|
||||
import { arrayGlobal } from "../stdlib/array.js"
|
||||
import { textDecoderGlobal, textEncoderGlobal, uint8ArrayGlobal } from "../stdlib/bytes.js"
|
||||
import { mapGlobal, setGlobal } from "../stdlib/collections.js"
|
||||
@@ -11,6 +12,8 @@ import { objectGlobal } from "../stdlib/object.js"
|
||||
import { regexpGlobal } from "../stdlib/regexp.js"
|
||||
import { stringGlobal } from "../stdlib/string.js"
|
||||
import { uriGlobal, urlGlobal, urlSearchParamsGlobal } from "../stdlib/url.js"
|
||||
import { headersGlobal } from "../stdlib/headers.js"
|
||||
import { iteratorGlobals } from "../stdlib/iterator.js"
|
||||
import { coercion } from "../stdlib/value.js"
|
||||
import { base64Global, cryptoGlobal } from "../stdlib/web.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
@@ -49,7 +52,7 @@ const symbolGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return symbol
|
||||
}
|
||||
|
||||
type Factory = <R>(ctx: Interpreter<R>) => unknown
|
||||
type Factory = <R>(ctx: Interpreter<R>) => Value
|
||||
|
||||
// A table rather than a list so the names are known before any runtime exists.
|
||||
const table: Record<string, Factory> = {
|
||||
@@ -80,6 +83,7 @@ const table: Record<string, Factory> = {
|
||||
Set: (ctx) => setGlobal(ctx),
|
||||
URL: (ctx) => urlGlobal(ctx),
|
||||
URLSearchParams: (ctx) => urlSearchParamsGlobal(ctx),
|
||||
Headers: (ctx) => headersGlobal(ctx),
|
||||
Uint8Array: (ctx) => uint8ArrayGlobal(ctx),
|
||||
TextEncoder: (ctx) => textEncoderGlobal(ctx),
|
||||
TextDecoder: (ctx) => textDecoderGlobal(ctx),
|
||||
@@ -97,7 +101,8 @@ const table: Record<string, Factory> = {
|
||||
export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
|
||||
|
||||
/** The immutable global bindings of every program, in declaration order. */
|
||||
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, unknown]> => {
|
||||
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, Value]> => {
|
||||
generatorGlobals(ctx)
|
||||
iteratorGlobals(ctx)
|
||||
return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
Statement,
|
||||
Super,
|
||||
SwitchStatement,
|
||||
Literal,
|
||||
TemplateLiteral,
|
||||
ThrowStatement,
|
||||
TryStatement,
|
||||
@@ -62,7 +63,7 @@ import {
|
||||
} from "./model.js"
|
||||
import { checkStringLength } from "./limits.js"
|
||||
import { locate, materialize } from "./errors.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { type Builtins, primitivePrototype } from "./intrinsics.js"
|
||||
import { globals } from "./globals.js"
|
||||
import {
|
||||
assign,
|
||||
@@ -75,18 +76,16 @@ import {
|
||||
Native,
|
||||
parseArrayIndex,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
Fn,
|
||||
GeneratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
SetObj,
|
||||
URLSearchParamsObj,
|
||||
record,
|
||||
remove,
|
||||
set,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "./objects.js"
|
||||
import { preserveConsumerError } from "./callback.js"
|
||||
import { Pending, resolvePromise, resolvePromiseValue } from "./promises.js"
|
||||
@@ -94,7 +93,7 @@ import { containsOpaqueReference, describeValue, rejectCircularInsertion, typeof
|
||||
import { ScopeStack } from "./scope.js"
|
||||
import { constructRegExp } from "../stdlib/regexp.js"
|
||||
import { enumerableSource } from "../stdlib/object.js"
|
||||
import { coerceToNumber, coerceToString, compoundOperators } from "../stdlib/value.js"
|
||||
import { compoundOperators } from "../stdlib/value.js"
|
||||
|
||||
// What a loop does with its body's result: exit with a StatementResult, or undefined to keep iterating.
|
||||
// Unlabelled break ends this loop; a label the loop does not carry propagates outward.
|
||||
@@ -108,24 +107,36 @@ const loopExit = (result: StatementResult, labels: ReadonlySet<string> | undefin
|
||||
return undefined
|
||||
}
|
||||
|
||||
const calleeDescription = (callee: Expression | Super | undefined): string => {
|
||||
if (callee?.type === "Identifier") return callee.name
|
||||
if (callee?.type === "MemberExpression") {
|
||||
const object = callee.object
|
||||
const property = callee.property
|
||||
const key =
|
||||
!callee.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (object.type === "Identifier" && key !== undefined) return `${object.name}.${key}`
|
||||
// Native engines name the callee (`search(...).catch is not a function`), including call chains. Returns
|
||||
// undefined when a link cannot be named, so a chain is either named completely or not at all.
|
||||
const calleeDescription = (node: Expression | Super | undefined): string | undefined => {
|
||||
if (node?.type === "Identifier") return node.name
|
||||
if (node?.type === "CallExpression") {
|
||||
const target = calleeDescription(node.callee)
|
||||
return target === undefined ? undefined : `${target}(...)`
|
||||
}
|
||||
return "The called value"
|
||||
if (node?.type !== "MemberExpression") return undefined
|
||||
const property = node.property
|
||||
const key =
|
||||
!node.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (key === undefined) return undefined
|
||||
const object = calleeDescription(node.object)
|
||||
return object === undefined ? undefined : `${object}.${key}`
|
||||
}
|
||||
|
||||
// OrdinaryHasInstance: walk the left operand's chain looking for the constructor's `prototype`.
|
||||
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
|
||||
// acorn types every literal as possibly a BigInt or RegExp; regex literals become RegExp objects before this is asked.
|
||||
const literal = (node: Literal): Value => {
|
||||
if (typeof node.value === "bigint") throw typeError("BigInt literals are not supported.", node)
|
||||
if (node.value instanceof RegExp) throw unsupportedSyntax("RegExpLiteral", node)
|
||||
return node.value
|
||||
}
|
||||
|
||||
const instanceofValue = (lhs: Value, rhs: Value, node: AstNode): boolean => {
|
||||
if (!(rhs instanceof Callable)) {
|
||||
throw typeError("The right-hand side of 'instanceof' is not callable.", node)
|
||||
}
|
||||
@@ -222,7 +233,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
|
||||
|
||||
type CustomIterator = {
|
||||
iterator: Obj
|
||||
next: unknown
|
||||
next: Value
|
||||
asynchronous: boolean
|
||||
}
|
||||
|
||||
@@ -230,13 +241,13 @@ type CustomIterator = {
|
||||
type MemberReference = {
|
||||
target: Obj
|
||||
key: PropertyKey
|
||||
receiver: unknown
|
||||
receiver: Value
|
||||
}
|
||||
|
||||
type GeneratorRequest = {
|
||||
kind: GeneratorRequestKind
|
||||
value: unknown
|
||||
response: Deferred.Deferred<unknown, unknown>
|
||||
value: Value
|
||||
response: Deferred.Deferred<Value, unknown>
|
||||
}
|
||||
|
||||
type GeneratorState = {
|
||||
@@ -262,7 +273,7 @@ export class Interpreter<R> {
|
||||
readonly pending: Pending<R>
|
||||
readonly builtins: Builtins
|
||||
readonly logs?: Array<string>
|
||||
readonly globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>
|
||||
readonly globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, Value]>
|
||||
}) {
|
||||
this.tools = options.tools
|
||||
this.pending = options.pending
|
||||
@@ -276,27 +287,27 @@ export class Interpreter<R> {
|
||||
}
|
||||
}
|
||||
|
||||
run(program: Program): Effect.Effect<unknown, unknown, R> {
|
||||
run(program: Program): Effect.Effect<Value, unknown, R> {
|
||||
return this.root.run(program)
|
||||
}
|
||||
|
||||
call(callable: unknown, thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||
call(callable: Value, thisValue: Value, args: Array<Value>): Effect.Effect<Value, unknown, R> {
|
||||
return this.root.call(callable, thisValue, args)
|
||||
}
|
||||
|
||||
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
|
||||
await(promise: PromiseObj): Effect.Effect<Value, unknown, never> {
|
||||
return this.root.await(promise)
|
||||
}
|
||||
|
||||
iterate(value: unknown) {
|
||||
iterate(value: Value) {
|
||||
return this.root.iterate(value)
|
||||
}
|
||||
|
||||
/** Runs one host tool: arguments cross as JSON and the result comes back as program values. */
|
||||
tool(
|
||||
run: (args: Array<Json | undefined>) => Effect.Effect<Json | undefined, unknown, R>,
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<unknown, unknown, R> {
|
||||
args: Array<Value>,
|
||||
): Effect.Effect<Value, unknown, R> {
|
||||
const ctx = this
|
||||
return Effect.gen(function* () {
|
||||
const json = yield* Effect.forEach(args, (arg) => toBoundary(ctx, arg))
|
||||
@@ -319,7 +330,7 @@ class Frame<R> {
|
||||
private depth = 0,
|
||||
) {}
|
||||
|
||||
run(program: Program): Effect.Effect<unknown, unknown, R> {
|
||||
run(program: Program): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
// Keep top-level declarations separate so they can shadow builtins.
|
||||
this.scopes.push()
|
||||
@@ -327,7 +338,7 @@ class Frame<R> {
|
||||
self.predeclareLexical(program.body)
|
||||
self.hoistFunctions(program.body)
|
||||
self.hoistVars(program.body)
|
||||
let value: unknown = undefined
|
||||
let value: Value = undefined
|
||||
for (const [index, statement] of program.body.entries()) {
|
||||
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
|
||||
value = yield* self.evaluateExpression(statement.expression)
|
||||
@@ -352,15 +363,12 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// Fork at the call site so admission and hooks occur when the call is made.
|
||||
private createToolCallPromise(
|
||||
path: ReadonlyArray<string>,
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<PromiseObj, never, R> {
|
||||
private createToolCallPromise(path: ReadonlyArray<string>, args: Array<Value>): Effect.Effect<PromiseObj, never, R> {
|
||||
return this.ctx.pending.create(this.ctx.tool((json) => this.ctx.tools.execute(path, json), args))
|
||||
}
|
||||
|
||||
// Fiber exits make settlement idempotent; yielding prevents inline continuation.
|
||||
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
|
||||
await(promise: PromiseObj): Effect.Effect<Value, unknown, never> {
|
||||
const pending = this.ctx.pending
|
||||
return Effect.suspend(() => {
|
||||
pending.markObserved(promise)
|
||||
@@ -451,7 +459,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// NamedEvaluation: an anonymous function definition takes the name of what it is assigned to.
|
||||
private evaluateNamed(node: Expression, name: string): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateNamed(node: Expression, name: string): Effect.Effect<Value, unknown, R> {
|
||||
if (node.type === "ArrowFunctionExpression" || (node.type === "FunctionExpression" && !node.id)) {
|
||||
return Effect.sync(() => this.createFunction(node, name))
|
||||
}
|
||||
@@ -649,11 +657,11 @@ class Frame<R> {
|
||||
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
const right = yield* self.evaluateExpression(node.right)
|
||||
|
||||
const iterator = yield* self.customIterator(right, node, awaiting)
|
||||
const cursor = iterator === undefined ? yield* self.iterate(right, node) : undefined
|
||||
const cursor = self.hostCursor(right)
|
||||
const iterator = cursor === undefined ? yield* self.customIterator(right, node, awaiting) : undefined
|
||||
if (iterator === undefined && cursor === undefined) {
|
||||
throw invalidData(
|
||||
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`,
|
||||
`${awaiting ? "for await...of" : "for...of"} requires an iterable value, received ${describeValue(right)}.`,
|
||||
node,
|
||||
)
|
||||
}
|
||||
@@ -669,7 +677,7 @@ class Frame<R> {
|
||||
}
|
||||
const assignment = left.type === "VariableDeclaration" ? undefined : left
|
||||
|
||||
const evaluateBody = (value: unknown) =>
|
||||
const evaluateBody = (value: Value) =>
|
||||
Effect.gen(function* () {
|
||||
if (declared?.lexical) {
|
||||
self.scopes.push()
|
||||
@@ -718,7 +726,7 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
|
||||
private awaitValue(value: unknown): Effect.Effect<unknown, unknown, R> {
|
||||
private awaitValue(value: Value): Effect.Effect<Value, unknown, R> {
|
||||
return Effect.flatMap(resolvePromise(this.ctx, value), (promise) =>
|
||||
Effect.ensuring(
|
||||
this.await(promise),
|
||||
@@ -729,10 +737,10 @@ class Frame<R> {
|
||||
|
||||
private awaitAsyncFromSyncValue(
|
||||
iterator: CustomIterator,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
node: AstNode | undefined,
|
||||
closeOnRejection: boolean,
|
||||
): Effect.Effect<unknown, unknown, R> {
|
||||
): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const settled = yield* Effect.exit(self.awaitValue(value))
|
||||
@@ -744,34 +752,9 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
iterate(value: unknown, node?: AstNode) {
|
||||
const iterator =
|
||||
value instanceof Arr
|
||||
? value.items[Symbol.iterator]()
|
||||
: typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof MapObj
|
||||
? value.map.entries()
|
||||
: value instanceof SetObj
|
||||
? value.set.values()
|
||||
: value instanceof URLSearchParamsObj
|
||||
? value.params.entries()
|
||||
: value instanceof Bytes
|
||||
? value.bytes.values()
|
||||
: undefined
|
||||
if (iterator !== undefined) {
|
||||
const proto = this.ctx.builtins.Array
|
||||
return Effect.succeed({
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return {
|
||||
done: Boolean(step.done),
|
||||
value: Array.isArray(step.value) ? new Arr(proto, step.value) : step.value,
|
||||
}
|
||||
}),
|
||||
close: Effect.void,
|
||||
})
|
||||
}
|
||||
iterate(value: Value, node?: AstNode) {
|
||||
const cursor = this.hostCursor(value)
|
||||
if (cursor !== undefined) return Effect.succeed(cursor)
|
||||
const self = this
|
||||
return Effect.map(this.customIterator(value, node, false), (iterator) =>
|
||||
iterator === undefined
|
||||
@@ -783,7 +766,24 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
|
||||
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
|
||||
private hostCursor(value: Value) {
|
||||
const iterator =
|
||||
typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof Obj
|
||||
? value.iterator(this.ctx.builtins)
|
||||
: undefined
|
||||
if (iterator === undefined) return undefined
|
||||
return {
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return { done: Boolean(step.done), value: step.value }
|
||||
}),
|
||||
close: Effect.void,
|
||||
}
|
||||
}
|
||||
|
||||
private customIterator(value: Value, node: AstNode | undefined, allowAsync = true) {
|
||||
if (!(value instanceof Obj)) return Effect.undefined
|
||||
const asyncMethod = allowAsync ? get(value, AsyncIteratorSymbol) : undefined
|
||||
const method = asyncMethod ?? get(value, IteratorSymbol)
|
||||
@@ -873,18 +873,18 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private requireIteratorObject(value: unknown, context: string, node?: AstNode): Obj {
|
||||
private requireIteratorObject(value: Value, context: string, node?: AstNode): Obj {
|
||||
if (value instanceof Obj) return value
|
||||
throw typeError(`${context} must be an object.`, node)
|
||||
}
|
||||
|
||||
private requireIteratorMethod(value: unknown, context: string, node?: AstNode): unknown {
|
||||
private requireIteratorMethod(value: Value, context: string, node?: AstNode): Value {
|
||||
if (typeofValue(value) === "function") return value
|
||||
throw typeError(`${context} must be a function.`, node)
|
||||
}
|
||||
|
||||
// for...in over null/undefined iterates nothing, like JS.
|
||||
private enumerableKeys(value: unknown, node: AstNode): Array<string> {
|
||||
private enumerableKeys(value: Value, node: AstNode): Array<string> {
|
||||
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
|
||||
if (value === null || value === undefined) return []
|
||||
return keys(enumerableSource(this.ctx, "for...in", value, node))
|
||||
@@ -1048,7 +1048,7 @@ class Frame<R> {
|
||||
|
||||
private declarePattern(
|
||||
pattern: Pattern,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
mutable: boolean,
|
||||
node: AstNode,
|
||||
initialize = false,
|
||||
@@ -1108,7 +1108,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private assignPattern(pattern: Pattern, value: unknown, node: AstNode): Effect.Effect<void, unknown, R> {
|
||||
private assignPattern(pattern: Pattern, value: Value, node: AstNode): Effect.Effect<void, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (pattern.type === "Identifier") {
|
||||
@@ -1160,7 +1160,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateDefault(pattern: AssignmentPattern): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateDefault(pattern: AssignmentPattern): Effect.Effect<Value, unknown, R> {
|
||||
return pattern.left.type === "Identifier"
|
||||
? this.evaluateNamed(pattern.right, pattern.left.name)
|
||||
: this.evaluateExpression(pattern.right)
|
||||
@@ -1168,8 +1168,8 @@ class Frame<R> {
|
||||
|
||||
private destructureArrayPattern(
|
||||
pattern: ArrayPattern,
|
||||
value: unknown,
|
||||
consume: (target: Pattern, value: unknown, context: AstNode) => Effect.Effect<void, unknown, R>,
|
||||
value: Value,
|
||||
consume: (target: Pattern, value: Value, context: AstNode) => Effect.Effect<void, unknown, R>,
|
||||
): Effect.Effect<void, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
@@ -1193,7 +1193,7 @@ class Frame<R> {
|
||||
done = step.done
|
||||
if (element === null) continue
|
||||
if (element.type === "RestElement") {
|
||||
const rest: Array<unknown> = []
|
||||
const rest: Array<Value> = []
|
||||
if (!step.done) rest.push(step.value)
|
||||
while (!done) {
|
||||
const next = yield* cursor.next
|
||||
@@ -1223,13 +1223,12 @@ class Frame<R> {
|
||||
throw unsupportedSyntax(keyNode.type, keyNode)
|
||||
}
|
||||
|
||||
private evaluateExpression(node: Expression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateExpression(node: Expression): Effect.Effect<Value, unknown, R> {
|
||||
switch (node.type) {
|
||||
case "Literal": {
|
||||
const regex = node.regex
|
||||
if (regex) return Effect.sync(() => constructRegExp(this.ctx.builtins, [regex.pattern, regex.flags]))
|
||||
if (typeof node.value === "bigint") throw typeError("BigInt literals are not supported.", node)
|
||||
return Effect.succeed(node.value)
|
||||
return Effect.succeed(literal(node))
|
||||
}
|
||||
case "Identifier":
|
||||
return Effect.sync(() => this.scopes.get(node.name, node))
|
||||
@@ -1244,7 +1243,7 @@ class Frame<R> {
|
||||
case "SequenceExpression": {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
let result: unknown
|
||||
let result: Value
|
||||
for (const expression of node.expressions) {
|
||||
result = yield* self.evaluateExpression(expression)
|
||||
}
|
||||
@@ -1285,7 +1284,7 @@ class Frame<R> {
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateNewExpression(node: NewExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateNewExpression(node: NewExpression): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const callee = yield* self.evaluateExpression(node.callee)
|
||||
@@ -1295,7 +1294,7 @@ class Frame<R> {
|
||||
// `new` itself is supported, so a non-constructible callee is a TypeError like JS rather than
|
||||
// unsupported syntax. Built-ins like Number are real constructors in JS, so do not claim
|
||||
// otherwise; say `new` is unsupported for them and point at the plain call.
|
||||
const name = calleeDescription(node.callee)
|
||||
const name = calleeDescription(node.callee) ?? "The called value"
|
||||
const message =
|
||||
callee instanceof Fn
|
||||
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
|
||||
@@ -1309,7 +1308,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateBinaryExpression(node: BinaryExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateBinaryExpression(node: BinaryExpression): Effect.Effect<Value, unknown, R> {
|
||||
const operator = node.operator
|
||||
const left = node.left
|
||||
if (left.type === "PrivateIdentifier") throw unsupportedSyntax(left.type, left)
|
||||
@@ -1322,7 +1321,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
|
||||
private applyBinaryOperator(operator: string, lhs: Value, rhs: Value, node: AstNode): Value {
|
||||
if (operator === "===") return lhs === rhs
|
||||
if (operator === "!==") return lhs !== rhs
|
||||
if (operator === "in" && rhs instanceof Obj && !containsOpaqueReference(lhs)) {
|
||||
@@ -1331,14 +1330,9 @@ class Frame<R> {
|
||||
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
|
||||
throw invalidData("Binary operators require data values.", node)
|
||||
}
|
||||
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
|
||||
// Dates use their default string hint for addition and loose equality, and epoch time elsewhere.
|
||||
const coerceOperand = (operand: unknown): unknown => {
|
||||
if (operand instanceof DateObj) {
|
||||
return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time
|
||||
}
|
||||
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
|
||||
}
|
||||
// Addition and loose equality use the default hint; every other operator asks for a number.
|
||||
const hint = operator === "+" || operator === "==" || operator === "!=" ? "default" : "number"
|
||||
const coerceOperand = (operand: Value) => (operand instanceof Obj ? operand.toPrimitive(hint) : operand)
|
||||
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
|
||||
const l = coerceOperand(lhs)
|
||||
const r = coerceOperand(rhs)
|
||||
@@ -1392,7 +1386,7 @@ class Frame<R> {
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateLogicalExpression(node: LogicalExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateLogicalExpression(node: LogicalExpression): Effect.Effect<Value, unknown, R> {
|
||||
const operator = node.operator
|
||||
return Effect.flatMap(this.evaluateExpression(node.left), (left) => {
|
||||
if (operator === "&&") return left ? this.evaluateExpression(node.right) : Effect.succeed(left)
|
||||
@@ -1403,7 +1397,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateUnaryExpression(node: UnaryExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateUnaryExpression(node: UnaryExpression): Effect.Effect<Value, unknown, R> {
|
||||
const operator = node.operator
|
||||
const argument = node.argument
|
||||
if (operator === "delete") return this.evaluateDeleteExpression(argument)
|
||||
@@ -1418,13 +1412,8 @@ class Frame<R> {
|
||||
if (containsOpaqueReference(value)) {
|
||||
throw invalidData("Unary operators require data values.", node)
|
||||
}
|
||||
const operand =
|
||||
value instanceof DateObj
|
||||
? value.time
|
||||
: value !== null && typeof value === "object"
|
||||
? coerceToString(value)
|
||||
: value
|
||||
let result: unknown
|
||||
const operand = value instanceof Obj ? value.toPrimitive("number") : value
|
||||
let result: Value
|
||||
switch (operator) {
|
||||
case "+":
|
||||
result = +(operand as number)
|
||||
@@ -1442,7 +1431,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateAssignmentExpression(node: AssignmentExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateAssignmentExpression(node: AssignmentExpression): Effect.Effect<Value, unknown, R> {
|
||||
const left = node.left
|
||||
const operator = node.operator
|
||||
const self = this
|
||||
@@ -1482,9 +1471,9 @@ class Frame<R> {
|
||||
node: AssignmentExpression,
|
||||
left: Pattern,
|
||||
operator: string,
|
||||
): Effect.Effect<unknown, unknown, R> {
|
||||
): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
const shouldAssign = (current: unknown): boolean =>
|
||||
const shouldAssign = (current: Value): boolean =>
|
||||
operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current)
|
||||
if (left.type === "Identifier") {
|
||||
const name = left.name
|
||||
@@ -1509,7 +1498,7 @@ class Frame<R> {
|
||||
throw typeError("Assignment target must be an Identifier or MemberExpression.", left)
|
||||
}
|
||||
|
||||
private evaluateUpdateExpression(node: UpdateExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateUpdateExpression(node: UpdateExpression): Effect.Effect<Value, unknown, R> {
|
||||
const operator = node.operator
|
||||
const argument = node.argument
|
||||
const prefix = node.prefix
|
||||
@@ -1522,7 +1511,7 @@ class Frame<R> {
|
||||
|
||||
// CodeMode numeric coercion, not host Number(): null-prototype data objects would make
|
||||
// the host throw during ToPrimitive, and opaque runtime references must reject clearly.
|
||||
const operand = (current: unknown): number => {
|
||||
const operand = (current: Value): number => {
|
||||
if (containsOpaqueReference(current)) {
|
||||
throw invalidData(`'${operator}' requires a data value.`, argument)
|
||||
}
|
||||
@@ -1551,7 +1540,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// EvaluateCall: a member callee supplies its base object as `this`; anything else calls with undefined.
|
||||
private evaluateCallExpression(node: CallExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateCallExpression(node: CallExpression): Effect.Effect<Value, unknown, R> {
|
||||
const callee = node.callee
|
||||
|
||||
const self = this
|
||||
@@ -1569,7 +1558,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private readMethod(node: MemberExpression): Effect.Effect<{ callable: unknown; thisValue: unknown }, unknown, R> {
|
||||
private readMethod(node: MemberExpression): Effect.Effect<{ callable: Value; thisValue: Value }, unknown, R> {
|
||||
return Effect.map(this.getMemberReference(node), (reference) => {
|
||||
if (reference === OptionalShortCircuit) return { callable: OptionalShortCircuit, thisValue: undefined }
|
||||
if (reference instanceof ToolReference) return { callable: reference, thisValue: undefined }
|
||||
@@ -1580,12 +1569,12 @@ class Frame<R> {
|
||||
|
||||
// The single dispatch for every invocation: call expressions and callbacks share it.
|
||||
call(
|
||||
callable: unknown,
|
||||
thisValue: unknown,
|
||||
args: Array<unknown>,
|
||||
callable: Value,
|
||||
thisValue: Value,
|
||||
args: Array<Value>,
|
||||
node?: AstNode,
|
||||
callee?: Expression,
|
||||
): Effect.Effect<unknown, unknown, R> {
|
||||
): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (callable instanceof ToolReference) {
|
||||
@@ -1598,12 +1587,12 @@ class Frame<R> {
|
||||
if (callable instanceof Native) {
|
||||
return yield* self.native(() => (callable as Native<R>).call(thisValue, args), node)
|
||||
}
|
||||
throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
|
||||
throw typeError(`${calleeDescription(callee) ?? "The called value"} is not a function.`, callee ?? node)
|
||||
})
|
||||
}
|
||||
|
||||
// Built-ins throw without a location, synchronously or inside their Effect; the call site supplies it.
|
||||
private native(body: () => Effect.Effect<unknown, unknown, R>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
private native(body: () => Effect.Effect<Value, unknown, R>, node?: AstNode): Effect.Effect<Value, unknown, R> {
|
||||
return Effect.provideService(
|
||||
Effect.catchDefect(Effect.suspend(body), (defect) => Effect.die(locate(defect, node))),
|
||||
CallSite,
|
||||
@@ -1613,10 +1602,10 @@ class Frame<R> {
|
||||
|
||||
private evaluateCallArguments(
|
||||
argNodes: ReadonlyArray<Expression | SpreadElement>,
|
||||
): Effect.Effect<Array<unknown>, unknown, R> {
|
||||
): Effect.Effect<Array<Value>, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const args: Array<unknown> = []
|
||||
const args: Array<Value> = []
|
||||
for (const argNode of argNodes) {
|
||||
if (argNode.type === "SpreadElement") {
|
||||
const spread = yield* self.evaluateExpression(argNode.argument)
|
||||
@@ -1636,7 +1625,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
|
||||
invokeFunction(fn: Fn, args: Array<unknown>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
invokeFunction(fn: Fn, args: Array<Value>, node?: AstNode): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.flatMap(CallSite, (site) => {
|
||||
const depth = Math.max(self.depth, site.depth) + 1
|
||||
@@ -1683,16 +1672,16 @@ class Frame<R> {
|
||||
|
||||
private createGenerator(
|
||||
invocation: Frame<R>,
|
||||
run: Effect.Effect<unknown, unknown, R>,
|
||||
run: Effect.Effect<Value, unknown, R>,
|
||||
asynchronous: boolean,
|
||||
): GeneratorObj {
|
||||
const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
|
||||
invocation.generatorState = state
|
||||
invocation.generatorAsync = asynchronous
|
||||
const builtins = this.ctx.builtins
|
||||
const result = (value: unknown, done: boolean) => record(builtins.Object, { value, done })
|
||||
const request = (kind: GeneratorRequestKind, value: unknown) => {
|
||||
const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
|
||||
const result = (value: Value, done: boolean) => record(builtins.Object, { value, done })
|
||||
const request = (kind: GeneratorRequestKind, value: Value) => {
|
||||
const request = { kind, value, response: Deferred.makeUnsafe<Value, unknown>() }
|
||||
if (!asynchronous && state.active) return Effect.die(typeError("Generator is already running."))
|
||||
if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
|
||||
state.started = true
|
||||
@@ -1763,7 +1752,7 @@ class Frame<R> {
|
||||
|
||||
private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
|
||||
const self = this
|
||||
const result = (value: unknown, done: boolean) => record(self.ctx.builtins.Object, { value, done })
|
||||
const result = (value: Value, done: boolean) => record(self.ctx.builtins.Object, { value, done })
|
||||
return Effect.gen(function* () {
|
||||
while (true) {
|
||||
const pending = self.dequeueGeneratorRequest(state)
|
||||
@@ -1809,7 +1798,7 @@ class Frame<R> {
|
||||
return request
|
||||
}
|
||||
|
||||
private evaluateYieldExpression(node: YieldExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateYieldExpression(node: YieldExpression): Effect.Effect<Value, unknown, R> {
|
||||
const argument = node.argument
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
@@ -1824,7 +1813,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
private suspendGenerator(value: Value, node: AstNode): Effect.Effect<Value, unknown, R> {
|
||||
const state = this.generatorState
|
||||
if (!state?.active) throw typeError("Generator has no active request.", node)
|
||||
Deferred.doneUnsafe(state.active.response, Exit.succeed(record(this.ctx.builtins.Object, { value, done: false })))
|
||||
@@ -1839,19 +1828,11 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private delegateYield(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
private delegateYield(value: Value, node: AstNode): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (
|
||||
value instanceof Arr ||
|
||||
typeof value === "string" ||
|
||||
value instanceof MapObj ||
|
||||
value instanceof SetObj ||
|
||||
value instanceof URLSearchParamsObj ||
|
||||
value instanceof Bytes
|
||||
) {
|
||||
const cursor = yield* self.iterate(value, node)
|
||||
if (!cursor) throw typeError("Built-in iterator is unavailable.", node)
|
||||
const cursor = self.hostCursor(value)
|
||||
if (cursor !== undefined) {
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return undefined
|
||||
@@ -1875,7 +1856,7 @@ class Frame<R> {
|
||||
const iterator = yield* self.customIterator(value, node, self.generatorAsync)
|
||||
if (!iterator) throw typeError("yield* requires a compatible iterable value.", node)
|
||||
let kind: GeneratorRequestKind = "next"
|
||||
let input: unknown = undefined
|
||||
let input: Value = undefined
|
||||
while (true) {
|
||||
const method = kind === "next" ? iterator.next : get(iterator.iterator, kind)
|
||||
if (method === undefined || method === null) {
|
||||
@@ -1895,7 +1876,7 @@ class Frame<R> {
|
||||
node,
|
||||
)
|
||||
const done = Boolean(get(result, "done"))
|
||||
const resultValue: unknown =
|
||||
const resultValue: Value =
|
||||
self.generatorAsync && !iterator.asynchronous
|
||||
? yield* self.awaitAsyncFromSyncValue(iterator, get(result, "value"), node, kind !== "return" && !done)
|
||||
: get(result, "value")
|
||||
@@ -1904,7 +1885,7 @@ class Frame<R> {
|
||||
return resultValue
|
||||
}
|
||||
|
||||
const resumed: Exit.Exit<unknown, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
|
||||
const resumed: Exit.Exit<Value, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
|
||||
if (Exit.isSuccess(resumed)) {
|
||||
kind = "next"
|
||||
input = resumed.value
|
||||
@@ -1945,7 +1926,7 @@ class Frame<R> {
|
||||
} else if (keyNode.type === "Identifier") {
|
||||
key = keyNode.name
|
||||
} else if (keyNode.type === "Literal") {
|
||||
key = self.toPropertyKey(keyNode.value, keyNode)
|
||||
key = self.toPropertyKey(literal(keyNode), keyNode)
|
||||
} else {
|
||||
throw typeError("Unsupported object property key shape.", keyNode)
|
||||
}
|
||||
@@ -1964,7 +1945,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<Arr, unknown, R> {
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
@@ -2019,13 +2000,13 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateConditionalExpression(node: ConditionalExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private evaluateConditionalExpression(node: ConditionalExpression): Effect.Effect<Value, unknown, R> {
|
||||
return Effect.flatMap(this.evaluateExpression(node.test), (test) =>
|
||||
this.evaluateExpression(test ? node.consequent : node.alternate),
|
||||
)
|
||||
}
|
||||
|
||||
private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown {
|
||||
private applyCompoundAssignment(operator: string, current: Value, incoming: Value, node: AstNode): Value {
|
||||
if (!compoundOperators.has(operator)) {
|
||||
throw typeError(`Unsupported assignment operator '${operator}'.`, node)
|
||||
}
|
||||
@@ -2034,7 +2015,7 @@ class Frame<R> {
|
||||
|
||||
private getMemberReference(
|
||||
node: MemberExpression,
|
||||
): Effect.Effect<MemberReference | ToolReference | { value: unknown } | typeof OptionalShortCircuit, unknown, R> {
|
||||
): Effect.Effect<MemberReference | ToolReference | { value: Value } | typeof OptionalShortCircuit, unknown, R> {
|
||||
const objectNode = node.object
|
||||
const propertyNode = node.property
|
||||
if (objectNode.type === "Super") throw unsupportedSyntax(objectNode.type, objectNode)
|
||||
@@ -2060,16 +2041,14 @@ class Frame<R> {
|
||||
|
||||
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
|
||||
|
||||
// Primitives read through their wrapper prototype without being boxed; strings own length and indexes.
|
||||
const builtins = self.ctx.builtins
|
||||
// Strings own length and indexes; every other primitive property reads through the wrapper prototype.
|
||||
if (typeof objectValue === "string") {
|
||||
if (key === "length") return { value: objectValue.length }
|
||||
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
|
||||
if (index !== undefined) return { value: objectValue[index] }
|
||||
return { target: builtins.String, key, receiver: objectValue }
|
||||
}
|
||||
if (typeof objectValue === "number") return { target: builtins.Number, key, receiver: objectValue }
|
||||
if (typeof objectValue === "boolean") return { target: builtins.Boolean, key, receiver: objectValue }
|
||||
const proto = primitivePrototype(self.ctx.builtins, objectValue)
|
||||
if (proto !== undefined) return { target: proto, key, receiver: objectValue }
|
||||
|
||||
if (objectValue === null || objectValue === undefined) {
|
||||
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
|
||||
@@ -2078,7 +2057,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private readReference(reference: MemberReference, node: MemberExpression): unknown {
|
||||
private readReference(reference: MemberReference, node: MemberExpression): Value {
|
||||
// Reject unknown promise properties so a missing await cannot hide.
|
||||
if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
|
||||
throw invalidData(
|
||||
@@ -2090,7 +2069,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// Accessors throw without a location; the member or pattern that read them supplies it.
|
||||
private readProperty(target: Obj, key: PropertyKey, node: AstNode, receiver: unknown = target): unknown {
|
||||
private readProperty(target: Obj, key: PropertyKey, node: AstNode, receiver: Value = target): Value {
|
||||
try {
|
||||
return get(target, key, receiver)
|
||||
} catch (error) {
|
||||
@@ -2098,7 +2077,7 @@ class Frame<R> {
|
||||
}
|
||||
}
|
||||
|
||||
private readMember(node: MemberExpression): Effect.Effect<unknown, unknown, R> {
|
||||
private readMember(node: MemberExpression): Effect.Effect<Value, unknown, R> {
|
||||
return Effect.map(this.getMemberReference(node), (reference) => {
|
||||
if (reference === OptionalShortCircuit) return OptionalShortCircuit
|
||||
if (reference instanceof ToolReference) return reference
|
||||
@@ -2107,7 +2086,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private writeMember(node: MemberExpression, value: unknown): Effect.Effect<unknown, unknown, R> {
|
||||
private writeMember(node: MemberExpression, value: Value): Effect.Effect<Value, unknown, R> {
|
||||
return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
|
||||
}
|
||||
|
||||
@@ -2129,8 +2108,8 @@ class Frame<R> {
|
||||
// Resolve side-effecting object and key expressions exactly once.
|
||||
private modifyMember(
|
||||
node: MemberExpression,
|
||||
compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>,
|
||||
): Effect.Effect<unknown, unknown, R> {
|
||||
compute: (current: Value) => Effect.Effect<{ write: boolean; next: Value; result: Value }, unknown, R>,
|
||||
): Effect.Effect<Value, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const reference = yield* self.getMemberReference(node)
|
||||
@@ -2150,7 +2129,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private assignToReference(target: Obj, key: PropertyKey, next: unknown, node: AstNode): void {
|
||||
private assignToReference(target: Obj, key: PropertyKey, next: Value, node: AstNode): void {
|
||||
const written = (() => {
|
||||
try {
|
||||
rejectCircularInsertion(
|
||||
@@ -2168,7 +2147,7 @@ class Frame<R> {
|
||||
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
|
||||
}
|
||||
|
||||
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
|
||||
private toPropertyKey(value: Value, node: AstNode): PropertyKey {
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define, hidden, Native, Arr, ErrorObj, Obj } from "./objects.js"
|
||||
import { define, hidden, Native, Arr, ErrorObj, Obj, type Value } from "./objects.js"
|
||||
|
||||
export const errorTypes = [
|
||||
"Error",
|
||||
@@ -29,6 +29,7 @@ const builtins = [
|
||||
"Set",
|
||||
"URL",
|
||||
"URLSearchParams",
|
||||
"Headers",
|
||||
"Uint8Array",
|
||||
"TextEncoder",
|
||||
"TextDecoder",
|
||||
@@ -51,6 +52,14 @@ export const createErrorValue = (prototype: Obj, message: string | undefined): E
|
||||
return value
|
||||
}
|
||||
|
||||
/** The prototype a primitive reads its methods from without being boxed; none for null, undefined, and symbols. */
|
||||
export const primitivePrototype = (builtins: Builtins, value: Value): Obj | undefined => {
|
||||
if (typeof value === "string") return builtins.String
|
||||
if (typeof value === "number") return builtins.Number
|
||||
if (typeof value === "boolean") return builtins.Boolean
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const createBuiltins = (): Builtins => {
|
||||
const object = new Obj(null)
|
||||
// Function.prototype is itself callable and returns undefined.
|
||||
@@ -80,6 +89,7 @@ export const createBuiltins = (): Builtins => {
|
||||
Set: plain(),
|
||||
URL: plain(),
|
||||
URLSearchParams: plain(),
|
||||
Headers: plain(),
|
||||
Uint8Array: plain(),
|
||||
TextEncoder: plain(),
|
||||
TextDecoder: plain(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Node } from "acorn"
|
||||
import { Context } from "effect"
|
||||
import type { ErrorType } from "./intrinsics.js"
|
||||
import type { DiagnosticKind } from "../codemode.js"
|
||||
import type { ErrorObj } from "./objects.js"
|
||||
import type { ErrorObj, Value } from "./objects.js"
|
||||
|
||||
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
|
||||
export type AstNode = Node
|
||||
@@ -14,13 +14,13 @@ export const CallSite = Context.Reference<{ readonly node?: AstNode; readonly de
|
||||
|
||||
export type Binding = {
|
||||
mutable: boolean
|
||||
value: unknown
|
||||
value: Value
|
||||
initialized?: boolean
|
||||
}
|
||||
|
||||
export type StatementResult =
|
||||
| { kind: "none" }
|
||||
| { kind: "return"; value: unknown }
|
||||
| { kind: "return"; value: Value }
|
||||
| { kind: "break"; label?: string }
|
||||
| { kind: "continue"; label?: string }
|
||||
|
||||
@@ -31,11 +31,11 @@ export const IteratorSymbol: unique symbol = Symbol("codemode.iterator")
|
||||
export const IteratorSymbols = [AsyncIteratorSymbol, IteratorSymbol] as const
|
||||
|
||||
export class Throw {
|
||||
constructor(readonly value: unknown) {}
|
||||
constructor(readonly value: Value) {}
|
||||
}
|
||||
|
||||
export class GeneratorReturn {
|
||||
constructor(readonly value: unknown) {}
|
||||
constructor(readonly value: Value) {}
|
||||
}
|
||||
|
||||
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
|
||||
@@ -83,12 +83,10 @@ export const unsupportedSyntax = (kind: string, node: AstNode): PendingThrow =>
|
||||
[supportedSyntaxMessage],
|
||||
)
|
||||
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null
|
||||
|
||||
// Acorn lines are 1-based and its columns are 0-based. Diagnostics use 1-based columns of the submitted source.
|
||||
export const sourceLocation = (node: AstNode): { readonly line: number; readonly column: number } => ({
|
||||
line: Math.max(1, (node.loc?.start.line ?? 2) - 1),
|
||||
column: Math.max(1, (node.loc?.start.column ?? 4) - 3),
|
||||
line: node.loc?.start.line ?? 1,
|
||||
column: (node.loc?.start.column ?? 0) + 1,
|
||||
})
|
||||
|
||||
export const formatLocation = (node?: AstNode): string => {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { typeError } from "./model.js"
|
||||
import { type Callable, define, frozen, hidden, Native, type NativeOptions, Obj } from "./objects.js"
|
||||
import { type Callable, define, frozen, hidden, Native, type NativeOptions, Obj, type Value } from "./objects.js"
|
||||
import { describeValue } from "./references.js"
|
||||
|
||||
/** A native function body: a plain value, a thrown `PendingThrow`, or an Effect. */
|
||||
export type Impl = (thisValue: unknown, args: Array<unknown>) => unknown
|
||||
/** A native function body: a value, a thrown `PendingThrow`, or an Effect of a value. */
|
||||
export type Impl = (thisValue: Value, args: Array<Value>) => Value | Effect.Effect<Value, unknown, unknown>
|
||||
|
||||
// The dispatch in `Frame.call` suspends every native call, so a synchronous throw here is a defect.
|
||||
const lift =
|
||||
<R>(impl: Impl) =>
|
||||
(thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
(thisValue: Value, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const result = impl(thisValue, args)
|
||||
return Effect.isEffect(result) ? (result as Effect.Effect<unknown, unknown, R>) : Effect.succeed(result)
|
||||
return Effect.isEffect(result) ? (result as Effect.Effect<Value, unknown, R>) : Effect.succeed(result)
|
||||
}
|
||||
|
||||
export const native = <R>(builtins: Builtins, options: NativeOptions<R>): Native<R> =>
|
||||
@@ -27,7 +27,7 @@ export const methods = (builtins: Builtins, target: Obj, table: ReadonlyArray<Me
|
||||
for (const [name, length, impl] of table) define(target, name, fn(builtins, name, length, impl), hidden)
|
||||
}
|
||||
|
||||
export const constants = (target: Obj, table: Record<string, unknown>): void => {
|
||||
export const constants = (target: Obj, table: Record<string, Value>): void => {
|
||||
for (const [name, value] of Object.entries(table)) define(target, name, value, frozen)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const prototypeFrom = (newTarget: Callable, fallback: Obj): Obj => {
|
||||
/** Narrows a method receiver to the built-in it belongs to, or throws the TypeError JS would. */
|
||||
export const receiver = <T extends Obj>(
|
||||
cls: abstract new (...args: never) => T,
|
||||
thisValue: unknown,
|
||||
thisValue: Value,
|
||||
method: string,
|
||||
): T => {
|
||||
if (thisValue instanceof cls) return thisValue
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BlockStatement, Expression, Pattern } from "acorn"
|
||||
import type { Effect, Fiber } from "effect"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { checkArrayLength } from "./limits.js"
|
||||
import {
|
||||
AsyncIteratorSymbol,
|
||||
@@ -16,12 +18,12 @@ export type Attributes = {
|
||||
readonly configurable: boolean
|
||||
}
|
||||
|
||||
export type Getter = (receiver: unknown) => unknown
|
||||
export type Setter = (receiver: unknown, value: unknown) => void
|
||||
export type Getter = (receiver: Value) => Value
|
||||
export type Setter = (receiver: Value, value: Value) => void
|
||||
|
||||
/** One own property: a data slot or a native accessor pair. */
|
||||
export type Slot =
|
||||
| { value: unknown; writable: boolean; enumerable: boolean; configurable: boolean }
|
||||
| { value: Value; writable: boolean; enumerable: boolean; configurable: boolean }
|
||||
| { get: Getter | undefined; set: Setter | undefined; enumerable: boolean; configurable: boolean }
|
||||
|
||||
/** Ordinary assignment: writable, enumerable, configurable. */
|
||||
@@ -33,33 +35,119 @@ export const readonly: Attributes = { writable: false, enumerable: false, config
|
||||
/** Constants such as `Math.PI` and a constructor's `prototype`. */
|
||||
export const frozen: Attributes = { writable: false, enumerable: false, configurable: false }
|
||||
|
||||
/** An object owned by the program: own properties plus a prototype link. */
|
||||
/**
|
||||
* An object owned by the program: own properties plus a prototype link. Subclasses answer, in one place, how a
|
||||
* built-in kind of object prints, coerces, iterates, and crosses to the host.
|
||||
*/
|
||||
export class Obj {
|
||||
readonly props = new Map<string | symbol, Slot>()
|
||||
constructor(public proto: Obj | null) {}
|
||||
|
||||
/** The class name `Object.prototype.toString` reports: `[object Map]`. */
|
||||
readonly tag: string = "Object"
|
||||
|
||||
/** How diagnostics refer to a value of this kind. */
|
||||
get describe(): string {
|
||||
if (this.tag === "Object") return "a data object"
|
||||
return `${/^[AEIO]/.test(this.tag) ? "an" : "a"} ${this.tag}`
|
||||
}
|
||||
|
||||
/** ToString without consulting program-defined methods. */
|
||||
toString(): string {
|
||||
return `[object ${this.tag}]`
|
||||
}
|
||||
|
||||
/** ToPrimitive without consulting program-defined methods: only a Date answers a number hint differently. */
|
||||
toPrimitive(hint: "default" | "number" | "string"): string | number {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/** ToNumber without consulting program-defined methods. */
|
||||
toNumber(): number {
|
||||
return Number(this.toPrimitive("number"))
|
||||
}
|
||||
|
||||
/** How `console.log` shows the value; `item` formats a child with cycle and depth tracking. */
|
||||
inspect(item: (value: Value) => string): string {
|
||||
return `{${entries(this)
|
||||
.map(([key, value]) => `${JSON.stringify(key)}:${item(value)}`)
|
||||
.join(",")}}`
|
||||
}
|
||||
|
||||
/** A copy the host can hold; `item` converts a child. A `__proto__` key never reaches host code. */
|
||||
toHost(item: (value: Value) => unknown): unknown {
|
||||
return Object.fromEntries(
|
||||
entries(this)
|
||||
.filter(([key]) => key !== "__proto__")
|
||||
.map(([key, value]) => [key, item(value)]),
|
||||
)
|
||||
}
|
||||
|
||||
/** The built-in iteration `for...of` and spread use, when this kind of object has one. */
|
||||
iterator(builtins: Builtins): Iterator<Value, undefined> | undefined {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class Arr extends Obj {
|
||||
override readonly tag = "Array"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly items: Array<unknown> = [],
|
||||
readonly items: Array<Value> = [],
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override get describe() {
|
||||
return "an array"
|
||||
}
|
||||
override toString() {
|
||||
return this.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
}
|
||||
override inspect(item: (value: Value) => string) {
|
||||
return `[${this.items.map(item).join(",")}]`
|
||||
}
|
||||
override toHost(item: (value: Value) => unknown) {
|
||||
return this.items.map(item)
|
||||
}
|
||||
override iterator() {
|
||||
return this.items.values()
|
||||
}
|
||||
}
|
||||
|
||||
/** An object with the [[ErrorData]] slot: what `Error.prototype.toString` and the host boundary recognize as an error. */
|
||||
export class ErrorObj extends Obj {
|
||||
override readonly tag = "Error"
|
||||
/** The interpreter failure this error materialized from, so rethrowing it keeps the diagnostic kind and location. */
|
||||
host?: PendingThrow
|
||||
/** Error.prototype.toString: "name: message", or just one when the other is empty. */
|
||||
override toString() {
|
||||
const name = get(this, "name")
|
||||
const message = get(this, "message")
|
||||
const shownName = typeof name === "string" ? name : "Error"
|
||||
const shownMessage = typeof message === "string" ? message : ""
|
||||
if (shownMessage === "") return shownName
|
||||
if (shownName === "") return shownMessage
|
||||
return `${shownName}: ${shownMessage}`
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Callable extends Obj {
|
||||
/** Interpreter machinery a program can hold but never inspect, serialize, or hand to the host. */
|
||||
export abstract class Opaque extends Obj {
|
||||
override inspect() {
|
||||
return "[opaque reference]"
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Callable extends Opaque {
|
||||
override readonly tag = "Function"
|
||||
constructor(proto: Obj, name: string, length: number) {
|
||||
super(proto)
|
||||
define(this, "length", length, readonly)
|
||||
define(this, "name", name, readonly)
|
||||
}
|
||||
override get describe() {
|
||||
return "a function"
|
||||
}
|
||||
}
|
||||
|
||||
export class Fn extends Callable {
|
||||
@@ -77,8 +165,8 @@ export class Fn extends Callable {
|
||||
}
|
||||
}
|
||||
|
||||
export type NativeCall<R> = (thisValue: unknown, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
export type NativeConstruct<R> = (args: Array<unknown>, newTarget: Callable) => Effect.Effect<unknown, unknown, R>
|
||||
export type NativeCall<R> = (thisValue: Value, args: Array<Value>) => Effect.Effect<Value, unknown, R>
|
||||
export type NativeConstruct<R> = (args: Array<Value>, newTarget: Callable) => Effect.Effect<Value, unknown, R>
|
||||
|
||||
export type NativeOptions<R> = {
|
||||
readonly name: string
|
||||
@@ -103,60 +191,163 @@ export class Native<R = never> extends Callable {
|
||||
}
|
||||
}
|
||||
|
||||
export class PromiseObj extends Obj {
|
||||
export class PromiseObj extends Opaque {
|
||||
override readonly tag = "Promise"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly fiber: Fiber.Fiber<unknown, unknown>,
|
||||
readonly fiber: Fiber.Fiber<Value, unknown>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override get describe() {
|
||||
return "an un-awaited Promise"
|
||||
}
|
||||
override inspect() {
|
||||
return "[Promise (await it to get its value)]"
|
||||
}
|
||||
}
|
||||
|
||||
export class GeneratorObj extends Obj {
|
||||
export class GeneratorObj extends Opaque {
|
||||
override readonly tag = "Generator"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly asynchronous: boolean,
|
||||
readonly request: (kind: GeneratorRequestKind, value: unknown) => Effect.Effect<unknown, unknown, unknown>,
|
||||
readonly request: (kind: GeneratorRequestKind, value: Value) => Effect.Effect<Value, unknown, unknown>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override get describe() {
|
||||
return "a generator"
|
||||
}
|
||||
}
|
||||
|
||||
export class DateObj extends Obj {
|
||||
/** A built-in collection iterator: live over the host collection, yielding program values. */
|
||||
export class IteratorObj extends Opaque {
|
||||
override readonly tag = "Iterator"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly source: IteratorObject<Value, undefined>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override get describe() {
|
||||
return "an iterator"
|
||||
}
|
||||
override iterator() {
|
||||
return this.source
|
||||
}
|
||||
}
|
||||
|
||||
/** A built-in object around a host value: data-like, so it prints as itself and crosses to extensions as a copy. */
|
||||
export abstract class Wrapper extends Obj {
|
||||
override inspect(item: (value: Value) => string) {
|
||||
return this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export class DateObj extends Wrapper {
|
||||
override readonly tag = "Date"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
public time: number,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override toString() {
|
||||
return Number.isFinite(this.time) ? new Date(this.time).toISOString() : "Invalid Date"
|
||||
}
|
||||
override toPrimitive(hint: "default" | "number" | "string") {
|
||||
return hint === "number" ? this.time : this.toString()
|
||||
}
|
||||
override toHost() {
|
||||
return new Date(this.time)
|
||||
}
|
||||
}
|
||||
|
||||
export class RegExpObj extends Obj {
|
||||
export class RegExpObj extends Wrapper {
|
||||
override readonly tag = "RegExp"
|
||||
readonly regex: RegExp
|
||||
constructor(proto: Obj, pattern: string, flags: string) {
|
||||
super(proto)
|
||||
this.regex = new RegExp(pattern, flags)
|
||||
}
|
||||
override toString() {
|
||||
return `/${this.regex.source}/${this.regex.flags}`
|
||||
}
|
||||
override toHost() {
|
||||
return new RegExp(this.regex.source, this.regex.flags)
|
||||
}
|
||||
}
|
||||
|
||||
export class MapObj extends Obj {
|
||||
readonly map = new Map<unknown, unknown>()
|
||||
export class MapObj extends Wrapper {
|
||||
override readonly tag = "Map"
|
||||
readonly map = new Map<Value, Value>()
|
||||
override inspect(item: (value: Value) => string) {
|
||||
return `Map(${this.map.size}) [${[...this.map].map(([key, value]) => `[${item(key)},${item(value)}]`).join(",")}]`
|
||||
}
|
||||
override toHost(item: (value: Value) => unknown) {
|
||||
return new Map([...this.map].map(([key, value]) => [item(key), item(value)]))
|
||||
}
|
||||
override iterator(builtins: Builtins) {
|
||||
return this.map.entries().map((entry) => new Arr(builtins.Array, entry))
|
||||
}
|
||||
}
|
||||
|
||||
export class SetObj extends Obj {
|
||||
readonly set = new Set<unknown>()
|
||||
export class SetObj extends Wrapper {
|
||||
override readonly tag = "Set"
|
||||
readonly set = new Set<Value>()
|
||||
override inspect(item: (value: Value) => string) {
|
||||
return `Set(${this.set.size}) [${[...this.set].map(item).join(",")}]`
|
||||
}
|
||||
override toHost(item: (value: Value) => unknown) {
|
||||
return new Set([...this.set].map(item))
|
||||
}
|
||||
override iterator() {
|
||||
return this.set.values()
|
||||
}
|
||||
}
|
||||
|
||||
export class URLSearchParamsObj extends Obj {
|
||||
export class URLSearchParamsObj extends Wrapper {
|
||||
override readonly tag = "URLSearchParams"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly params: URLSearchParams,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override toString() {
|
||||
return this.params.toString()
|
||||
}
|
||||
override toHost() {
|
||||
return new URLSearchParams(this.params)
|
||||
}
|
||||
override iterator(builtins: Builtins) {
|
||||
return this.params.entries().map((entry) => new Arr(builtins.Array, entry))
|
||||
}
|
||||
}
|
||||
|
||||
export class URLObj extends Obj {
|
||||
export class HeadersObj extends Wrapper {
|
||||
override readonly tag = "Headers"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly headers: Headers,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override inspect() {
|
||||
return `Headers ${JSON.stringify(Object.fromEntries(this.headers))}`
|
||||
}
|
||||
override toHost() {
|
||||
return new Headers(this.headers)
|
||||
}
|
||||
override iterator(builtins: Builtins) {
|
||||
// Bun's Headers typings lack the iterator helpers, so the host iterator is lifted first.
|
||||
return Iterator.from(this.headers.entries()).map((entry) => new Arr(builtins.Array, entry))
|
||||
}
|
||||
}
|
||||
|
||||
export class URLObj extends Wrapper {
|
||||
override readonly tag = "URL"
|
||||
readonly searchParams: URLSearchParamsObj
|
||||
constructor(
|
||||
proto: Obj,
|
||||
@@ -166,29 +357,52 @@ export class URLObj extends Obj {
|
||||
super(proto)
|
||||
this.searchParams = new URLSearchParamsObj(searchParamsProto, url.searchParams)
|
||||
}
|
||||
override toString() {
|
||||
return this.url.href
|
||||
}
|
||||
override toHost() {
|
||||
return new URL(this.url.href)
|
||||
}
|
||||
}
|
||||
|
||||
/** A `Uint8Array`: the host array does the byte clamping and ignores out-of-range writes, as JS does. */
|
||||
export class Bytes extends Obj {
|
||||
export class Bytes extends Wrapper {
|
||||
override readonly tag = "Uint8Array"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly bytes: Uint8Array,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override toString() {
|
||||
return this.bytes.join(",")
|
||||
}
|
||||
override inspect() {
|
||||
return `Uint8Array(${this.bytes.length}) [${this.bytes.join(",")}]`
|
||||
}
|
||||
override toHost() {
|
||||
return new Uint8Array(this.bytes)
|
||||
}
|
||||
override iterator() {
|
||||
return this.bytes.values()
|
||||
}
|
||||
}
|
||||
|
||||
/** Built-in objects that wrap a host value; data-like, but never plain data. */
|
||||
export const isWrapper = (
|
||||
value: unknown,
|
||||
): value is DateObj | RegExpObj | MapObj | SetObj | URLObj | URLSearchParamsObj | Bytes =>
|
||||
value instanceof DateObj ||
|
||||
value instanceof RegExpObj ||
|
||||
value instanceof MapObj ||
|
||||
value instanceof SetObj ||
|
||||
value instanceof URLObj ||
|
||||
value instanceof URLSearchParamsObj ||
|
||||
value instanceof Bytes
|
||||
/** Every value a program can hold. Host values never appear here; they are copied in at the boundaries. */
|
||||
export type Value = string | number | boolean | null | undefined | symbol | Obj | ToolReference
|
||||
|
||||
/** ToString without consulting program-defined methods. */
|
||||
export const coerceToString = (value: Value): string => (value instanceof Obj ? value.toString() : String(value))
|
||||
|
||||
/** ToNumber without consulting program-defined methods; tool references are not numbers. */
|
||||
export const coerceToNumber = (value: Value): number => {
|
||||
if (value instanceof Obj) return value.toNumber()
|
||||
return value instanceof ToolReference ? Number.NaN : Number(value)
|
||||
}
|
||||
|
||||
/** Values that cannot cross the data boundary: opaque machinery and host-backed wrappers. */
|
||||
export const isRuntimeReference = (value: Value): boolean =>
|
||||
value instanceof Opaque || value instanceof Wrapper || value instanceof ToolReference
|
||||
|
||||
const MAX_ARRAY_INDEX = 4_294_967_295
|
||||
|
||||
@@ -206,7 +420,7 @@ type Indexed = Arr | Bytes
|
||||
|
||||
const isIndexed = (target: Obj): target is Indexed => target instanceof Arr || target instanceof Bytes
|
||||
|
||||
const elements = (target: Indexed): Array<unknown> | Uint8Array => (target instanceof Arr ? target.items : target.bytes)
|
||||
const elements = (target: Indexed): Array<Value> | Uint8Array => (target instanceof Arr ? target.items : target.bytes)
|
||||
|
||||
const index = (target: Obj, key: string | symbol): number | undefined =>
|
||||
isIndexed(target) && typeof key === "string" ? parseArrayIndex(key) : undefined
|
||||
@@ -227,18 +441,18 @@ export const own = (target: Obj, key: PropertyKey): Slot | undefined => {
|
||||
return target.props.get(name)
|
||||
}
|
||||
|
||||
const read = (slot: Slot, receiver: unknown): unknown =>
|
||||
const read = (slot: Slot, receiver: Value): Value =>
|
||||
"value" in slot ? slot.value : slot.get === undefined ? undefined : slot.get(receiver)
|
||||
|
||||
export const hasOwn = (target: Obj, key: PropertyKey): boolean => own(target, key) !== undefined
|
||||
|
||||
export const getOwn = (target: Obj, key: PropertyKey): unknown => {
|
||||
export const getOwn = (target: Obj, key: PropertyKey): Value => {
|
||||
const slot = own(target, key)
|
||||
return slot === undefined ? undefined : read(slot, target)
|
||||
}
|
||||
|
||||
/** [[Get]]: walks the prototype chain; accessors see `receiver`, which is the primitive for wrapper prototypes. */
|
||||
export const get = (target: Obj, key: PropertyKey, receiver: unknown = target): unknown => {
|
||||
export const get = (target: Obj, key: PropertyKey, receiver: Value = target): Value => {
|
||||
for (let current: Obj | null = target; current !== null; current = current.proto) {
|
||||
const slot = own(current, key)
|
||||
if (slot !== undefined) return read(slot, receiver)
|
||||
@@ -253,14 +467,14 @@ export const has = (target: Obj, key: PropertyKey): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
export const hasPrototype = (value: unknown, proto: Obj): boolean => {
|
||||
export const hasPrototype = (value: Value, proto: Obj): boolean => {
|
||||
for (let current = value instanceof Obj ? value.proto : null; current !== null; current = current.proto) {
|
||||
if (current === proto) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const writeElement = (target: Indexed, name: string | symbol, value: unknown): boolean | undefined => {
|
||||
const writeElement = (target: Indexed, name: string | symbol, value: Value): boolean | undefined => {
|
||||
const at = index(target, name)
|
||||
if (at !== undefined) {
|
||||
if (target instanceof Bytes) target.bytes[at] = typeof value === "number" ? value : Number(value)
|
||||
@@ -276,7 +490,7 @@ const writeElement = (target: Indexed, name: string | symbol, value: unknown): b
|
||||
}
|
||||
|
||||
/** [[Set]]: an inherited setter or read-only property decides before an own data property is created. */
|
||||
export const set = (target: Obj, key: PropertyKey, value: unknown): boolean => {
|
||||
export const set = (target: Obj, key: PropertyKey, value: Value): boolean => {
|
||||
const name = canonical(key)
|
||||
for (let current: Obj | null = target; current !== null; current = current.proto) {
|
||||
const slot = own(current, name)
|
||||
@@ -304,7 +518,7 @@ export const set = (target: Obj, key: PropertyKey, value: unknown): boolean => {
|
||||
}
|
||||
|
||||
/** [[DefineOwnProperty]] for a data property, ignoring the chain. */
|
||||
export const define = (target: Obj, key: PropertyKey, value: unknown, attrs: Attributes = data): void => {
|
||||
export const define = (target: Obj, key: PropertyKey, value: Value, attrs: Attributes = data): void => {
|
||||
const name = canonical(key)
|
||||
if (isIndexed(target) && writeElement(target, name, value) !== undefined) return
|
||||
target.props.set(name, { value, ...attrs })
|
||||
@@ -355,9 +569,9 @@ export const keys = (target: Obj): Array<string> =>
|
||||
ownKeys(target).filter((key): key is string => typeof key === "string" && enumerable(target, key))
|
||||
|
||||
/** Own enumerable string entries: `Object.entries` and serialization. */
|
||||
export const entries = (target: Obj): Array<[string, unknown]> => keys(target).map((key) => [key, getOwn(target, key)])
|
||||
export const entries = (target: Obj): Array<[string, Value]> => keys(target).map((key) => [key, getOwn(target, key)])
|
||||
|
||||
export const record = (proto: Obj, fields: Record<string, unknown>): Obj => {
|
||||
export const record = (proto: Obj, fields: Record<string, Value>): Obj => {
|
||||
const target = new Obj(proto)
|
||||
for (const [key, value] of Object.entries(fields)) define(target, key, value)
|
||||
return target
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
|
||||
import type { Diagnostic } from "../codemode.js"
|
||||
import { MAX_PENDING_PROMISES } from "./limits.js"
|
||||
import { CallSite, Throw, rangeError, typeError } from "./model.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj, PromiseObj, record } from "./objects.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj, PromiseObj, record, type Value } from "./objects.js"
|
||||
import { constructor, fn, methods, native, receiver, requiresNew } from "./native.js"
|
||||
import { createAggregateErrorValue, locate, materialize, normalizeError } from "./errors.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
@@ -10,7 +10,7 @@ import { applyCollectionCallback, isSupportedCallback } from "./callback.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
|
||||
// A `resolve`/`reject` handed to an executor or thenable: calling it settles the capability.
|
||||
const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: unknown) => void) =>
|
||||
const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: Value) => void) =>
|
||||
fn(ctx.builtins, name, 1, (_, args) => {
|
||||
settle(args[0])
|
||||
return undefined
|
||||
@@ -31,7 +31,7 @@ export class Pending<R> {
|
||||
|
||||
// Resolution bodies need the promise's own identity to reject `resolve(promise)` self-resolution.
|
||||
createWithSelf(
|
||||
body: (self: { promise?: PromiseObj }) => Effect.Effect<unknown, unknown, R>,
|
||||
body: (self: { promise?: PromiseObj }) => Effect.Effect<Value, unknown, R>,
|
||||
): Effect.Effect<PromiseObj, never, R> {
|
||||
const self: { promise?: PromiseObj } = {}
|
||||
return Effect.map(this.create(body(self)), (promise) => {
|
||||
@@ -40,7 +40,7 @@ export class Pending<R> {
|
||||
})
|
||||
}
|
||||
|
||||
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<PromiseObj, never, R> {
|
||||
create(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R> {
|
||||
return Effect.flatMap(CallSite, (site) => {
|
||||
if (this.active.size >= MAX_PENDING_PROMISES) {
|
||||
throw rangeError(
|
||||
@@ -79,7 +79,7 @@ export class Pending<R> {
|
||||
if (id !== undefined) this.failures.delete(id)
|
||||
}
|
||||
|
||||
await(promise: PromiseObj): Effect.Effect<Exit.Exit<unknown, unknown>> {
|
||||
await(promise: PromiseObj): Effect.Effect<Exit.Exit<Value, unknown>> {
|
||||
return Fiber.await(promise.fiber)
|
||||
}
|
||||
|
||||
@@ -105,9 +105,9 @@ export class Pending<R> {
|
||||
|
||||
export const resolvePromiseValue = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
own?: { promise?: PromiseObj },
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
): Effect.Effect<Value, unknown, R> => {
|
||||
if (own?.promise !== undefined && value === own.promise) {
|
||||
return Effect.die(typeError("Chaining cycle detected: a promise cannot resolve with itself."))
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export const resolvePromiseValue = <R>(
|
||||
return Effect.gen(function* () {
|
||||
// Promise resolution invokes a thenable's method in a later job.
|
||||
yield* Effect.yieldNow
|
||||
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const deferred = Deferred.makeUnsafe<Value, unknown>()
|
||||
const resolve = capability(ctx, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)))
|
||||
const reject = capability(ctx, "reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(reason))))
|
||||
const executed = yield* Effect.exit(ctx.call(then, value, [resolve, reject]))
|
||||
@@ -131,7 +131,7 @@ export const resolvePromiseValue = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
export const resolvePromise = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<PromiseObj, never, R> => {
|
||||
export const resolvePromise = <R>(ctx: Interpreter<R>, value: Value): Effect.Effect<PromiseObj, never, R> => {
|
||||
if (value instanceof PromiseObj) return Effect.succeed(value)
|
||||
return ctx.pending.createWithSelf((self) => resolvePromiseValue(ctx, value, self))
|
||||
}
|
||||
@@ -141,8 +141,8 @@ const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"]
|
||||
const invokePromiseMethod = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
name: (typeof promiseStatics)[number],
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
args: Array<Value>,
|
||||
): Effect.Effect<Value, unknown, R> => {
|
||||
if (name === "resolve") {
|
||||
return resolvePromise(ctx, args[0])
|
||||
}
|
||||
@@ -175,7 +175,7 @@ const invokePromiseMethod = <R>(
|
||||
)
|
||||
}
|
||||
if (name === "allSettled") {
|
||||
const outcomes: Array<unknown> = []
|
||||
const outcomes: Array<Value> = []
|
||||
for (const item of items) {
|
||||
const exit = yield* ctx.pending.await(item)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
@@ -223,8 +223,8 @@ const invokePromiseMethod = <R>(
|
||||
const instanceMethod = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
name: "then" | "catch" | "finally",
|
||||
thisValue: unknown,
|
||||
args: Array<unknown>,
|
||||
thisValue: Value,
|
||||
args: Array<Value>,
|
||||
): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
const method = `Promise.prototype.${name}`
|
||||
const promise = receiver(PromiseObj, thisValue, method)
|
||||
@@ -237,12 +237,12 @@ const instanceMethod = <R>(
|
||||
return chainReaction(ctx, promise, onFulfilled, onRejected, method)
|
||||
}
|
||||
|
||||
const constructPromise = <R>(ctx: Interpreter<R>, executor: unknown): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
const constructPromise = <R>(ctx: Interpreter<R>, executor: Value): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
if (!(executor instanceof Fn)) {
|
||||
throw typeError("new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).")
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const deferred = Deferred.makeUnsafe<Value, unknown>()
|
||||
const promise = yield* ctx.pending.createWithSelf((self) =>
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)),
|
||||
)
|
||||
@@ -262,10 +262,10 @@ const settleAfterTurn = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A
|
||||
Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit))
|
||||
|
||||
class PromiseAnyFulfilled {
|
||||
constructor(readonly value: unknown) {}
|
||||
constructor(readonly value: Value) {}
|
||||
}
|
||||
|
||||
const reactionHandler = (value: unknown, method: string): Callable | undefined => {
|
||||
const reactionHandler = (value: Value, method: string): Callable | undefined => {
|
||||
if (isSupportedCallback(value)) return value
|
||||
if (typeofValue(value) === "function") {
|
||||
throw typeError(
|
||||
@@ -279,7 +279,7 @@ const reactionHandler = (value: unknown, method: string): Callable | undefined =
|
||||
const reactionExit = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
source: PromiseObj,
|
||||
): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> =>
|
||||
): Effect.Effect<Exit.Exit<Value, unknown>, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* ctx.pending.await(source)
|
||||
if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
|
||||
|
||||
@@ -1,44 +1,18 @@
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { invalidData } from "./model.js"
|
||||
import {
|
||||
Callable,
|
||||
getOwn,
|
||||
isWrapper,
|
||||
ownKeys,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
GeneratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "./objects.js"
|
||||
import { Callable, getOwn, isRuntimeReference, Obj, Opaque, ownKeys, type Value } from "./objects.js"
|
||||
|
||||
/** Values that cannot cross the data boundary. */
|
||||
export const isRuntimeReference = (value: unknown): boolean =>
|
||||
value instanceof Callable ||
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof ToolReference ||
|
||||
value instanceof PromiseObj ||
|
||||
isWrapper(value)
|
||||
|
||||
function* childValues(value: object): Generator {
|
||||
if (!(value instanceof Obj)) return
|
||||
for (const key of ownKeys(value)) yield getOwn(value, key)
|
||||
}
|
||||
/** Interpreter machinery that is never data, unlike a Date or Map, which cross some boundaries as copies. */
|
||||
export const isOpaque = (value: Value): boolean => value instanceof Opaque || value instanceof ToolReference
|
||||
|
||||
// Depth-first search over a value tree. `match` stops the walk; `skip` prunes a subtree without matching it.
|
||||
const find = (
|
||||
value: unknown,
|
||||
match: (current: unknown) => boolean,
|
||||
skip: (current: unknown) => boolean,
|
||||
value: Value,
|
||||
match: (current: Value) => boolean,
|
||||
skip: (current: Value) => boolean,
|
||||
seen: Set<object>,
|
||||
): boolean => {
|
||||
const pending: Array<Iterator<unknown>> = [[value].values()]
|
||||
const pending: Array<Iterator<Value>> = [[value].values()]
|
||||
while (pending.length > 0) {
|
||||
const next = pending.at(-1)!.next()
|
||||
if (next.done) {
|
||||
@@ -47,25 +21,28 @@ const find = (
|
||||
}
|
||||
const current = next.value
|
||||
if (match(current)) return true
|
||||
if (current === null || typeof current !== "object" || skip(current) || seen.has(current)) continue
|
||||
if (!(current instanceof Obj) || skip(current) || seen.has(current)) continue
|
||||
seen.add(current)
|
||||
pending.push(childValues(current))
|
||||
pending.push(
|
||||
ownKeys(current)
|
||||
.map((key) => getOwn(current, key))
|
||||
.values(),
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const never = () => false
|
||||
|
||||
export const containsRuntimeReference = (value: unknown): boolean => find(value, isRuntimeReference, never, new Set())
|
||||
export const containsRuntimeReference = (value: Value): boolean => find(value, isRuntimeReference, never, new Set())
|
||||
|
||||
// Wrapper values are data here, not opaque interpreter references.
|
||||
export const containsOpaqueReference = (value: unknown): boolean =>
|
||||
find(value, (current) => !isWrapper(current) && isRuntimeReference(current), isWrapper, new Set())
|
||||
export const containsOpaqueReference = (value: Value): boolean =>
|
||||
find(value, isOpaque, (current) => isRuntimeReference(current) && !isOpaque(current), new Set())
|
||||
|
||||
// Reject cycles before mutation so later boundary walks remain safe.
|
||||
export const rejectCircularInsertion = (
|
||||
container: object,
|
||||
value: unknown,
|
||||
container: Obj,
|
||||
value: Value,
|
||||
label: string,
|
||||
seen = new Set<object>(),
|
||||
): void => {
|
||||
@@ -74,25 +51,14 @@ export const rejectCircularInsertion = (
|
||||
}
|
||||
}
|
||||
|
||||
export const describeValue = (value: unknown): string => {
|
||||
export const describeValue = (value: Value): string => {
|
||||
if (value === null || value === undefined) return String(value)
|
||||
if (value instanceof Arr) return "an array"
|
||||
if (value instanceof PromiseObj) return "an un-awaited Promise"
|
||||
if (value instanceof Obj) return value.describe
|
||||
if (value instanceof ToolReference) return "a tool reference"
|
||||
if (value instanceof DateObj) return "a Date"
|
||||
if (value instanceof RegExpObj) return "a RegExp"
|
||||
if (value instanceof MapObj) return "a Map"
|
||||
if (value instanceof SetObj) return "a Set"
|
||||
if (value instanceof URLObj) return "a URL"
|
||||
if (value instanceof URLSearchParamsObj) return "a URLSearchParams"
|
||||
if (value instanceof Bytes) return "a Uint8Array"
|
||||
if (value instanceof GeneratorObj) return "a generator"
|
||||
if (isRuntimeReference(value)) return "a function"
|
||||
if (typeof value === "object") return "a data object"
|
||||
return `a ${typeof value}`
|
||||
}
|
||||
|
||||
export const typeofValue = (value: unknown): string => {
|
||||
export const typeofValue = (value: Value): string => {
|
||||
if (value instanceof Callable) return "function"
|
||||
if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
|
||||
return typeof value
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type AstNode, type Binding, referenceError, typeError } from "./model.js"
|
||||
import type { Value } from "./objects.js"
|
||||
|
||||
export class ScopeStack {
|
||||
private readonly scopes: Array<Map<string, Binding>>
|
||||
@@ -15,7 +16,7 @@ export class ScopeStack {
|
||||
scope.set(name, { mutable, value: undefined, initialized: false })
|
||||
}
|
||||
|
||||
initialize(name: string, value: unknown, node: AstNode): void {
|
||||
initialize(name: string, value: Value, node: AstNode): void {
|
||||
const binding = this.current().get(name)
|
||||
if (!binding || binding.initialized !== false) {
|
||||
throw typeError(`Identifier '${name}' has not been reserved for initialization.`, node)
|
||||
@@ -24,7 +25,7 @@ export class ScopeStack {
|
||||
binding.initialized = true
|
||||
}
|
||||
|
||||
declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
|
||||
declare(name: string, value: Value, mutable: boolean, node: AstNode): void {
|
||||
const scope = this.current()
|
||||
if (scope.has(name)) {
|
||||
throw typeError(`Identifier '${name}' has already been declared.`, node)
|
||||
@@ -32,7 +33,7 @@ export class ScopeStack {
|
||||
scope.set(name, { mutable, value, initialized: true })
|
||||
}
|
||||
|
||||
get(name: string, node: AstNode): unknown {
|
||||
get(name: string, node: AstNode): Value {
|
||||
const binding = this.resolve(name)
|
||||
|
||||
if (!binding) {
|
||||
@@ -46,7 +47,7 @@ export class ScopeStack {
|
||||
return binding.value
|
||||
}
|
||||
|
||||
set(name: string, value: unknown, node: AstNode): unknown {
|
||||
set(name: string, value: Value, node: AstNode): Value {
|
||||
const binding = this.resolve(name)
|
||||
|
||||
if (!binding) {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
||||
|
||||
export interface TranspileResult {
|
||||
readonly outputText: string
|
||||
readonly error?: string
|
||||
}
|
||||
|
||||
// Full TypeScript transpilation on node/bun runtimes.
|
||||
export const transpile = (source: string): TranspileResult => {
|
||||
const transpiled = transpileModule(source, {
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: {
|
||||
target: ScriptTarget.ESNext,
|
||||
module: ModuleKind.ESNext,
|
||||
},
|
||||
})
|
||||
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
|
||||
if (diagnostic) {
|
||||
return {
|
||||
outputText: transpiled.outputText,
|
||||
error: flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
||||
}
|
||||
}
|
||||
return { outputText: transpiled.outputText }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export interface TranspileResult {
|
||||
readonly outputText: string
|
||||
readonly error?: string
|
||||
}
|
||||
|
||||
// workerd profile: the typescript compiler is ~11 MiB and probes node
|
||||
// internals at module init, so codemode programs are passed through
|
||||
// untranspiled. Plain-JS programs (the overwhelmingly common case) parse
|
||||
// fine downstream via acorn; TypeScript-only syntax surfaces as a parse
|
||||
// error from the interpreter instead of a transpile diagnostic.
|
||||
export const transpile = (source: string): TranspileResult => ({ outputText: source })
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength, MAX_ARRAY_LENGTH } from "../interpreter/limits.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { get, Arr, GeneratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
get,
|
||||
hidden,
|
||||
Arr,
|
||||
GeneratorObj,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import { applyCollectionCallback, invoke, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { compareText } from "../tool-runtime.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const arrayLikeSource = (source: unknown): { readonly length: number; readonly source: Obj } => {
|
||||
const arrayLikeSource = (source: Value): { readonly length: number; readonly source: Obj } => {
|
||||
if (source instanceof Obj && typeof get(source, "length") === "number") {
|
||||
const length = get(source, "length") as number
|
||||
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
|
||||
@@ -21,7 +31,7 @@ const arrayLikeSource = (source: unknown): { readonly length: number; readonly s
|
||||
)
|
||||
}
|
||||
|
||||
const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const source = args[0]
|
||||
const proto = ctx.builtins.Array
|
||||
const apply =
|
||||
@@ -33,14 +43,14 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<
|
||||
throw typeError("Array.from expects a synchronous iterable or array-like value.")
|
||||
}
|
||||
const arrayLike = arrayLikeSource(source)
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
for (let index = 0; index < arrayLike.length; index += 1) {
|
||||
const item = get(arrayLike.source, index)
|
||||
values.push(apply === undefined ? item : yield* apply([item, index]))
|
||||
}
|
||||
return new Arr(proto, values)
|
||||
}
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -53,21 +63,21 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<
|
||||
|
||||
export const sortArray = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
target: Array<unknown>,
|
||||
comparator: unknown,
|
||||
target: Array<Value>,
|
||||
comparator: Value,
|
||||
name: string,
|
||||
): Effect.Effect<Array<unknown>, unknown, R> => {
|
||||
): Effect.Effect<Array<Value>, unknown, R> => {
|
||||
if (comparator === undefined) {
|
||||
return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b))))
|
||||
}
|
||||
const apply = applyCollectionCallback(ctx, comparator, name)
|
||||
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
|
||||
const mergeSort = (items: Array<Value>): Effect.Effect<Array<Value>, unknown, R> => {
|
||||
if (items.length <= 1) return Effect.succeed(items)
|
||||
const midpoint = Math.floor(items.length / 2)
|
||||
return Effect.gen(function* () {
|
||||
const left = yield* mergeSort(items.slice(0, midpoint))
|
||||
const right = yield* mergeSort(items.slice(midpoint))
|
||||
const merged: Array<unknown> = []
|
||||
const merged: Array<Value> = []
|
||||
let leftIndex = 0
|
||||
let rightIndex = 0
|
||||
while (leftIndex < left.length && rightIndex < right.length) {
|
||||
@@ -88,8 +98,8 @@ export const sortArray = <R>(
|
||||
export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Array
|
||||
const wrap = (items: Array<unknown>) => new Arr(proto, items)
|
||||
const construct = (args: Array<unknown>, into: Obj): Arr => {
|
||||
const wrap = (items: Array<Value>) => new Arr(proto, items)
|
||||
const construct = (args: Array<Value>, into: Obj): Arr => {
|
||||
if (args.length !== 1) return new Arr(into, [...args])
|
||||
const first = args[0]
|
||||
if (typeof first !== "number") return new Arr(into, [first])
|
||||
@@ -109,8 +119,8 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["from", 1, (_, args) => arrayFrom(ctx, args)],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
|
||||
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") {
|
||||
throw typeError(`Array.${name} expects ${label} to be a number.`)
|
||||
@@ -122,11 +132,11 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
name: string,
|
||||
length: number,
|
||||
body: (
|
||||
target: Array<unknown>,
|
||||
target: Array<Value>,
|
||||
receiver: Arr,
|
||||
apply: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
args: Array<unknown>,
|
||||
) => Effect.Effect<unknown, unknown, R>,
|
||||
apply: (args: Array<Value>) => Effect.Effect<Value, unknown, R>,
|
||||
args: Array<Value>,
|
||||
) => Effect.Effect<Value, unknown, R>,
|
||||
): Method => [
|
||||
name,
|
||||
length,
|
||||
@@ -214,7 +224,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
"flat",
|
||||
0,
|
||||
(thisValue, args) => {
|
||||
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
|
||||
const flatten = (items: Array<Value>, depth: number): Array<Value> =>
|
||||
items.flatMap((item) => (item instanceof Arr && depth > 0 ? flatten(item.items, depth - 1) : [item]))
|
||||
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
|
||||
checkArrayLength(flattened.length)
|
||||
@@ -340,18 +350,36 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").items.keys()))],
|
||||
["values", 0, (thisValue) => wrap([...self(thisValue, "values").items])],
|
||||
[
|
||||
"toLocaleString",
|
||||
0,
|
||||
(thisValue) =>
|
||||
Effect.map(
|
||||
Effect.forEach(self(thisValue, "toLocaleString").items, (item) =>
|
||||
item === null || item === undefined
|
||||
? Effect.succeed("")
|
||||
: Effect.map(invoke(ctx, item, "toLocaleString", "Array.prototype.toLocaleString"), coerceToString),
|
||||
),
|
||||
(parts) => parts.join(","),
|
||||
),
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").items.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").items.values())],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
wrap(Array.from(self(thisValue, "entries").items.entries(), ([index, item]) => wrap([index, item]))),
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.items.entries()
|
||||
.map(([index, item]) => wrap([index, item])),
|
||||
),
|
||||
],
|
||||
iterate("map", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
values.length = length
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
@@ -363,7 +391,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
iterate("flatMap", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const mapped = yield* apply([target[index], index, receiver])
|
||||
@@ -376,7 +404,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
iterate("filter", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
const values: Array<unknown> = []
|
||||
const values: Array<Value> = []
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const item = target[index]
|
||||
@@ -490,5 +518,6 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
}),
|
||||
),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { Effect } from "effect"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { rangeError, syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { defineAccessor, get, Arr, Bytes, Obj } from "../interpreter/objects.js"
|
||||
import { IteratorSymbol, rangeError, syntaxError, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
get,
|
||||
hidden,
|
||||
Arr,
|
||||
Bytes,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
/** The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. */
|
||||
const collectBytes = <R>(ctx: Interpreter<R>, source: unknown, name: string): Effect.Effect<Uint8Array, unknown, R> => {
|
||||
const collectBytes = <R>(ctx: Interpreter<R>, source: Value, name: string): Effect.Effect<Uint8Array, unknown, R> => {
|
||||
if (source instanceof Bytes) return Effect.succeed(new Uint8Array(source.bytes))
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
@@ -27,7 +38,7 @@ const collectBytes = <R>(ctx: Interpreter<R>, source: unknown, name: string): Ef
|
||||
})
|
||||
}
|
||||
|
||||
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
|
||||
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<Value>, proto: Obj) => {
|
||||
const source = args[0]
|
||||
if (source !== null && typeof source === "object") {
|
||||
return Effect.map(collectBytes(ctx, source, "new Uint8Array(...)"), (bytes) => new Bytes(proto, bytes))
|
||||
@@ -48,7 +59,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
call: requiresNew("Uint8Array"),
|
||||
construct: (args, newTarget) => constructBytes(ctx, args, prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const decode = (name: string, args: Array<unknown>, from: (text: string) => Uint8Array) => {
|
||||
const decode = (name: string, args: Array<Value>, from: (text: string) => Uint8Array) => {
|
||||
if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`)
|
||||
try {
|
||||
return wrap(from(args[0]))
|
||||
@@ -63,13 +74,13 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["fromHex", 1, (_, args) => decode("fromHex", args, (text) => Uint8Array.fromHex(text))],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
|
||||
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") throw typeError(`Uint8Array.${name} expects ${label} to be a number.`)
|
||||
return value
|
||||
}
|
||||
const wrapAll = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const wrapAll = (items: Array<Value>) => new Arr(builtins.Array, items)
|
||||
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
|
||||
methods(builtins, proto, [
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber("at", args[0], "index") ?? 0)],
|
||||
@@ -173,15 +184,21 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").bytes.join(",")],
|
||||
["toBase64", 0, (thisValue) => self(thisValue, "toBase64").bytes.toBase64()],
|
||||
["toHex", 0, (thisValue) => self(thisValue, "toHex").bytes.toHex()],
|
||||
["keys", 0, (thisValue) => wrapAll(Array.from(self(thisValue, "keys").bytes.keys()))],
|
||||
["values", 0, (thisValue) => wrapAll(Array.from(self(thisValue, "values").bytes.values()))],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").bytes.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").bytes.values())],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
wrapAll(Array.from(self(thisValue, "entries").bytes.entries(), ([index, byte]) => wrapAll([index, byte]))),
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.bytes.entries()
|
||||
.map(([index, byte]) => wrapAll([index, byte])),
|
||||
),
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return uint8Array
|
||||
}
|
||||
|
||||
@@ -216,8 +233,7 @@ const utf8Labels = new Set(["unicode-1-1-utf-8", "unicode11utf8", "unicode20utf8
|
||||
export const textDecoderGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.TextDecoder
|
||||
const self = (thisValue: unknown, name: string) =>
|
||||
receiver(TextDecoderObj, thisValue, `TextDecoder.prototype.${name}`)
|
||||
const self = (thisValue: Value, name: string) => receiver(TextDecoderObj, thisValue, `TextDecoder.prototype.${name}`)
|
||||
defineAccessor(proto, "encoding", (thisValue) => self(thisValue, "encoding").decoder.encoding)
|
||||
defineAccessor(proto, "fatal", (thisValue) => self(thisValue, "fatal").decoder.fatal)
|
||||
defineAccessor(proto, "ignoreBOM", (thisValue) => self(thisValue, "ignoreBOM").decoder.ignoreBOM)
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { invalidData, typeError } from "../interpreter/model.js"
|
||||
import { invalidData, IteratorSymbol, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
get,
|
||||
getOwn,
|
||||
hidden,
|
||||
isWrapper,
|
||||
Arr,
|
||||
coerceToString,
|
||||
IteratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
SetObj,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
|
||||
import { describeValue, isOpaque } from "../interpreter/references.js"
|
||||
import {
|
||||
applyCollectionCallback,
|
||||
isSupportedCallback,
|
||||
@@ -24,9 +26,9 @@ import {
|
||||
} from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
const coerceGroupByPropertyKey = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<string, unknown, R> => {
|
||||
if (value instanceof PromiseObj) return Effect.succeed("[object Promise]")
|
||||
if (!isWrapper(value) && isRuntimeReference(value)) {
|
||||
const coerceGroupByPropertyKey = <R>(ctx: Interpreter<R>, value: Value): Effect.Effect<string, unknown, R> => {
|
||||
if (value instanceof PromiseObj) return Effect.succeed(coerceToString(value))
|
||||
if (isOpaque(value)) {
|
||||
throw invalidData(`Object.groupBy callback must return a data value, received ${describeValue(value)}.`)
|
||||
}
|
||||
return toPrimitiveString(ctx, value)
|
||||
@@ -80,7 +82,7 @@ export const groupBy = <R>(ctx: Interpreter<R>, namespace: "Map" | "Object") =>
|
||||
})
|
||||
})
|
||||
|
||||
const constructMap = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
|
||||
const constructMap = <R>(ctx: Interpreter<R>, init: Value, proto: Obj) => {
|
||||
const target = new MapObj(proto)
|
||||
if (init === undefined || init === null) return Effect.succeed(target)
|
||||
return Effect.gen(function* () {
|
||||
@@ -104,7 +106,7 @@ const constructMap = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
|
||||
})
|
||||
}
|
||||
|
||||
const constructSet = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
|
||||
const constructSet = <R>(ctx: Interpreter<R>, init: Value, proto: Obj) => {
|
||||
const target = new SetObj(proto)
|
||||
if (init === undefined || init === null) return Effect.succeed(target)
|
||||
return Effect.gen(function* () {
|
||||
@@ -129,8 +131,7 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
construct: (args, newTarget) => constructMap(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
define(map, "groupBy", groupBy(ctx, "Map"), hidden)
|
||||
const self = (thisValue: unknown, name: string) => receiver(MapObj, thisValue, `Map.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const self = (thisValue: Value, name: string) => receiver(MapObj, thisValue, `Map.prototype.${name}`)
|
||||
defineAccessor(proto, "size", (thisValue) => receiver(MapObj, thisValue, "Map.prototype.size").map.size)
|
||||
methods(builtins, proto, [
|
||||
["get", 1, (thisValue, args) => self(thisValue, "get").map.get(args[0])],
|
||||
@@ -144,6 +145,29 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsert",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsert").map
|
||||
if (!target.has(args[0])) target.set(args[0], args[1])
|
||||
return target.get(args[0])
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsertComputed",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsertComputed").map
|
||||
const apply = applyCollectionCallback(ctx, args[1], "Map.getOrInsertComputed")
|
||||
if (target.has(args[0])) return target.get(args[0])
|
||||
// The callback sees the stored key (-0 is +0) and its result wins over anything it inserted itself.
|
||||
return Effect.map(apply([args[0] === 0 ? 0 : args[0]]), (value) => {
|
||||
target.set(args[0], value)
|
||||
return value
|
||||
})
|
||||
},
|
||||
],
|
||||
["delete", 1, (thisValue, args) => self(thisValue, "delete").map.delete(args[0])],
|
||||
[
|
||||
"clear",
|
||||
@@ -153,13 +177,9 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").map.keys()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").map.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) => wrap(Array.from(self(thisValue, "entries").map.entries(), ([key, item]) => wrap([key, item]))),
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").map.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").map.values())],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
[
|
||||
"forEach",
|
||||
1,
|
||||
@@ -173,31 +193,32 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return map
|
||||
}
|
||||
|
||||
type SetRecord<R> = {
|
||||
readonly size: number
|
||||
readonly has: (item: unknown) => Effect.Effect<boolean, unknown, R>
|
||||
readonly keys: () => Effect.Effect<Iterable<unknown>, unknown, R>
|
||||
readonly has: (item: Value) => Effect.Effect<boolean, unknown, R>
|
||||
readonly keys: () => Effect.Effect<Iterable<Value>, unknown, R>
|
||||
}
|
||||
|
||||
const loadSetRecord = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
source: unknown,
|
||||
source: Value,
|
||||
name: string,
|
||||
): Effect.Effect<SetRecord<R>, unknown, R> => {
|
||||
if (source instanceof SetObj) {
|
||||
return Effect.succeed({
|
||||
size: source.set.size,
|
||||
has: (item: unknown) => Effect.succeed(source.set.has(item)),
|
||||
has: (item: Value) => Effect.succeed(source.set.has(item)),
|
||||
keys: () => Effect.succeed(source.set.values()),
|
||||
})
|
||||
}
|
||||
if (source instanceof MapObj) {
|
||||
return Effect.succeed({
|
||||
size: source.map.size,
|
||||
has: (item: unknown) => Effect.succeed(source.map.has(item)),
|
||||
has: (item: Value) => Effect.succeed(source.map.has(item)),
|
||||
keys: () => Effect.succeed(source.map.keys()),
|
||||
})
|
||||
}
|
||||
@@ -216,9 +237,10 @@ const loadSetRecord = <R>(
|
||||
}
|
||||
return {
|
||||
size: Math.max(Math.trunc(size), 0),
|
||||
has: (item: unknown) => Effect.map(ctx.call(has, source, [item]), Boolean),
|
||||
has: (item: Value) => Effect.map(ctx.call(has, source, [item]), Boolean),
|
||||
keys: () =>
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result) => {
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result): Effect.Effect<Iterable<Value>> => {
|
||||
if (result instanceof IteratorObj) return Effect.succeed(result.source)
|
||||
if (result instanceof Arr) return Effect.succeed(result.items)
|
||||
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
|
||||
}),
|
||||
@@ -230,8 +252,8 @@ const setOperation = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
target: SetObj,
|
||||
name: string,
|
||||
source: unknown,
|
||||
): Effect.Effect<unknown, unknown, R> =>
|
||||
source: Value,
|
||||
): Effect.Effect<Value, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const other = yield* loadSetRecord(ctx, source, name)
|
||||
const copy = () => {
|
||||
@@ -310,8 +332,8 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
call: requiresNew("Set"),
|
||||
construct: (args, newTarget) => constructSet(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const self = (thisValue: unknown, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const self = (thisValue: Value, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`)
|
||||
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
|
||||
const operation = (name: string): Method => [
|
||||
name,
|
||||
1,
|
||||
@@ -338,12 +360,18 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").set.values()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").set.values()))],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").set.values())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").set.values())],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) => wrap(Array.from(self(thisValue, "entries").set.values(), (item) => wrap([item, item]))),
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.set.values()
|
||||
.map((item) => wrap([item, item])),
|
||||
),
|
||||
],
|
||||
[
|
||||
"forEach",
|
||||
@@ -365,5 +393,6 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
operation("isSupersetOf"),
|
||||
operation("isDisjointFrom"),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
import { type Method, methods } from "../interpreter/native.js"
|
||||
import {
|
||||
entries,
|
||||
get,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, isRuntimeReference } from "../interpreter/references.js"
|
||||
import { entries, get, Arr, Obj, type Value } from "../interpreter/objects.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { containsOpaqueReference } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
const consoleMethods = ["log", "info", "debug", "warn", "error", "dir", "table"]
|
||||
|
||||
@@ -42,7 +29,7 @@ export const consoleGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
|
||||
const MAX_CONSOLE_DEPTH = 32
|
||||
|
||||
const formatConsoleMessage = (name: string, args: Array<unknown>): string => {
|
||||
const formatConsoleMessage = (name: string, args: Array<Value>): string => {
|
||||
if (name === "dir") return args.length === 0 ? "undefined" : formatValue(args[0])
|
||||
if (name === "table") return formatConsoleTable(args[0], args[1])
|
||||
const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
|
||||
@@ -50,59 +37,27 @@ const formatConsoleMessage = (name: string, args: Array<unknown>): string => {
|
||||
}
|
||||
|
||||
/** One value as `console.log` shows it. */
|
||||
export const formatValue = (value: unknown): string => {
|
||||
export const formatValue = (value: Value): string => {
|
||||
if (value === undefined) return "undefined"
|
||||
if (typeof value === "string") return value
|
||||
return formatConsoleValue(value, new Set(), 0)
|
||||
}
|
||||
|
||||
const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): string => {
|
||||
const formatConsoleValue = (value: Value, seen: Set<object>, depth: number): string => {
|
||||
if (value === null || value === undefined) return "null"
|
||||
if (typeof value === "string") return JSON.stringify(value)
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
if (typeof value !== "object") return String(value)
|
||||
if (value instanceof PromiseObj) return "[Promise (await it to get its value)]"
|
||||
if (value instanceof DateObj) return coerceToString(value)
|
||||
if (value instanceof RegExpObj) return coerceToString(value)
|
||||
if (value instanceof URLObj) return coerceToString(value)
|
||||
if (value instanceof URLSearchParamsObj) return coerceToString(value)
|
||||
if (value instanceof Bytes) return `Uint8Array(${value.bytes.length}) [${value.bytes.join(",")}]`
|
||||
if (!(value instanceof Obj)) return value instanceof ToolReference ? "[opaque reference]" : String(value)
|
||||
if (depth > MAX_CONSOLE_DEPTH) return "..."
|
||||
if (seen.has(value)) return "[Circular]"
|
||||
if (value instanceof MapObj) {
|
||||
seen.add(value)
|
||||
try {
|
||||
const items = Array.from(value.map.entries(), ([key, item]) => `[${formatItems([key, item], seen, depth + 1)}]`)
|
||||
return `Map(${value.map.size}) [${items.join(",")}]`
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
}
|
||||
}
|
||||
if (value instanceof SetObj) {
|
||||
seen.add(value)
|
||||
try {
|
||||
return `Set(${value.set.size}) [${formatItems([...value.set.values()], seen, depth + 1)}]`
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
}
|
||||
}
|
||||
if (isRuntimeReference(value)) return "[opaque reference]"
|
||||
seen.add(value)
|
||||
try {
|
||||
if (value instanceof Arr) return `[${formatItems(value.items, seen, depth + 1)}]`
|
||||
if (!(value instanceof Obj)) return "[object Object]"
|
||||
return `{${entries(value)
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
|
||||
.join(",")}}`
|
||||
return value.inspect((item) => formatConsoleValue(item, seen, depth + 1))
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
}
|
||||
}
|
||||
|
||||
const formatItems = (items: Array<unknown>, seen: Set<object>, depth: number): string =>
|
||||
items.map((item) => formatConsoleValue(item, seen, depth)).join(",")
|
||||
|
||||
const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => {
|
||||
const formatConsoleTable = (value: Value, columnsArgument: Value): string => {
|
||||
if (value === undefined) return "undefined"
|
||||
if (containsOpaqueReference(value)) return "[opaque reference]"
|
||||
const columns = columnsArgument instanceof Arr ? columnsArgument.items.map(String) : undefined
|
||||
@@ -116,9 +71,9 @@ const formatConsoleTable = (value: unknown, columnsArgument: unknown): string =>
|
||||
}
|
||||
|
||||
const consoleTableRows = (
|
||||
data: unknown,
|
||||
data: Value,
|
||||
columns: ReadonlyArray<string> | undefined,
|
||||
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
|
||||
): Array<{ readonly index: string; readonly values: Record<string, Value> }> => {
|
||||
if (data instanceof Arr) {
|
||||
return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
|
||||
}
|
||||
@@ -128,7 +83,7 @@ const consoleTableRows = (
|
||||
return [{ index: "0", values: { Value: data } }]
|
||||
}
|
||||
|
||||
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
|
||||
const consoleTableValues = (value: Value, columns: ReadonlyArray<string> | undefined): Record<string, Value> => {
|
||||
if (value instanceof Obj && !(value instanceof Arr)) {
|
||||
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, get(value, column)]))
|
||||
return Object.fromEntries(entries(value))
|
||||
@@ -136,7 +91,7 @@ const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | und
|
||||
return { Value: value }
|
||||
}
|
||||
|
||||
const formatConsoleTableCell = (value: unknown): string => {
|
||||
const formatConsoleTableCell = (value: Value): string => {
|
||||
if (value === undefined) return ""
|
||||
if (typeof value === "string") return value
|
||||
return formatConsoleValue(value, new Set(), 0)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { rangeError } from "../interpreter/model.js"
|
||||
import { DateObj, Obj } from "../interpreter/objects.js"
|
||||
import { DateObj, Obj, coerceToNumber, coerceToString, type Value } from "../interpreter/objects.js"
|
||||
import { toPrimitive, toPrimitiveNumber } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const constructDate = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
|
||||
const constructDate = <R>(ctx: Interpreter<R>, args: Array<Value>, proto: Obj) => {
|
||||
if (args.length === 0) return Effect.succeed(new DateObj(proto, Date.now()))
|
||||
if (args.length === 1) {
|
||||
const arg = args[0]
|
||||
@@ -83,7 +82,7 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["UTC", 7, (_, args) => Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
|
||||
const self = (thisValue: Value, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
|
||||
const iso = (value: DateObj) => {
|
||||
if (!Number.isFinite(value.time)) throw rangeError("Invalid time value.")
|
||||
return new Date(value.time).toISOString()
|
||||
@@ -105,6 +104,13 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["toTimeString", 0, (thisValue) => new Date(self(thisValue, "toTimeString").time).toTimeString()],
|
||||
["toUTCString", 0, (thisValue) => new Date(self(thisValue, "toUTCString").time).toUTCString()],
|
||||
["toGMTString", 0, (thisValue) => new Date(self(thisValue, "toGMTString").time).toUTCString()],
|
||||
...(["toLocaleString", "toLocaleDateString", "toLocaleTimeString"] as const).map(
|
||||
(name): Method => [
|
||||
name,
|
||||
0,
|
||||
(thisValue) => new Date(self(thisValue, name).time)[name]("en-US", { timeZone: "UTC" }),
|
||||
],
|
||||
),
|
||||
...getters.map((name): Method => [name, 0, (thisValue) => new Date(self(thisValue, name).time)[name]()]),
|
||||
...setters.map(
|
||||
([name, length]): Method => [
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { IteratorSymbol, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
entries,
|
||||
get,
|
||||
hidden,
|
||||
Arr,
|
||||
HeadersObj,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
coerceToString,
|
||||
isRuntimeReference,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { applyCollectionCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { readPairs } from "./url.js"
|
||||
|
||||
// The host validates header names and values and throws its own TypeError; the program gets one of its own.
|
||||
const attempt = <T>(run: () => T): T => {
|
||||
try {
|
||||
return run()
|
||||
} catch (error) {
|
||||
throw typeError(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
}
|
||||
|
||||
const constructHeaders = <R>(ctx: Interpreter<R>, init: Value, proto: Obj): Effect.Effect<HeadersObj, unknown, R> => {
|
||||
const wrap = (headers: Headers) => new HeadersObj(proto, headers)
|
||||
if (init === undefined) return Effect.succeed(wrap(new Headers()))
|
||||
return Effect.gen(function* () {
|
||||
const pairs = init instanceof Obj ? yield* readPairs(ctx, init, "new Headers(...)") : undefined
|
||||
if (pairs !== undefined) return wrap(attempt(() => new Headers(pairs)))
|
||||
if (!(init instanceof Obj) || isRuntimeReference(init)) {
|
||||
throw typeError("new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.")
|
||||
}
|
||||
return wrap(
|
||||
attempt(() => new Headers(Object.fromEntries(entries(init).map(([key, value]) => [key, coerceToString(value)])))),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const headersGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Headers
|
||||
const headers = constructor<R>(builtins, proto, {
|
||||
name: "Headers",
|
||||
call: requiresNew("Headers"),
|
||||
construct: (args, newTarget) => constructHeaders(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const self = (thisValue: Value, name: string) => receiver(HeadersObj, thisValue, `Headers.prototype.${name}`)
|
||||
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
|
||||
const arg = (args: Array<Value>, index: number): string => coerceToString(args[index])
|
||||
const requireArgs = (name: string, args: Array<Value>, count: number): void => {
|
||||
if (args.length < count) throw typeError(`Headers.${name} requires ${count} argument${count === 1 ? "" : "s"}.`)
|
||||
}
|
||||
methods(builtins, proto, [
|
||||
[
|
||||
"append",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
requireArgs("append", args, 2)
|
||||
const target = self(thisValue, "append").headers
|
||||
attempt(() => target.append(arg(args, 0), arg(args, 1)))
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
[
|
||||
"delete",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
requireArgs("delete", args, 1)
|
||||
const target = self(thisValue, "delete").headers
|
||||
attempt(() => target.delete(arg(args, 0)))
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
[
|
||||
"get",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
requireArgs("get", args, 1)
|
||||
const target = self(thisValue, "get").headers
|
||||
return attempt(() => target.get(arg(args, 0)))
|
||||
},
|
||||
],
|
||||
["getSetCookie", 0, (thisValue) => wrap(self(thisValue, "getSetCookie").headers.getSetCookie())],
|
||||
[
|
||||
"has",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
requireArgs("has", args, 1)
|
||||
const target = self(thisValue, "has").headers
|
||||
return attempt(() => target.has(arg(args, 0)))
|
||||
},
|
||||
],
|
||||
[
|
||||
"set",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
requireArgs("set", args, 2)
|
||||
const target = self(thisValue, "set").headers
|
||||
attempt(() => target.set(arg(args, 0), arg(args, 1)))
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
// Iterator.from because Bun's Headers typings predate iterator helpers; the runtime iterators already have them.
|
||||
[
|
||||
"keys",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "keys").headers.keys())),
|
||||
],
|
||||
[
|
||||
"values",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "values").headers.values())),
|
||||
],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
[
|
||||
"forEach",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
requireArgs("forEach", args, 1)
|
||||
const target = self(thisValue, "forEach")
|
||||
const apply = applyCollectionCallback(ctx, args[0], "Headers.forEach")
|
||||
return Effect.gen(function* () {
|
||||
for (const [key, value] of Array.from(target.headers.entries())) yield* apply([value, key, target])
|
||||
return undefined
|
||||
})
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return headers
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { methods, receiver } from "../interpreter/native.js"
|
||||
import { IteratorObj, record } from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
// Every built-in collection iterator shares the Iterator prototype; JS gives each collection its own, which is only
|
||||
// observable through getPrototypeOf.
|
||||
export const iteratorGlobals = <R>(ctx: Interpreter<R>): void => {
|
||||
const builtins = ctx.builtins
|
||||
methods(builtins, builtins.Iterator, [
|
||||
[
|
||||
"next",
|
||||
0,
|
||||
(thisValue) => {
|
||||
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").source.next()
|
||||
return record(builtins.Object, { value: step.value, done: Boolean(step.done) })
|
||||
},
|
||||
],
|
||||
])
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { checkStringLength } from "../interpreter/limits.js"
|
||||
import { syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { fromJson, toJson } from "../data.js"
|
||||
import { get, keys, Arr, Obj, record, remove, set } from "../interpreter/objects.js"
|
||||
import { get, keys, Arr, Obj, record, remove, set, type Value } from "../interpreter/objects.js"
|
||||
|
||||
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const json = new Obj(ctx.builtins.Object)
|
||||
@@ -17,7 +17,7 @@ export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return json
|
||||
}
|
||||
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
|
||||
|
||||
@@ -31,7 +31,7 @@ const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unkn
|
||||
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
|
||||
|
||||
const apply = applyCollectionCallback(ctx, args[1], "JSON.parse")
|
||||
const visit = (holder: Obj, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
const visit = (holder: Obj, key: string): Effect.Effect<Value, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = get(holder, key)
|
||||
if (value instanceof Obj) {
|
||||
@@ -46,7 +46,7 @@ const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unkn
|
||||
return visit(record(ctx.builtins.Object, { "": parsed }), "")
|
||||
}
|
||||
|
||||
const stringify = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const stringify = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const space = args[2]
|
||||
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
|
||||
const replacer = args[1]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { constants, type Method, methods } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { Obj } from "../interpreter/objects.js"
|
||||
import { Obj, type Value } from "../interpreter/objects.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
@@ -14,7 +14,7 @@ declare global {
|
||||
|
||||
// Validate only the arguments a method consumes; like JS, extras are ignored
|
||||
// (so built-ins work as callbacks receiving (element, index, array)).
|
||||
const number = (name: string, args: Array<unknown>, index: number): number => {
|
||||
const number = (name: string, args: Array<Value>, index: number): number => {
|
||||
if (index >= args.length) return Number.NaN
|
||||
const arg = args[index]
|
||||
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { constructor, constants, methods } from "../interpreter/native.js"
|
||||
import { coerceToString, type Value } from "../interpreter/objects.js"
|
||||
import { rangeError, typeError } from "../interpreter/model.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coercion, coerceToString } from "./value.js"
|
||||
import { coercion } from "./value.js"
|
||||
|
||||
export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
@@ -39,17 +40,18 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["parseFloat", 1, (_, args) => parseFloat(coerceToString(args[0]))],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string): number => {
|
||||
const self = (thisValue: Value, name: string): number => {
|
||||
if (typeof thisValue === "number") return thisValue
|
||||
throw typeError(`Number.prototype.${name} requires that 'this' be a Number.`)
|
||||
}
|
||||
const optNum = (name: string, arg: unknown): number | undefined => {
|
||||
const optNum = (name: string, arg: Value): number | undefined => {
|
||||
if (arg === undefined) return undefined
|
||||
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
|
||||
return arg
|
||||
}
|
||||
methods(builtins, builtins.Number, [
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
|
||||
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
|
||||
[
|
||||
"toExponential",
|
||||
1,
|
||||
@@ -88,7 +90,7 @@ export const booleanGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
length: 1,
|
||||
call: coercion(ctx, "Boolean").call,
|
||||
})
|
||||
const self = (thisValue: unknown, name: string): boolean => {
|
||||
const self = (thisValue: Value, name: string): boolean => {
|
||||
if (typeof thisValue === "boolean") return thisValue
|
||||
throw typeError(`Boolean.prototype.${name} requires that 'this' be a Boolean.`)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
typeError,
|
||||
} from "../interpreter/model.js"
|
||||
import {
|
||||
Callable,
|
||||
define,
|
||||
entries,
|
||||
enumerableKeys,
|
||||
@@ -20,23 +19,20 @@ import {
|
||||
keys,
|
||||
own,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
set,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import { invoke, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { groupBy } from "./collections.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
// ToObject for enumeration.
|
||||
export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: unknown, node?: AstNode): Obj => {
|
||||
export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: Value, node?: AstNode): Obj => {
|
||||
if (value === null || value === undefined) {
|
||||
throw typeError(`${label} cannot convert ${describeValue(value)} to an object.`, node)
|
||||
}
|
||||
@@ -54,7 +50,7 @@ export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: u
|
||||
return new Obj(ctx.builtins.Object)
|
||||
}
|
||||
|
||||
export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<unknown>): unknown => {
|
||||
export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<Value>): Value => {
|
||||
const target = args[0]
|
||||
// JS would box a primitive target; wrappers and primitives cannot hold fields here.
|
||||
if (!(target instanceof Obj)) {
|
||||
@@ -75,7 +71,7 @@ export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<unknown>): unkn
|
||||
return target
|
||||
}
|
||||
|
||||
const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effect<Obj, unknown, R> => {
|
||||
const objectFromEntries = <R>(ctx: Interpreter<R>, source: Value): Effect.Effect<Obj, unknown, R> => {
|
||||
const out = new Obj(ctx.builtins.Object)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
@@ -98,29 +94,24 @@ const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effe
|
||||
})
|
||||
}
|
||||
|
||||
export const classTag = (value: unknown): string => {
|
||||
const classTag = (value: Value): string => {
|
||||
if (value === null) return "Null"
|
||||
if (value === undefined) return "Undefined"
|
||||
if (value instanceof Arr) return "Array"
|
||||
if (value instanceof Callable) return "Function"
|
||||
if (value instanceof ErrorObj) return "Error"
|
||||
if (value instanceof DateObj) return "Date"
|
||||
if (value instanceof RegExpObj) return "RegExp"
|
||||
if (value instanceof Bytes) return "Uint8Array"
|
||||
if (value instanceof Obj) return value.tag
|
||||
if (typeof value === "string") return "String"
|
||||
if (typeof value === "number") return "Number"
|
||||
if (typeof value === "boolean") return "Boolean"
|
||||
return "Object"
|
||||
}
|
||||
|
||||
const propertyKey = (value: unknown): PropertyKey =>
|
||||
const propertyKey = (value: Value): PropertyKey =>
|
||||
value === AsyncIteratorSymbol || value === IteratorSymbol ? value : coerceToString(value)
|
||||
|
||||
// Object constructs identically with or without new, like JS. Only `keys` copies its result into the
|
||||
// program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values.
|
||||
export const objectGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const construct = (args: Array<unknown>): unknown => {
|
||||
const construct = (args: Array<Value>): Value => {
|
||||
const first = args[0]
|
||||
if (first === null || first === undefined) return new Obj(builtins.Object)
|
||||
if (first instanceof Obj) return first
|
||||
@@ -196,7 +187,7 @@ export const objectGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
true,
|
||||
],
|
||||
["toString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => invoke(ctx, thisValue, "toString", "Object.prototype.toLocaleString")],
|
||||
[
|
||||
"valueOf",
|
||||
0,
|
||||
|
||||
@@ -2,9 +2,18 @@ import { Effect } from "effect"
|
||||
import type { Builtins } from "../interpreter/intrinsics.js"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { define, defineAccessor, Arr, Obj, RegExpObj, record } from "../interpreter/objects.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
Arr,
|
||||
Obj,
|
||||
RegExpObj,
|
||||
record,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const flagProperties = [
|
||||
"hasIndices",
|
||||
@@ -23,7 +32,7 @@ const regexFailureReason = (error: unknown): string =>
|
||||
const escapeRegexHint =
|
||||
'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.'
|
||||
|
||||
export const toHostRegex = (arg: unknown, method: string, extraFlags = ""): RegExp => {
|
||||
export const toHostRegex = (arg: Value, method: string, extraFlags = ""): RegExp => {
|
||||
// Native parity: an undefined pattern behaves as an empty pattern.
|
||||
if (arg === undefined) return new RegExp("", extraFlags)
|
||||
if (arg instanceof RegExpObj) return arg.regex
|
||||
@@ -53,7 +62,7 @@ export const matchToValue = (builtins: Builtins, match: RegExpMatchArray): Arr =
|
||||
return result
|
||||
}
|
||||
|
||||
export const constructRegExp = (builtins: Builtins, args: Array<unknown>, proto: Obj = builtins.RegExp): RegExpObj => {
|
||||
export const constructRegExp = (builtins: Builtins, args: Array<Value>, proto: Obj = builtins.RegExp): RegExpObj => {
|
||||
const first = args[0]
|
||||
const pattern = first instanceof RegExpObj ? first.regex.source : first === undefined ? "" : coerceToString(first)
|
||||
const flagsArg = args[1]
|
||||
@@ -96,7 +105,7 @@ export const regexpGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(RegExpObj, thisValue, `RegExp.prototype.${name}`)
|
||||
const self = (thisValue: Value, name: string) => receiver(RegExpObj, thisValue, `RegExp.prototype.${name}`)
|
||||
defineAccessor(proto, "source", (thisValue) => self(thisValue, "source").regex.source)
|
||||
defineAccessor(proto, "flags", (thisValue) => self(thisValue, "flags").regex.flags)
|
||||
// The host regex holds the only lastIndex, so exec/test and the String methods share one counter.
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, type Method, methods } from "../interpreter/native.js"
|
||||
import { constructor, fn, type Impl, type Method, methods } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { Arr, PromiseObj, RegExpObj, record } from "../interpreter/objects.js"
|
||||
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
hidden,
|
||||
Arr,
|
||||
IteratorObj,
|
||||
RegExpObj,
|
||||
record,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, typeofValue } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, isSupportedCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { matchToValue, toHostRegex } from "./regexp.js"
|
||||
import { coerceToNumber, coerceToString, coercion } from "./value.js"
|
||||
import { coercion } from "./value.js"
|
||||
|
||||
// console is intercepted by the interpreter before reaching here.
|
||||
const requireDataArgument = (name: string, index: number, arg: unknown): unknown => {
|
||||
const requireDataArgument = (name: string, index: number, arg: Value): Value => {
|
||||
if (containsOpaqueReference(arg)) {
|
||||
throw invalidData(`String.${name} expects argument ${index + 1} to be a data value.`)
|
||||
}
|
||||
@@ -29,21 +39,23 @@ const replaceWithCallback = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
value: string,
|
||||
name: "replace" | "replaceAll",
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
args: Array<Value>,
|
||||
): Effect.Effect<Value, unknown, R> => {
|
||||
const builtins = ctx.builtins
|
||||
const apply = applyCollectionCallback(ctx, args[1], `String.${name}`)
|
||||
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
|
||||
const collect = (...callbackArgs: Array<unknown>): string => {
|
||||
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<Value> }> = []
|
||||
// The host calls back with (match, ...captures, offset, string, groups?); only groups is not already a Value.
|
||||
const collect = (
|
||||
...callbackArgs: Array<string | number | undefined | Record<string, string | undefined>>
|
||||
): string => {
|
||||
const match = callbackArgs[0]
|
||||
const groups = callbackArgs[callbackArgs.length - 1]
|
||||
const hasGroups = groups !== null && typeof groups === "object"
|
||||
const hasGroups = typeof callbackArgs.at(-1) === "object"
|
||||
const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)]
|
||||
if (typeof match !== "string" || typeof offset !== "number") {
|
||||
throw typeError(`String.${name} produced an invalid replacement match.`)
|
||||
}
|
||||
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(builtins.Object, groups as Record<string, unknown>)
|
||||
matches.push({ match, offset, args: callbackArgs })
|
||||
const args = callbackArgs.map((arg) => (typeof arg === "object" ? record(builtins.Object, arg) : arg))
|
||||
matches.push({ match, offset, args })
|
||||
return match
|
||||
}
|
||||
|
||||
@@ -63,10 +75,7 @@ const replaceWithCallback = <R>(
|
||||
let end = 0
|
||||
for (const match of matches) {
|
||||
const replacement = yield* apply(match.args)
|
||||
output.push(
|
||||
value.slice(end, match.offset),
|
||||
replacement instanceof PromiseObj ? "[object Promise]" : coerceToString(replacement),
|
||||
)
|
||||
output.push(value.slice(end, match.offset), coerceToString(replacement))
|
||||
end = match.offset + match.match.length
|
||||
}
|
||||
output.push(value.slice(end))
|
||||
@@ -99,7 +108,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
codeUnits("fromCodePoint", String.fromCodePoint),
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string): string => {
|
||||
const self = (thisValue: Value, name: string): string => {
|
||||
if (typeof thisValue === "string") return thisValue
|
||||
if (thisValue === null || thisValue === undefined) {
|
||||
throw typeError(`String.prototype.${name} called on null or undefined.`)
|
||||
@@ -107,26 +116,26 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return coerceToString(thisValue)
|
||||
}
|
||||
// Coerce arguments like native JS; opaque runtime references still reject.
|
||||
const str = (name: string, args: Array<unknown>, index: number): string =>
|
||||
const str = (name: string, args: Array<Value>, index: number): string =>
|
||||
coerceToString(requireDataArgument(name, index, args[index]))
|
||||
const num = (name: string, args: Array<unknown>, index: number): number =>
|
||||
const num = (name: string, args: Array<Value>, index: number): number =>
|
||||
coerceToNumber(requireDataArgument(name, index, args[index]))
|
||||
const optNum = (name: string, args: Array<unknown>, index: number): number | undefined =>
|
||||
const optNum = (name: string, args: Array<Value>, index: number): number | undefined =>
|
||||
args[index] === undefined ? undefined : num(name, args, index)
|
||||
const optStr = (name: string, args: Array<unknown>, index: number): string | undefined =>
|
||||
const optStr = (name: string, args: Array<Value>, index: number): string | undefined =>
|
||||
args[index] === undefined ? undefined : str(name, args, index)
|
||||
const rejectRegex = (name: string, args: Array<unknown>): void => {
|
||||
const rejectRegex = (name: string, args: Array<Value>): void => {
|
||||
if (args[0] instanceof RegExpObj) {
|
||||
throw typeError(
|
||||
`String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
const simple = (name: string, length: number, op: (value: string, args: Array<unknown>) => unknown): Method => [
|
||||
name,
|
||||
length,
|
||||
(thisValue, args) => op(self(thisValue, name), args),
|
||||
]
|
||||
const simple = (
|
||||
name: string,
|
||||
length: number,
|
||||
op: (value: string, args: Array<Value>) => ReturnType<Impl>,
|
||||
): Method => [name, length, (thisValue, args) => op(self(thisValue, name), args)]
|
||||
const replace = (name: "replace" | "replaceAll") =>
|
||||
simple(name, 2, (value, args) => {
|
||||
if (isSupportedCallback(args[1])) return replaceWithCallback(ctx, value, name, args)
|
||||
@@ -150,6 +159,8 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
simple("valueOf", 0, (value) => value),
|
||||
simple("toLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("toLocaleLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toLocaleUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("trim", 0, (value) => value.trim()),
|
||||
simple("trimStart", 0, (value) => value.trimStart()),
|
||||
simple("trimLeft", 0, (value) => value.trimStart()),
|
||||
@@ -216,7 +227,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
`String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`,
|
||||
)
|
||||
}
|
||||
const matches: Array<unknown> = []
|
||||
const matches: Array<Value> = []
|
||||
for (const match of value.matchAll(pattern)) {
|
||||
checkArrayLength(matches.length + 1)
|
||||
matches.push(matchToValue(builtins, match))
|
||||
@@ -258,5 +269,16 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return joined
|
||||
}),
|
||||
])
|
||||
define(
|
||||
builtins.String,
|
||||
IteratorSymbol,
|
||||
fn(
|
||||
builtins,
|
||||
"[Symbol.iterator]",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
|
||||
),
|
||||
hidden,
|
||||
)
|
||||
return string
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { PendingThrow, typeError, uriError } from "../interpreter/model.js"
|
||||
import { defineAccessor, entries, isWrapper, Arr, Obj, URLObj, URLSearchParamsObj } from "../interpreter/objects.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import { IteratorSymbol, PendingThrow, typeError, uriError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
entries,
|
||||
get,
|
||||
hidden,
|
||||
Arr,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
coerceToString,
|
||||
isRuntimeReference,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
const urlProperties = [
|
||||
"href",
|
||||
@@ -40,12 +52,12 @@ export const uriGlobal = <R>(ctx: Interpreter<R>, name: UriFunction) =>
|
||||
}
|
||||
})
|
||||
|
||||
const urlArgument = (value: unknown): string => (value instanceof URLObj ? value.url.href : coerceToString(value))
|
||||
const urlArgument = (value: Value): string => (value instanceof URLObj ? value.url.href : coerceToString(value))
|
||||
|
||||
export const urlGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.URL
|
||||
const construct = (args: Array<unknown>, into: Obj): URLObj => {
|
||||
const construct = (args: Array<Value>, into: Obj): URLObj => {
|
||||
if (args.length === 0) {
|
||||
throw typeError("new URL(...) requires a URL string and an optional base URL.")
|
||||
}
|
||||
@@ -80,7 +92,7 @@ export const urlGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
]
|
||||
methods(builtins, url, [parse("canParse"), parse("parse")])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(URLObj, thisValue, `URL.prototype.${name}`)
|
||||
const self = (thisValue: Value, name: string) => receiver(URLObj, thisValue, `URL.prototype.${name}`)
|
||||
for (const name of urlProperties) {
|
||||
defineAccessor(
|
||||
proto,
|
||||
@@ -107,12 +119,10 @@ export const urlGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return url
|
||||
}
|
||||
|
||||
const readPair = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<Array<string>, unknown, R> =>
|
||||
const readPair = <R>(ctx: Interpreter<R>, value: Value, label: string): Effect.Effect<Array<string>, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(value)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("new URLSearchParams(...) expects iterable [name, value] pairs.")
|
||||
}
|
||||
if (cursor === undefined) throw typeError(`${label} expects iterable [name, value] pairs.`)
|
||||
const items: Array<string> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -126,9 +136,32 @@ const readPair = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<Array<s
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Reads a synchronous iterable of `[name, value]` pairs as strings; `undefined` when `init` is not iterable. As in
|
||||
* WebIDL, the whole sequence is converted before any pair's length is checked.
|
||||
*/
|
||||
export const readPairs = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
init: Value,
|
||||
label: string,
|
||||
): Effect.Effect<Array<[string, string]> | undefined, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(init)
|
||||
if (cursor === undefined) return undefined
|
||||
const pairs: Array<Array<string>> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) {
|
||||
if (pairs.some((entry) => entry.length !== 2)) throw typeError(`${label} expects iterable [name, value] pairs.`)
|
||||
return pairs as Array<[string, string]>
|
||||
}
|
||||
pairs.push(yield* preserveConsumerError(cursor, readPair(ctx, step.value, label)))
|
||||
}
|
||||
})
|
||||
|
||||
const constructURLSearchParams = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
init: unknown,
|
||||
init: Value,
|
||||
proto: Obj,
|
||||
): Effect.Effect<URLSearchParamsObj, unknown, R> => {
|
||||
const wrap = (params: URLSearchParams) => new URLSearchParamsObj(proto, params)
|
||||
@@ -139,24 +172,11 @@ const constructURLSearchParams = <R>(
|
||||
return Effect.succeed(wrap(new URLSearchParams(coerceToString(init))))
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(init)
|
||||
if (cursor !== undefined) {
|
||||
const pairs: Array<Array<string>> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) {
|
||||
if (pairs.some((entry) => entry.length !== 2)) {
|
||||
throw typeError("new URLSearchParams(...) expects iterable [name, value] pairs.")
|
||||
}
|
||||
return wrap(new URLSearchParams(pairs.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])))
|
||||
}
|
||||
pairs.push(yield* preserveConsumerError(cursor, readPair(ctx, step.value)))
|
||||
}
|
||||
}
|
||||
const pairs = yield* readPairs(ctx, init, "new URLSearchParams(...)")
|
||||
if (pairs !== undefined) return wrap(new URLSearchParams(pairs))
|
||||
if (isRuntimeReference(init)) {
|
||||
throw typeError("new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.")
|
||||
}
|
||||
if (isWrapper(init)) return wrap(new URLSearchParams())
|
||||
if (!(init instanceof Obj)) {
|
||||
throw typeError(
|
||||
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
|
||||
@@ -176,11 +196,11 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
call: requiresNew("URLSearchParams"),
|
||||
construct: (args, newTarget) => constructURLSearchParams(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const self = (thisValue: unknown, name: string) =>
|
||||
const self = (thisValue: Value, name: string) =>
|
||||
receiver(URLSearchParamsObj, thisValue, `URLSearchParams.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const arg = (args: Array<unknown>, index: number): string => coerceToString(args[index])
|
||||
const requireArgs = (name: string, args: Array<unknown>, count: number): void => {
|
||||
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
|
||||
const arg = (args: Array<Value>, index: number): string => coerceToString(args[index])
|
||||
const requireArgs = (name: string, args: Array<Value>, count: number): void => {
|
||||
if (args.length < count) {
|
||||
throw typeError(`URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`)
|
||||
}
|
||||
@@ -249,14 +269,9 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").params.keys()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").params.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
wrap(Array.from(self(thisValue, "entries").params.entries(), ([key, value]) => wrap([key, value]))),
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").params.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").params.values())],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
|
||||
[
|
||||
"forEach",
|
||||
@@ -272,5 +287,6 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return searchParams
|
||||
}
|
||||
|
||||
@@ -1,62 +1,13 @@
|
||||
import { fn } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
get,
|
||||
isWrapper,
|
||||
type Native,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
MapObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import { coerceToNumber, coerceToString, type Native, type Value } from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
|
||||
|
||||
/** The built-in string form of a value, without consulting program-defined `toString` methods. */
|
||||
export const coerceToString = (value: unknown): string => {
|
||||
if (value === null) return "null"
|
||||
if (value === undefined) return "undefined"
|
||||
if (value instanceof DateObj) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
|
||||
if (value instanceof RegExpObj) return `/${value.regex.source}/${value.regex.flags}`
|
||||
if (value instanceof MapObj) return "[object Map]"
|
||||
if (value instanceof SetObj) return "[object Set]"
|
||||
if (value instanceof URLObj) return value.url.href
|
||||
if (value instanceof URLSearchParamsObj) return value.params.toString()
|
||||
if (value instanceof Bytes) return value.bytes.join(",")
|
||||
if (value instanceof ErrorObj) {
|
||||
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
|
||||
const name = get(value, "name")
|
||||
const message = get(value, "message")
|
||||
const shownName = typeof name === "string" ? name : "Error"
|
||||
const shownMessage = typeof message === "string" ? message : ""
|
||||
if (shownMessage === "") return shownName
|
||||
if (shownName === "") return shownMessage
|
||||
return `${shownName}: ${shownMessage}`
|
||||
}
|
||||
if (value instanceof Arr) {
|
||||
return value.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
}
|
||||
if (typeof value === "object") return "[object Object]"
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export const coerceToNumber = (value: unknown): number => {
|
||||
if (value instanceof DateObj) return value.time
|
||||
if (value instanceof Bytes) return Number(coerceToString(value))
|
||||
if (isWrapper(value)) return Number.NaN
|
||||
if (value instanceof Arr) return Number(coerceToString(value))
|
||||
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
|
||||
}
|
||||
|
||||
export type Coercion = "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN"
|
||||
|
||||
const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<unknown>): unknown => {
|
||||
const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<Value>): Value => {
|
||||
// Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the
|
||||
// other coercers match native through the undefined-argument path below.
|
||||
if (args.length === 0) {
|
||||
@@ -64,15 +15,6 @@ const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<unknown>): u
|
||||
if (name === "String") return ""
|
||||
}
|
||||
const raw = args[0]
|
||||
if (isWrapper(raw)) {
|
||||
if (name === "Boolean") return true
|
||||
if (name === "Number") return coerceToNumber(raw)
|
||||
if (name === "String") return coerceToString(raw)
|
||||
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
|
||||
if (name === "isNaN") return Number.isNaN(coerceToNumber(raw))
|
||||
if (name === "parseInt") return parseInt(coerceToString(raw))
|
||||
return parseFloat(coerceToString(raw))
|
||||
}
|
||||
if (name === "Number") return coerceToNumber(raw)
|
||||
if (name === "Boolean") return Boolean(raw)
|
||||
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { fn, methods } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { Bytes, Obj } from "../interpreter/objects.js"
|
||||
import { Bytes, Obj, coerceToString } from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies. Invalid input is a
|
||||
// TypeError as well; browsers throw a DOMException named InvalidCharacterError, which CodeMode does not have.
|
||||
|
||||
@@ -298,21 +298,18 @@ const cases = [
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/keys/iteration.js",
|
||||
code: `return ["a", "b", "c"].keys()`,
|
||||
expected: [0, 1, 2],
|
||||
code: `const it = ["a", "b", "c"].keys(); return [it.next(), it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: 0, done: false }, { value: 1, done: false }, { value: 2, done: false }, { done: true }],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/values/iteration.js",
|
||||
code: `return ["a", "b", "c"].values()`,
|
||||
expected: ["a", "b", "c"],
|
||||
code: `const it = ["a", "b", "c"].values(); return [it.next(), it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: "a", done: false }, { value: "b", done: false }, { value: "c", done: false }, { done: true }],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/entries/iteration.js",
|
||||
code: `return ["a", "b"].entries()`,
|
||||
expected: [
|
||||
[0, "a"],
|
||||
[1, "b"],
|
||||
],
|
||||
code: `const it = ["a", "b"].entries(); return [it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: [0, "a"], done: false }, { value: [1, "b"], done: false }, { done: true }],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/isArray/15.4.3.2-0-3.js",
|
||||
|
||||
@@ -15,7 +15,23 @@ const error = async (code: string) => {
|
||||
return result.error
|
||||
}
|
||||
|
||||
describe("source syntax", () => {
|
||||
test("rejects TypeScript-only syntax", async () => {
|
||||
expect((await error(`const value: number = 1; return value`)).kind).toBe("ParseError")
|
||||
})
|
||||
})
|
||||
|
||||
describe("error identity", () => {
|
||||
test("Error.isError is true for every Error value and nothing else", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const caught = (() => { try { null.foo } catch (error) { return error } })()
|
||||
return [Error.isError(new Error("x")), Error.isError(new RangeError("x")), Error.isError(caught),
|
||||
Error.isError({ name: "Error", message: "x" }), Error.isError("Error"), Error.isError(null)]
|
||||
`),
|
||||
).toEqual([true, true, true, false, false, false])
|
||||
})
|
||||
|
||||
test("awaiting the same rejected promise twice yields the same error object", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
@@ -89,6 +105,24 @@ describe("uncaught program throws", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("source locations", () => {
|
||||
test("uses the submitted line and 1-based column", async () => {
|
||||
const failure = await error("const value = 1\nreturn value()")
|
||||
expect(failure.location).toEqual({ line: 2, column: 8 })
|
||||
expect(failure.message).toBe("TypeError: value is not a function. (line 2, col 8)")
|
||||
})
|
||||
|
||||
test("names a missing method on a call instead of the previous line", async () => {
|
||||
const failure = await error(`// Try search with different namespaces
|
||||
for (const ns of ["github", "tools.github", "tools", ""]) {
|
||||
const s = await search({query: "star", namespace: ns, limit: 100}).catch(e=>({items:[],error:String(e)}));
|
||||
return s
|
||||
}`)
|
||||
expect(failure.location).toEqual({ line: 3, column: 19 })
|
||||
expect(failure.message).toBe("TypeError: search(...).catch is not a function. (line 3, col 19)")
|
||||
})
|
||||
})
|
||||
|
||||
describe("host errors escaping built-ins", () => {
|
||||
test("become the same-named program error", async () => {
|
||||
expect(
|
||||
@@ -115,7 +149,7 @@ describe("host errors escaping built-ins", () => {
|
||||
"(line 1, col 14)",
|
||||
)
|
||||
expect((await error(`let p; p = Promise.resolve().then(() => p); return await p`)).message).toEndWith(
|
||||
"(line 2, col 5)",
|
||||
"(line 1, col 12)",
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -128,6 +128,34 @@ describe("values are converted at the boundary, never shared", () => {
|
||||
expect([...(held[0] as Set<{ z: number }>)][0]).toEqual({ z: 1 })
|
||||
})
|
||||
|
||||
test("Headers cross as copies in both directions", async () => {
|
||||
const stored = new Headers({ "X-A": "1" })
|
||||
const target = CodeMode.make({
|
||||
extensions: [
|
||||
Extension.make({
|
||||
name: "http",
|
||||
globals: {
|
||||
headers: () => stored,
|
||||
keep: (value: Headers) => {
|
||||
held.push(value)
|
||||
return value
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
held.length = 0
|
||||
expect(
|
||||
await value(
|
||||
`const h = headers(); h.set("x-a", "2"); const back = keep(h); back.set("x-a", "3"); return [h instanceof Headers, h.get("x-a"), back === h, back.get("x-a"), [...back]]`,
|
||||
target,
|
||||
),
|
||||
).toEqual([true, "2", false, "3", [["x-a", "3"]]])
|
||||
expect(stored.get("x-a")).toBe("1")
|
||||
expect(held[0]).toBeInstanceOf(Headers)
|
||||
expect((held[0] as Headers).get("x-a")).toBe("2")
|
||||
})
|
||||
|
||||
test("bytes cross as copies in both directions; ArrayBuffer comes in as Uint8Array", async () => {
|
||||
const stored = new Uint8Array([1, 2, 3])
|
||||
const target = CodeMode.make({
|
||||
@@ -192,8 +220,9 @@ describe("values are converted at the boundary, never shared", () => {
|
||||
expect(held[1]).toBeInstanceOf(Error)
|
||||
})
|
||||
|
||||
test("functions, promises, and symbols cannot be passed in", async () => {
|
||||
test("functions, promises, iterators, and symbols cannot be passed in", async () => {
|
||||
expect((await failure(`keep(() => 1)`)).message).toContain("Argument 1 to keep contains a function")
|
||||
expect((await failure(`keep([1].keys())`)).message).toContain("Argument 1 to keep contains an iterator")
|
||||
expect((await failure(`keep(later(1))`)).message).toContain("un-awaited Promise")
|
||||
expect((await failure(`keep(Symbol.iterator)`)).message).toContain("Argument 1 to keep contains a symbol")
|
||||
})
|
||||
|
||||
@@ -529,6 +529,6 @@ describe("Test262 for-await-of adaptations", () => {
|
||||
const result = await execute(`for await (const item of { values: [1, 2] }) {}`)
|
||||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
expect(result.error.message).toContain("or custom iterator value")
|
||||
expect(result.error.message).toContain("requires an iterable value, received a data object")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1030,7 +1030,7 @@ describe("confined generators", () => {
|
||||
const params = new URLSearchParams(entries())
|
||||
return [events, params.toString()]
|
||||
`),
|
||||
).toEqual([["first", "second", "pair close", "outer close"], "%5Bobject+Object%5D=2"])
|
||||
).toEqual([["first", "second", "pair close", "outer close"], "%5Bobject+Promise%5D=2"])
|
||||
})
|
||||
|
||||
test("validates URLSearchParams pair lengths after converting the outer sequence", async () => {
|
||||
|
||||
@@ -122,10 +122,10 @@ describe("Map.groupBy Test262 parity", () => {
|
||||
grouped.get(1),
|
||||
grouped.get("1"),
|
||||
grouped.has(stringable),
|
||||
grouped.keys().length,
|
||||
[...grouped.keys()].length,
|
||||
parity.get("even"),
|
||||
parity.get("odd"),
|
||||
lengths.keys(),
|
||||
[...lengths.keys()],
|
||||
lengths.get(5),
|
||||
lengths.get(4),
|
||||
]
|
||||
@@ -162,7 +162,7 @@ describe("Map.groupBy Test262 parity", () => {
|
||||
await value(`
|
||||
const grouped = Map.groupBy("🥰💩🙏😈", (char) => char < "🙏" ? "before" : "after")
|
||||
const empty = Map.groupBy([], () => { throw new Error("not called") })
|
||||
return [grouped.keys(), grouped.get("before"), grouped.get("after"), empty.size]
|
||||
return [[...grouped.keys()], grouped.get("before"), grouped.get("after"), empty.size]
|
||||
`),
|
||||
).toEqual([["after", "before"], ["💩", "😈"], ["🥰", "🙏"], 0])
|
||||
})
|
||||
|
||||
@@ -502,9 +502,15 @@ describe("CodeMode-specific array behavior", () => {
|
||||
expect(err.message).toContain("circular")
|
||||
})
|
||||
|
||||
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
||||
test("indexOf and lastIndexOf with no argument search for undefined", async () => {
|
||||
expect(await value(`return [1, undefined, 3].indexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, undefined, 3].lastIndexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, 2, 3].indexOf()`)).toBe(-1)
|
||||
})
|
||||
|
||||
test("keys/values/entries return iterators usable with for...of and spread", async () => {
|
||||
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
||||
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
||||
expect(await value(`return [...["x","y"].values()]`)).toEqual(["x", "y"])
|
||||
expect(
|
||||
await value(`
|
||||
const out = []
|
||||
@@ -940,6 +946,12 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
expect(await value(`try { JSON.rawJSON("1") } catch (e) { return e.message }`)).toBe(
|
||||
"JSON.rawJSON is not a function.",
|
||||
)
|
||||
expect(await value(`try { search({ query: "star" }).catch(() => 1) } catch (e) { return e.message }`)).toBe(
|
||||
"search(...).catch is not a function.",
|
||||
)
|
||||
expect(
|
||||
await value(`const foo = () => ({ bar: () => ({}) }); try { foo().bar().baz() } catch (e) { return e.message }`),
|
||||
).toBe("foo(...).bar(...).baz is not a function.")
|
||||
})
|
||||
|
||||
test("built-ins are objects on a real prototype chain", async () => {
|
||||
@@ -961,6 +973,17 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("async function line breaks", () => {
|
||||
test("a line break between function and the name is an async function", async () => {
|
||||
expect(await value(`async function\nfoo() { return 1 }\nreturn await foo()`)).toBe(1)
|
||||
})
|
||||
|
||||
test("a line break between async and function is not an async function", async () => {
|
||||
const failure = await error(`async\nfunction foo() { return 1 }\nreturn foo()`)
|
||||
expect(failure.message).toContain("Unknown identifier 'async'")
|
||||
})
|
||||
})
|
||||
|
||||
describe("functions are objects", () => {
|
||||
test("name follows NamedEvaluation and length counts required parameters", async () => {
|
||||
expect(
|
||||
|
||||
@@ -198,10 +198,10 @@ describe("Set composition Test262 parity", () => {
|
||||
keys: () => [-0],
|
||||
}
|
||||
return [
|
||||
1 / new Set([1]).union(setlike).values()[1] === Infinity,
|
||||
1 / new Set([0, 1, 2]).intersection(setlike).values()[0] === Infinity,
|
||||
1 / [...new Set([1]).union(setlike)][1] === Infinity,
|
||||
1 / [...new Set([0, 1, 2]).intersection(setlike)][0] === Infinity,
|
||||
[...new Set([0, 1]).difference(setlike)],
|
||||
1 / new Set([1, 2]).symmetricDifference(setlike).values()[2] === Infinity,
|
||||
1 / [...new Set([1, 2]).symmetricDifference(setlike)][2] === Infinity,
|
||||
]
|
||||
`),
|
||||
).toEqual([true, true, [1], true])
|
||||
|
||||
@@ -20,9 +20,9 @@ import { CodeMode, Tool } from "../src/index.js"
|
||||
|
||||
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
|
||||
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
|
||||
// values, while at the host boundary (final result, tool arguments, JSON.stringify) they
|
||||
// serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
|
||||
// URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
|
||||
// values. JSON.stringify keeps Date -> ISO string (invalid -> null), URL -> href, and
|
||||
// RegExp/Map/Set/URLSearchParams -> {}. The host boundary matches that except URLSearchParams,
|
||||
// which cross as their query string, and Set, which crosses as an array.
|
||||
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
const value = async (code: string) => {
|
||||
const result = await run(code)
|
||||
@@ -635,6 +635,154 @@ describe("URL and URI helpers", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Headers", () => {
|
||||
test("constructs from records, pairs, Maps, and Headers; names fold to lowercase and values combine", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const headers = new Headers({ "Content-Type": "text/plain", "X-Count": 1, "X-Null": null })
|
||||
headers.append("Accept", "text/html")
|
||||
headers.append("accept", "application/json")
|
||||
headers.set("x-count", "2")
|
||||
headers.delete("x-null")
|
||||
const copy = new Headers(headers)
|
||||
copy.set("content-type", "text/html")
|
||||
return {
|
||||
get: headers.get("content-type"),
|
||||
missing: headers.get("x-missing"),
|
||||
combined: headers.get("ACCEPT"),
|
||||
has: [headers.has("Accept"), headers.has("x-null")],
|
||||
count: headers.get("x-count"),
|
||||
copied: [headers.get("content-type"), copy.get("content-type")],
|
||||
pairs: [...new Headers([["b", "2"], ["A", "1"]])],
|
||||
map: [...new Headers(new Map([["k", "v"]]))],
|
||||
keys: [...headers.keys()],
|
||||
values: [...headers.values()],
|
||||
entries: [...headers.entries()],
|
||||
}
|
||||
`),
|
||||
).toEqual({
|
||||
get: "text/plain",
|
||||
missing: null,
|
||||
combined: "text/html, application/json",
|
||||
has: [true, false],
|
||||
count: "2",
|
||||
copied: ["text/plain", "text/html"],
|
||||
pairs: [
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
],
|
||||
map: [["k", "v"]],
|
||||
keys: ["accept", "content-type", "x-count"],
|
||||
values: ["text/html, application/json", "text/plain", "2"],
|
||||
entries: [
|
||||
["accept", "text/html, application/json"],
|
||||
["content-type", "text/plain"],
|
||||
["x-count", "2"],
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("iterates in sorted order everywhere iteration is allowed, and getSetCookie keeps cookies apart", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const headers = new Headers({ b: "2", a: "1" })
|
||||
headers.append("Set-Cookie", "x=1")
|
||||
headers.append("set-cookie", "y=2")
|
||||
const seen = []
|
||||
headers.forEach((value, name, self) => seen.push(name + "=" + value + ":" + (self === headers)))
|
||||
const [first] = headers
|
||||
function* pairs() { yield* headers }
|
||||
return {
|
||||
seen,
|
||||
first,
|
||||
spread: [...headers],
|
||||
from: Array.from(headers).length,
|
||||
generator: [...pairs()].length,
|
||||
object: Object.fromEntries(headers),
|
||||
cookies: headers.getSetCookie(),
|
||||
}
|
||||
`),
|
||||
).toEqual({
|
||||
seen: ["a=1:true", "b=2:true", "set-cookie=x=1:true", "set-cookie=y=2:true"],
|
||||
first: ["a", "1"],
|
||||
spread: [
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
["set-cookie", "x=1"],
|
||||
["set-cookie", "y=2"],
|
||||
],
|
||||
from: 4,
|
||||
generator: 4,
|
||||
object: { a: "1", b: "2", "set-cookie": "y=2" },
|
||||
cookies: ["x=1", "y=2"],
|
||||
})
|
||||
})
|
||||
|
||||
test("serializes as a name-to-value object at the boundary and in JSON; prints for console", async () => {
|
||||
const result = await run(`
|
||||
const headers = new Headers({ "X-A": "1", b: "2" })
|
||||
console.log(headers)
|
||||
return { headers, json: JSON.stringify({ headers }), text: String(headers), type: typeof headers, is: headers instanceof Headers }
|
||||
`)
|
||||
expect(result.ok && result.value).toEqual({
|
||||
headers: { b: "2", "x-a": "1" },
|
||||
json: '{"headers":{"b":"2","x-a":"1"}}',
|
||||
text: "[object Headers]",
|
||||
type: "object",
|
||||
is: true,
|
||||
})
|
||||
expect(result.ok && result.logs?.[0]).toBe('Headers {"b":"2","x-a":"1"}')
|
||||
})
|
||||
|
||||
test("rejects what it cannot build from, and invalid names and values, with TypeErrors the program can catch", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
function message(run) {
|
||||
try { run(); return null } catch (error) { return error instanceof TypeError ? error.message : error }
|
||||
}
|
||||
const headers = new Headers()
|
||||
return [
|
||||
message(() => Headers()),
|
||||
message(() => new Headers(null)),
|
||||
message(() => new Headers(1)),
|
||||
message(() => new Headers("a=1")),
|
||||
message(() => new Headers(new Date())),
|
||||
message(() => new Headers(() => 1)),
|
||||
message(() => new Headers([["name"]])),
|
||||
message(() => new Headers([["a", "b", "c"]])),
|
||||
message(() => new Headers({ "bad name": "x" })),
|
||||
message(() => new Headers({ name: "bad\u0000value" })),
|
||||
message(() => headers.get("invalid\u0100")),
|
||||
message(() => headers.has({})),
|
||||
message(() => headers.set("a", "invalid\u0100")),
|
||||
message(() => headers.append("a")),
|
||||
message(() => headers.forEach()),
|
||||
message(() => headers.forEach(1)),
|
||||
message(() => { const get = headers.get; return get("a") }),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
"Constructor Headers requires 'new'.",
|
||||
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
||||
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
||||
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
||||
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
||||
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
||||
"new Headers(...) expects iterable [name, value] pairs.",
|
||||
"new Headers(...) expects iterable [name, value] pairs.",
|
||||
expect.stringContaining("bad name"),
|
||||
expect.stringContaining("invalid value"),
|
||||
expect.stringContaining("Invalid header name"),
|
||||
expect.stringContaining("[object Object]"),
|
||||
expect.stringContaining("invalid value"),
|
||||
"Headers.append requires 2 arguments.",
|
||||
"Headers.forEach requires 1 argument.",
|
||||
"Headers.forEach expects a function callback.",
|
||||
"Headers.prototype.get called on incompatible receiver undefined.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Map", () => {
|
||||
test("get/set/has/size with chaining", async () => {
|
||||
expect(
|
||||
@@ -668,18 +816,28 @@ describe("Map", () => {
|
||||
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
||||
})
|
||||
|
||||
test("keys/values/entries return arrays", async () => {
|
||||
test("keys/values/entries return live iterators", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
return { keys: m.keys(), values: m.values(), entries: m.entries() }
|
||||
const keys = m.keys()
|
||||
const first = keys.next()
|
||||
m.set("c", 3)
|
||||
return { first, rest: [...keys], values: [...m.values()], entries: [...m.entries()], same: [...m[Symbol.iterator]()] }
|
||||
`),
|
||||
).toEqual({
|
||||
keys: ["a", "b"],
|
||||
values: [1, 2],
|
||||
first: { value: "a", done: false },
|
||||
rest: ["b", "c"],
|
||||
values: [1, 2, 3],
|
||||
entries: [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
],
|
||||
same: [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -740,6 +898,42 @@ describe("Map", () => {
|
||||
).toEqual({ a: 3, b: 1, c: 1 })
|
||||
})
|
||||
|
||||
test("getOrInsert and getOrInsertComputed insert only when the key is missing", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const groups = new Map()
|
||||
groups.getOrInsert("a", []).push(1)
|
||||
groups.getOrInsert("a", []).push(2)
|
||||
let calls = 0
|
||||
const computed = (key) => { calls++; return key + "!" }
|
||||
const first = groups.getOrInsertComputed("b", computed)
|
||||
const second = groups.getOrInsertComputed("b", computed)
|
||||
const zero = groups.getOrInsertComputed(-0, (key) => 1 / key === Infinity)
|
||||
return [[...groups], first, second, calls, zero]
|
||||
`),
|
||||
).toEqual([
|
||||
[
|
||||
["a", [1, 2]],
|
||||
["b", "b!"],
|
||||
[0, true],
|
||||
],
|
||||
"b!",
|
||||
"b!",
|
||||
1,
|
||||
true,
|
||||
])
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map()
|
||||
const outer = m.getOrInsertComputed("k", () => { m.set("k", "inner"); return "outer" })
|
||||
let thrown
|
||||
try { m.getOrInsertComputed("j", () => { throw new Error("boom") }) } catch (error) { thrown = error.message }
|
||||
return [outer, m.get("k"), thrown, m.has("j")]
|
||||
`),
|
||||
).toEqual(["outer", "outer", "boom", false])
|
||||
expect((await error(`new Map().getOrInsertComputed("k", 5)`)).message).toContain("expects a function callback")
|
||||
})
|
||||
|
||||
test("maps serialize to {} at the boundary, like JSON", async () => {
|
||||
expect(await value(`return new Map([["a", 1]])`)).toEqual({})
|
||||
expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
|
||||
@@ -903,7 +1097,7 @@ describe("Uint8Array", () => {
|
||||
console.log(b, new Uint8Array())
|
||||
return [String(b), b + "", +new Uint8Array([5]), Number.isNaN(Number(b)), b == "1,2", JSON.stringify(b), b.toLocaleString(), typeof b, b instanceof Uint8Array]
|
||||
`),
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "[object Uint8Array]", "object", true])
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "1,2", "object", true])
|
||||
expect((await run(`console.log(new Uint8Array([1, 2]), new Uint8Array())`)).logs).toEqual([
|
||||
"Uint8Array(2) [1,2] Uint8Array(0) []",
|
||||
])
|
||||
@@ -971,6 +1165,177 @@ describe("TextEncoder and TextDecoder", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("built-in iterators", () => {
|
||||
test("keys/values/entries and [Symbol.iterator] step with next() and stay live", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const items = ["a"]
|
||||
const it = items.entries()
|
||||
items.push("b")
|
||||
const steps = [it.next(), it.next(), it.next()]
|
||||
items.push("c")
|
||||
return { steps, after: it.next(), same: items[Symbol.iterator] === items.values }
|
||||
`),
|
||||
).toEqual({
|
||||
steps: [{ value: [0, "a"], done: false }, { value: [1, "b"], done: false }, { done: true }],
|
||||
after: { done: true },
|
||||
same: true,
|
||||
})
|
||||
expect(
|
||||
await value(`
|
||||
const s = new Set([1, 2])
|
||||
const u = new URLSearchParams("a=1&b=2")
|
||||
const h = new Headers({ b: "2", a: "1" })
|
||||
const bytes = new Uint8Array([7, 8])
|
||||
return [
|
||||
[...s.entries()], [...s[Symbol.iterator]()], s[Symbol.iterator] === s.values,
|
||||
[...u.keys()], [...u[Symbol.iterator]()], u[Symbol.iterator] === u.entries,
|
||||
[...h.values()], [...h[Symbol.iterator]()], h[Symbol.iterator] === h.entries,
|
||||
[...bytes.entries()], [...bytes[Symbol.iterator]()], bytes[Symbol.iterator] === bytes.values,
|
||||
[..."ab"[Symbol.iterator]()],
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
[
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
],
|
||||
[1, 2],
|
||||
true,
|
||||
["a", "b"],
|
||||
[
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
],
|
||||
true,
|
||||
["1", "2"],
|
||||
[
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
],
|
||||
true,
|
||||
[
|
||||
[0, 7],
|
||||
[1, 8],
|
||||
],
|
||||
[7, 8],
|
||||
true,
|
||||
["a", "b"],
|
||||
])
|
||||
})
|
||||
|
||||
test("iterators are consumed once by every iteration site", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const it = [1, 2, 3, 4].values()
|
||||
const picked = []
|
||||
for (const item of it) { picked.push(item); if (item === 2) break }
|
||||
const [third] = it
|
||||
return { picked, third, rest: [...it], spent: Array.from(it), again: it[Symbol.iterator]() === it }
|
||||
`),
|
||||
).toEqual({ picked: [1, 2], third: 3, rest: [4], spent: [], again: true })
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
return [
|
||||
Object.fromEntries(m.entries()), Array.from(m.keys(), (k) => k + "!"), new Set(m.values()).size,
|
||||
await Promise.all([Promise.resolve(1), 2].values()),
|
||||
]
|
||||
`),
|
||||
).toEqual([{ a: 1, b: 2 }, ["a!", "b!"], 2, [1, 2]])
|
||||
expect(await value(`let s = 0; for await (const v of [Promise.resolve(1), 2].values()) s += v; return s`)).toBe(3)
|
||||
expect(
|
||||
await value(`return new Set([1, 2]).union({ size: 1, has: () => false, keys: () => new Set([3]).keys() })`),
|
||||
).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
test("iterators are opaque references", async () => {
|
||||
expect(await value(`return [1].keys()`)).toEqual({})
|
||||
expect(await value(`return JSON.stringify({ it: [1].keys() })`)).toBe('{"it":{}}')
|
||||
expect(await value(`return [typeof [1].keys(), Array.isArray([1].keys()), Object.keys([1].keys())]`)).toEqual([
|
||||
"object",
|
||||
false,
|
||||
[],
|
||||
])
|
||||
const logged = await run(`console.log([1].keys()); return null`)
|
||||
expect(logged.logs?.[0]).toBe("[opaque reference]")
|
||||
expect((await error(`return [1].keys() + ""`)).message).toContain("Binary operators require data values")
|
||||
expect((await error(`return [1].keys().next.call({})`)).message).toContain("is not a function")
|
||||
expect((await error(`const it = [1].keys(); const next = it.next; return next()`)).message).toContain(
|
||||
"Iterator.prototype.next called on incompatible receiver undefined",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Object.prototype.toString", () => {
|
||||
test("reports the built-in kind it is inherited by, as JS does through Symbol.toStringTag", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
new Map().toString(), new Set().toString(), new Headers().toString(), Promise.resolve(1).toString(),
|
||||
[1].values().toString(), ({}).toString(), String(new Map()), String(Promise.resolve(1)),
|
||||
\`\${new Set([1])}\`, [new Map()] + "", new Map() == "[object Map]",
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
"[object Map]",
|
||||
"[object Set]",
|
||||
"[object Headers]",
|
||||
"[object Promise]",
|
||||
"[object Iterator]",
|
||||
"[object Object]",
|
||||
"[object Map]",
|
||||
"[object Promise]",
|
||||
"[object Set]",
|
||||
"[object Map]",
|
||||
true,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toLocaleString", () => {
|
||||
test("numbers and dates format as en-US in UTC; everything else falls back to toString", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
(1234567.891).toLocaleString(), new Date(0).toLocaleString(), new Date(0).toLocaleDateString(),
|
||||
new Date(0).toLocaleTimeString(), "a".toLocaleString(), true.toLocaleString(), ({}).toLocaleString(),
|
||||
({ toString: () => "custom" }).toLocaleString(), new Uint8Array([1, 2]).toLocaleString(),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
"1,234,567.891",
|
||||
"1/1/1970, 12:00:00 AM",
|
||||
"1/1/1970",
|
||||
"12:00:00 AM",
|
||||
"a",
|
||||
"true",
|
||||
"[object Object]",
|
||||
"custom",
|
||||
"1,2",
|
||||
])
|
||||
})
|
||||
|
||||
test("arrays join each element's toLocaleString, skipping holes and nullish elements", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let calls = 0
|
||||
const item = { toLocaleString() { calls++; return "o" } }
|
||||
return [[1234.5, "x", null, undefined, item, new Date(0)].toLocaleString(), [, item, , item].toLocaleString(), calls]
|
||||
`),
|
||||
).toEqual(["1,234.5,x,,,o,1/1/1970, 12:00:00 AM", ",o,,o", 3])
|
||||
expect((await error(`const f = ({}).toLocaleString; f()`)).message).toContain(
|
||||
"Object.prototype.toLocaleString called on null or undefined",
|
||||
)
|
||||
})
|
||||
|
||||
test("toLocaleLowerCase and toLocaleUpperCase ignore the locale argument", async () => {
|
||||
expect(
|
||||
await value(`return ["ABC".toLocaleLowerCase("tr"), "abc".toLocaleUpperCase(), "İ".toLocaleLowerCase()]`),
|
||||
).toEqual(["abc", "ABC", "i̇"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdlib integration", () => {
|
||||
test("constructor follows own keys, shadowing, writes, and new", async () => {
|
||||
expect(
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Interpreter } from "../../src/interpreter/interpreter.js"
|
||||
import { Throw } from "../../src/interpreter/model.js"
|
||||
import { createErrorValue } from "../../src/interpreter/intrinsics.js"
|
||||
import { constructor, fn, methods } from "../../src/interpreter/native.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj } from "../../src/interpreter/objects.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj, type Value } from "../../src/interpreter/objects.js"
|
||||
import { ToolRuntime } from "../../src/tool-runtime.js"
|
||||
|
||||
export const root = import.meta.dir
|
||||
@@ -64,10 +64,7 @@ export const run = async (file: string): Promise<Outcome> => {
|
||||
return { status: "pass" }
|
||||
}
|
||||
|
||||
const harness = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
onDone: (error: unknown) => void,
|
||||
): ReadonlyArray<readonly [string, unknown]> => {
|
||||
const harness = <R>(ctx: Interpreter<R>, onDone: (error: Value) => void): ReadonlyArray<readonly [string, Value]> => {
|
||||
const builtins = ctx.builtins
|
||||
const test262Prototype = new Obj(builtins.Object)
|
||||
define(test262Prototype, "name", "Test262Error", hidden)
|
||||
@@ -90,7 +87,7 @@ const harness = <R>(
|
||||
methods(builtins, compareArray, [["format", 1, (_, args) => show(args[0])]])
|
||||
const assert = fn<R>(builtins, "assert", 2, (_, args) =>
|
||||
args[0] === true
|
||||
? Effect.void
|
||||
? Effect.undefined
|
||||
: fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])),
|
||||
)
|
||||
methods(builtins, assert, [
|
||||
@@ -99,7 +96,7 @@ const harness = <R>(
|
||||
3,
|
||||
(_, args) =>
|
||||
Object.is(args[0], args[1])
|
||||
? Effect.void
|
||||
? Effect.undefined
|
||||
: fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`),
|
||||
],
|
||||
[
|
||||
@@ -108,14 +105,14 @@ const harness = <R>(
|
||||
(_, args) =>
|
||||
Object.is(args[0], args[1])
|
||||
? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`)
|
||||
: Effect.void,
|
||||
: Effect.undefined,
|
||||
],
|
||||
[
|
||||
"compareArray",
|
||||
3,
|
||||
(_, args) =>
|
||||
compare(args[0], args[1])
|
||||
? Effect.void
|
||||
? Effect.undefined
|
||||
: fail(
|
||||
`Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`,
|
||||
),
|
||||
@@ -132,7 +129,7 @@ const harness = <R>(
|
||||
const thrown = materialize(ctx, Cause.squash(cause))
|
||||
if (!(thrown instanceof Obj)) return fail(`${prefix(args[2])}Thrown value was not an object!`)
|
||||
const actual = get(thrown, "constructor")
|
||||
if (actual === args[0]) return Effect.void
|
||||
if (actual === args[0]) return Effect.undefined
|
||||
return fail(`${prefix(args[2])}Expected a ${expected} but got a ${show(actual)}`)
|
||||
},
|
||||
onSuccess: () =>
|
||||
@@ -146,7 +143,13 @@ const harness = <R>(
|
||||
["assert", assert],
|
||||
["compareArray", compareArray],
|
||||
["Test262Error", test262Error],
|
||||
["$DONE", fn<R>(builtins, "$DONE", 1, (_, args) => onDone(args[0]))],
|
||||
[
|
||||
"$DONE",
|
||||
fn<R>(builtins, "$DONE", 1, (_, args) => {
|
||||
onDone(args[0])
|
||||
return undefined
|
||||
}),
|
||||
],
|
||||
[
|
||||
"$DONOTEVALUATE",
|
||||
fn<R>(builtins, "$DONOTEVALUATE", 0, () =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user