From 79287edccd6aa47716c74419d3fcb122d248cb0c Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Wed, 8 Apr 2026 22:06:29 -0400 Subject: [PATCH 01/56] add mise.toml with zig & bun --- mise.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..f376f8980 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +bun = "latest" +zig = "{{ read_file(path='.zig-version') | trim }}" From aa21f28b878603292839b8b546b3dd212f576301 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Wed, 8 Apr 2026 22:08:11 -0400 Subject: [PATCH 02/56] add node version --- mise.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mise.toml b/mise.toml index f376f8980..f3888c9d8 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,4 @@ [tools] bun = "latest" zig = "{{ read_file(path='.zig-version') | trim }}" +node = "22.13.1" From 04d8dab152e37e545ccaf1caf73cec732b175ea7 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 01:03:23 -0400 Subject: [PATCH 03/56] bun shim --- bun.lock | 98 +++- package.json | 8 +- packages/core/package.json | 3 + packages/core/src/nodejs/NodeBun.ts | 126 +++++ packages/core/src/nodejs/bunModules/ffi.ts | 524 ++++++++++++++++++++ packages/core/src/nodejs/bunModules/test.ts | 1 + packages/core/src/nodejs/compat.ts | 32 ++ 7 files changed, 785 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/nodejs/NodeBun.ts create mode 100644 packages/core/src/nodejs/bunModules/ffi.ts create mode 100644 packages/core/src/nodejs/bunModules/test.ts create mode 100644 packages/core/src/nodejs/compat.ts diff --git a/bun.lock b/bun.lock index 5241241c7..2c54d5467 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,12 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@opentui", "devDependencies": { "oxfmt": "0.41.0", "oxlint": "1.56.0", + "vitest": "^4.1.3", }, }, "packages/core": { @@ -16,7 +16,10 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", + "koffi": "^2.15.6", "marked": "17.0.1", + "string-width": "^8.2.0", + "strip-ansi": "^7.2.0", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -116,6 +119,9 @@ }, }, }, + "trustedDependencies": [ + "koffi", + ], "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -365,6 +371,18 @@ "@opentui/core": ["@opentui/core@workspace:packages/core"], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZuPWAawlVat6ZHb8vaH/CVUeGwI0pI4vd+6zz1ZocZn95ZWJztfyhzNZOJrq1WjHmUROieJ7cOuYUZfvYNuLrg=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXxhz654vXgEu2wrFFFFnrSWbyk6/r6nXNnDTcMRWofdMZQLx87NhbcsErNmz9KmFdzoPiQSmlpYubLflKKzqQ=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-v3z0QWpRS3p8blE/A7pTu15hcFMtSndeiYhRxhrjp6zAhQ+UlruQs9DAG1ifSuVO1RJJ0pUKklFivdbu0pMzuw=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-o/m9mD1dvOCwkxOUUyoEILl+d6tzh/85foJc4uqjXYi71NNcwg8u+Eq3/gdHuSKnlT1pusCPKoS1IDuBvZE24A=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-Rwp7JOwrYm4wtzPHY2vv+2l91LXmKSI7CtbmWN1sSUGhBPtPGSvfwux3W5xaAZQa2KPEXicPjaKJZc+pob3YRg=="], + "@opentui/react": ["@opentui/react@workspace:packages/react"], "@opentui/solid": ["@opentui/solid@workspace:packages/solid"], @@ -517,6 +535,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], @@ -531,8 +551,12 @@ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -565,6 +589,20 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@vitest/expect": ["@vitest/expect@4.1.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.3", "", { "dependencies": { "@vitest/spy": "4.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.3", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg=="], + + "@vitest/runner": ["@vitest/runner@4.1.3", "", { "dependencies": { "@vitest/utils": "4.1.3", "pathe": "^2.0.3" } }, "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "@vitest/utils": "4.1.3", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ=="], + + "@vitest/spy": ["@vitest/spy@4.1.3", "", {}, "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw=="], + + "@vitest/utils": ["@vitest/utils@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw=="], + "@webgpu/types": ["@webgpu/types@0.1.68", "", {}, "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -589,6 +627,8 @@ "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "astro": ["astro@5.16.11", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.20.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.1", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.3", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA=="], @@ -645,6 +685,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -729,7 +771,7 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -763,6 +805,8 @@ "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -789,7 +833,7 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -877,6 +921,8 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "koffi": ["koffi@2.15.6", "", {}, "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw=="], + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -1027,6 +1073,8 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -1073,6 +1121,8 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], @@ -1183,6 +1233,8 @@ "shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -1197,15 +1249,19 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "stage-js": ["stage-js@1.0.0-alpha.17", "", {}, "sha512-AzlMO+t51v6cFvKZ+Oe9DJnL1OXEH5s9bEy6di5aOrUpcP7PCzI/wIeXF0u3zg0L89gwnceoKxrLId0ZpYnNXw=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], @@ -1221,6 +1277,8 @@ "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -1229,6 +1287,8 @@ "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -1291,12 +1351,16 @@ "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitest": ["vitest@4.1.3", "", { "dependencies": { "@vitest/expect": "4.1.3", "@vitest/mocker": "4.1.3", "@vitest/pretty-format": "4.1.3", "@vitest/runner": "4.1.3", "@vitest/snapshot": "4.1.3", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.3", "@vitest/browser-preview": "4.1.3", "@vitest/browser-webdriverio": "4.1.3", "@vitest/coverage-istanbul": "4.1.3", "@vitest/coverage-v8": "4.1.3", "@vitest/ui": "4.1.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -1331,6 +1395,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@astrojs/mdx/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "@opentui/react/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@opentui/solid/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], @@ -1345,10 +1411,14 @@ "astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "astro/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "astro/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -1371,10 +1441,18 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "unstorage/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@opentui/react/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "@opentui/solid/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], @@ -1385,8 +1463,18 @@ "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "boxen/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + "widest-line/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "wrap-ansi/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index 80f4841a8..79ff63f6e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ }, "devDependencies": { "oxfmt": "0.41.0", - "oxlint": "1.56.0" - } + "oxlint": "1.56.0", + "vitest": "^4.1.3" + }, + "trustedDependencies": [ + "koffi" + ] } diff --git a/packages/core/package.json b/packages/core/package.json index 530a62c62..d8a6dbe8b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,6 +40,8 @@ "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", + "string-width": "8.2.0", + "strip-ansi": "7.2.0", "yoga-layout": "3.2.1" }, "peerDependencies": { @@ -47,6 +49,7 @@ }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", + "koffi": "2.15.6", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0", diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts new file mode 100644 index 000000000..a0b5652b7 --- /dev/null +++ b/packages/core/src/nodejs/NodeBun.ts @@ -0,0 +1,126 @@ +import type { WriteFileOptions } from "node:fs" +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { isArrayBufferView } from "node:util/types" + +/** + * ```bash + * rg 'Bun\.(\w+)' -r ' | "$1"' -o -N -I | sort | uniq | pbcopy + * ``` + */ +type UsedBunApis = + | "argv" + | "build" + | "file" + | "Glob" + | "serve" + | "sleep" + | "spawn" + | "spawnSync" + | "stringWidth" + | "stripANSI" + | "write" + +type NodeBunInterface = Pick + +type BunFile = Bun.BunFile +type BunWrite = typeof Bun.write +type BunFileLike = { name: string | undefined } +type BunPathLike = string | NodeJS.TypedArray | ArrayBufferLike | URL + +class NodeBunError extends Error { + constructor(message: string) { + super(message) + this.name = "NodeBunError" + } +} + +class NodeBun implements NodeBunInterface { + get argv(): string[] { + return process.argv + } + + sleep(msOrDate: number | Date): Promise { + let ms: number + if (msOrDate instanceof Date) { + ms = msOrDate.getTime() - Date.now() + } else { + ms = msOrDate + } + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + stringWidth(text: string): number { + const stringWidth = require("string-width") + return stringWidth(text) + } + + stripANSI(text: string): string { + const stripANSI = require("strip-ansi") + return stripANSI(text) + } + + write: typeof Bun.write = (destination, data, options): Promise => { + let dest: string | URL + if (typeof destination === "string") { + dest = destination + } else if (destination instanceof URL) { + dest = destination + } else if ("name" in destination && destination.name !== undefined) { + dest = destination.name + } else { + // ArrayBuffer, NodeJS.TypedArray, etc. + throw new NodeBunError("Bun.write: Unsupported destination type") + } + + let buffer: Uint8Array + if (typeof data === "string") { + buffer = new TextEncoder().encode(data) + } else if (isArrayBufferView(data)) { + buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } else { + throw new NodeBunError("Bun.write: Unsupported data type") + } + + const nodeOptions: WriteFileOptions = {} + if (typeof buffer === "string") { + nodeOptions.encoding = "utf-8" + } + if (options && "mode" in options && options?.mode !== undefined) { + nodeOptions.mode = options.mode + } + if (options && "createPath" in options && options?.createPath) { + const destPath = typeof dest === "string" ? dest : dest.pathname + fs.mkdir(path.dirname(destPath), { recursive: true }) + } + + return fs.writeFile(dest, buffer, nodeOptions).then(() => buffer.length) + } + + // Unsupported + get Glob(): typeof Bun.Glob { + throw new NodeBunError("Bun.Glob is not supported in Node.js") + } + + get spawn(): typeof Bun.spawn { + throw new NodeBunError("Bun.spawn is not supported in Node.js") + } + + get spawnSync(): typeof Bun.spawnSync { + throw new NodeBunError("Bun.spawnSync is not supported in Node.js") + } + + get build(): typeof Bun.build { + throw new NodeBunError("Bun.build is not supported in Node.js") + } + + get file(): typeof Bun.file { + throw new NodeBunError("Bun.file is not supported in Node.js") + } + + get serve(): typeof Bun.serve { + throw new NodeBunError("Bun.serve is not supported in Node.js") + } +} + +export default new NodeBun() diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/nodejs/bunModules/ffi.ts new file mode 100644 index 000000000..977e9ebc7 --- /dev/null +++ b/packages/core/src/nodejs/bunModules/ffi.ts @@ -0,0 +1,524 @@ +import type { + dlopen as bunDlopen, + JSCallback as BunJSCallback, + ptr as bunPtr, + toArrayBuffer as bunToArrayBuffer, + ConvertFns, + FFIFunction, + FFITypeOrString, + Pointer, +} from "bun:ffi" +import koffi from "koffi" +import { isArrayBufferView } from "node:util/types" + +/** Copy of Bun's FFIType enum. */ +export enum FFIType { + char = 0, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int8_t = 1, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i8 = 1, + + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint8_t = 2, + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u8 = 2, + + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int16_t = 3, + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i16 = 3, + + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint16_t = 4, + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u16 = 4, + + /** + * 32-bit signed integer + */ + int32_t = 5, + + /** + * 32-bit signed integer + * + * Alias of {@link FFIType.int32_t} + */ + i32 = 5, + /** + * 32-bit signed integer + * + * The same as `int` in C + * + * ```c + * int + * ``` + */ + int = 5, + + /** + * 32-bit unsigned integer + * + * The same as `unsigned int` in C (on x64 & arm64) + * + * C: + * ```c + * unsigned int + * ``` + * JavaScript: + * ```js + * ptr(new Uint32Array(1)) + * ``` + */ + uint32_t = 6, + /** + * 32-bit unsigned integer + * + * Alias of {@link FFIType.uint32_t} + */ + u32 = 6, + + /** + * int64 is a 64-bit signed integer + */ + int64_t = 7, + /** + * i64 is a 64-bit signed integer + */ + i64 = 7, + + /** + * 64-bit unsigned integer + */ + uint64_t = 8, + /** + * 64-bit unsigned integer + */ + u64 = 8, + + /** + * IEEE-754 double precision float + */ + double = 9, + + /** + * Alias of {@link FFIType.double} + */ + f64 = 9, + + /** + * IEEE-754 single precision float + */ + float = 10, + + /** + * Alias of {@link FFIType.float} + */ + f32 = 10, + + /** + * Boolean value + * + * Must be `true` or `false`. `0` and `1` type coercion is not supported. + * + * In C, this corresponds to: + * ```c + * bool + * _Bool + * ``` + */ + bool = 11, + + /** + * Pointer value + * + * See {@link Bun.FFI.ptr} for more information + * + * In C: + * ```c + * void* + * ``` + * + * In JavaScript: + * ```js + * ptr(new Uint8Array(1)) + * ``` + */ + ptr = 12, + /** + * Pointer value + * + * alias of {@link FFIType.ptr} + */ + pointer = 12, + + /** + * void value + * + * void arguments are not supported + * + * void return type is the default return type + * + * In C: + * ```c + * void + * ``` + */ + void = 13, + + /** + * When used as a `returns`, this will automatically become a {@link CString}. + * + * When used in `args` it is equivalent to {@link FFIType.pointer} + */ + cstring = 14, + + /** + * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance + * but means you might get a `BigInt` or you might get a `number`. + * + * In C, this always becomes `int64_t` + * + * In JavaScript, this could be number or it could be BigInt, depending on what + * value is passed in. + */ + i64_fast = 15, + + /** + * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance + * but means you might get a `BigInt` or you might get a `number`. + * + * In C, this always becomes `uint64_t` + * + * In JavaScript, this could be number or it could be BigInt, depending on what + * value is passed in. + */ + u64_fast = 16, + function = 17, + + napi_env = 18, + napi_value = 19, + buffer = 20, +} + +const FFITypeStringToType = { + ["char"]: FFIType.char, + ["int8_t"]: FFIType.int8_t, + ["i8"]: FFIType.i8, + ["uint8_t"]: FFIType.uint8_t, + ["u8"]: FFIType.u8, + ["int16_t"]: FFIType.int16_t, + ["i16"]: FFIType.i16, + ["uint16_t"]: FFIType.uint16_t, + ["u16"]: FFIType.u16, + ["int32_t"]: FFIType.int32_t, + ["i32"]: FFIType.i32, + ["int"]: FFIType.int, + ["uint32_t"]: FFIType.uint32_t, + ["u32"]: FFIType.u32, + ["int64_t"]: FFIType.int64_t, + ["i64"]: FFIType.i64, + ["uint64_t"]: FFIType.uint64_t, + ["u64"]: FFIType.u64, + ["double"]: FFIType.double, + ["f64"]: FFIType.f64, + ["float"]: FFIType.float, + ["f32"]: FFIType.f32, + ["bool"]: FFIType.bool, + ["ptr"]: FFIType.ptr, + ["pointer"]: FFIType.pointer, + ["void"]: FFIType.void, + ["cstring"]: FFIType.cstring, + ["function"]: FFIType.pointer, // for now + ["usize"]: FFIType.uint64_t, // for now + ["callback"]: FFIType.pointer, // for now + ["napi_env"]: FFIType.napi_env, + ["napi_value"]: FFIType.napi_value, + ["buffer"]: FFIType.buffer, +} as const + +const BunPtrType = koffi.opaque("BunPtr") +const NapiEnvType = koffi.opaque("NapiEnv") +const NapiValueType = koffi.opaque("NapiValue") +const BufferType = koffi.opaque("Buffer") + +const ffiTypeToKoffiTypeMap: Record = { + [FFIType.char]: koffi.types.char, + [FFIType.int8_t]: koffi.types.int8_t, + [FFIType.uint8_t]: koffi.types.uint8_t, + [FFIType.int16_t]: koffi.types.int16_t, + [FFIType.uint16_t]: koffi.types.uint16_t, + [FFIType.int32_t]: koffi.types.int32_t, + [FFIType.uint32_t]: koffi.types.uint32_t, + [FFIType.int64_t]: koffi.types.int64_t, + [FFIType.uint64_t]: koffi.types.uint64_t, + [FFIType.double]: koffi.types.double, + [FFIType.float]: koffi.types.float, + [FFIType.bool]: koffi.types.bool, + [FFIType.ptr]: BunPtrType, + [FFIType.void]: koffi.types.void, + [FFIType.cstring]: koffi.types.string, + [FFIType.i64_fast]: koffi.types.int64_t, + [FFIType.u64_fast]: koffi.types.uint64_t, + [FFIType.function]: BunPtrType, + [FFIType.napi_env]: NapiEnvType, + [FFIType.napi_value]: NapiValueType, + [FFIType.buffer]: BufferType, +} + +function ffiTypeToKoffiType(type: FFITypeOrString): koffi.TypeSpec { + let numberType: FFIType + if (typeof type === "number") { + numberType = type + } else { + numberType = FFITypeStringToType[type] + } + + if (numberType === FFIType.napi_env || numberType === FFIType.napi_value || numberType === FFIType.cstring) { + throw new Error(`Unsupported FFI type: ${FFIType[numberType]} (${type})`) + } + + return ffiTypeToKoffiTypeMap[numberType] +} + +export class JSCallback implements BunJSCallback { + #threadsafe: boolean + #registeredCallback: koffi.IKoffiRegisteredCallback | null + + constructor(callback: (...args: any[]) => any, definition: FFIFunction) { + const proto = koffi.proto(returnsToKoffiType(definition.returns), argsToKoffiTypes(definition.args)) + this.#registeredCallback = koffi.register(callback, proto) + this.#threadsafe = definition.threadsafe ?? false + } + + get ptr(): Pointer | null { + if (!this.#registeredCallback) { + return null + } + return Number(koffi.address(this.#registeredCallback)) as Pointer + } + + get threadsafe(): boolean { + return this.#threadsafe + } + + close() { + if (!this.#registeredCallback) { + return + } + koffi.unregister(this.#registeredCallback) + this.#registeredCallback = null + } +} + +function argsToKoffiTypes(args: readonly FFITypeOrString[] | undefined): koffi.TypeSpec[] { + return args?.map(ffiTypeToKoffiType) ?? [] +} + +function returnsToKoffiType(returns: FFITypeOrString | undefined): koffi.TypeSpec { + return ffiTypeToKoffiType(returns ?? FFIType.void) +} + +function ffiFunctionToKoffiFunction unknown>( + lib: koffi.IKoffiLib, + name: string, + type: FFIFunction, +): T & koffi.KoffiFunction { + const func = lib.func(name, returnsToKoffiType(type.returns), argsToKoffiTypes(type.args)) + return func as T & koffi.KoffiFunction +} + +/** + * Bun returns the pointer to the data backing a TypedArray, ArrayBuffer, etc, + * directly aliasing the data. koffi doesn't appear to expose such magicks, so + * we have to settle for faking it. + * + * TODO: don't re-allocate every time. + * TODO: don't leak. + */ +export const ptr: typeof bunPtr = (value) => { + const uint8 = koffi.types.uint8 + const opaque = koffi.alloc(uint8, value.byteLength) + const encodable = isArrayBufferView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value + koffi.encode(opaque, uint8, encodable, encodable.byteLength) + const pointer = Number(koffi.address(opaque)) + return pointer as Pointer +} + +export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) => { + let ptrBigint = BigInt(pointer) + if (offset) { + ptrBigint += BigInt(offset) + } + return koffi.view(ptrBigint, length ? length : -1) +} + +function guessSuffix() { + switch (process.platform) { + case "darwin": + return "dylib" + case "linux": + return "so" + case "win32": + return "dll" + default: + return "so" + } +} + +export const suffix: string = guessSuffix() + +export const dlopen: typeof bunDlopen = (name, symbols) => { + let loadPath: string + if (typeof name === "string") { + loadPath = name + } else if (name instanceof URL) { + loadPath = name.pathname + } else { + throw new Error(`Unsupported FFI library name: ${name}`) + } + const lib = koffi.load(loadPath) + const library: Record = {} + for (const [name, ffiFunction] of Object.entries(symbols)) { + // Idea: could use defineProperty to lazily create the koffi.func + library[name] = ffiFunctionToKoffiFunction(lib, name, ffiFunction) + } + return { + symbols: library as unknown as ConvertFns, + close: () => lib.unload(), + } +} diff --git a/packages/core/src/nodejs/bunModules/test.ts b/packages/core/src/nodejs/bunModules/test.ts new file mode 100644 index 000000000..fb09aff02 --- /dev/null +++ b/packages/core/src/nodejs/bunModules/test.ts @@ -0,0 +1 @@ +export * from "vitest" diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts new file mode 100644 index 000000000..492bcc417 --- /dev/null +++ b/packages/core/src/nodejs/compat.ts @@ -0,0 +1,32 @@ +import * as mod from "node:module" + +/** + * Sets up Bun shims in a Node.js process. + */ +export function install() { + Object.defineProperty(globalThis, "Bun", { + configurable: true, + enumerable: true, + get: () => require("./NodeBun"), + }) + + mod.registerHooks({ resolve: resolveBun }) +} + +const BUN_PREFIX = "bun:" + +const resolveBun: mod.ResolveHookSync = (request, context, next) => { + console.log("resolveBun", request, context) + if (request.startsWith(BUN_PREFIX)) { + const name = request.slice(BUN_PREFIX.length) + const result = next(`./bunModules/${name}`, { + parentURL: import.meta.url, + importAttributes: { + type: "commonjs", + }, + }) + console.log("resolveBun result", result) + return result + } + return next(request, context) +} From cfb51d9fa001cd54ef0751f8c1fdbec15525c143 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:48:13 -0400 Subject: [PATCH 04/56] improve compat --- bun.lock | 178 ++++++++++++++++---- mise.toml | 2 +- packages/core/package.json | 2 + packages/core/src/nodejs/NodeBun.ts | 4 +- packages/core/src/nodejs/bunModules/bun.ts | 1 + packages/core/src/nodejs/bunModules/ffi.ts | 35 +++- packages/core/src/nodejs/bunModules/test.ts | 4 + packages/core/src/nodejs/compat.ts | 66 +++++++- 8 files changed, 243 insertions(+), 49 deletions(-) create mode 100644 packages/core/src/nodejs/bunModules/bun.ts diff --git a/bun.lock b/bun.lock index 2c54d5467..ca274f77d 100644 --- a/bun.lock +++ b/bun.lock @@ -16,10 +16,9 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", - "koffi": "^2.15.6", "marked": "17.0.1", - "string-width": "^8.2.0", - "strip-ansi": "^7.2.0", + "string-width": "8.2.0", + "strip-ansi": "7.2.0", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -27,6 +26,8 @@ "@types/node": "^24.0.0", "@types/three": "0.177.0", "commander": "^13.1.0", + "esbuild-register": "^3.6.0", + "tsx": "^4.21.0", "typescript": "^5", "web-tree-sitter": "0.25.10", }, @@ -39,6 +40,7 @@ "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", + "koffi": "2.15.6", "planck": "^1.4.2", "three": "0.177.0", }, @@ -201,57 +203,57 @@ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], @@ -777,7 +779,9 @@ "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -835,6 +839,8 @@ "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -1203,6 +1209,8 @@ "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], @@ -1299,6 +1307,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1413,6 +1423,8 @@ "astro/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "astro/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "astro/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], @@ -1447,6 +1459,8 @@ "unstorage/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -1463,12 +1477,116 @@ "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "boxen/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "widest-line/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], diff --git a/mise.toml b/mise.toml index f3888c9d8..ed51a31f6 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,4 @@ [tools] bun = "latest" zig = "{{ read_file(path='.zig-version') | trim }}" -node = "22.13.1" +node = "22.22.2" diff --git a/packages/core/package.json b/packages/core/package.json index d8a6dbe8b..8ceeed910 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,6 +32,8 @@ "@types/node": "^24.0.0", "@types/three": "0.177.0", "commander": "^13.1.0", + "esbuild-register": "^3.6.0", + "tsx": "^4.21.0", "typescript": "^5", "web-tree-sitter": "0.25.10" }, diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts index a0b5652b7..b40b8a5be 100644 --- a/packages/core/src/nodejs/NodeBun.ts +++ b/packages/core/src/nodejs/NodeBun.ts @@ -51,12 +51,12 @@ class NodeBun implements NodeBunInterface { } stringWidth(text: string): number { - const stringWidth = require("string-width") + const stringWidth = import.meta.require("string-width") return stringWidth(text) } stripANSI(text: string): string { - const stripANSI = require("strip-ansi") + const stripANSI = import.meta.require("strip-ansi") return stripANSI(text) } diff --git a/packages/core/src/nodejs/bunModules/bun.ts b/packages/core/src/nodejs/bunModules/bun.ts new file mode 100644 index 000000000..7d29a20d9 --- /dev/null +++ b/packages/core/src/nodejs/bunModules/bun.ts @@ -0,0 +1 @@ +export default Bun diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/nodejs/bunModules/ffi.ts index 977e9ebc7..fd4340ed8 100644 --- a/packages/core/src/nodejs/bunModules/ffi.ts +++ b/packages/core/src/nodejs/bunModules/ffi.ts @@ -371,7 +371,7 @@ const FFITypeStringToType = { ["buffer"]: FFIType.buffer, } as const -const BunPtrType = koffi.opaque("BunPtr") +const BunPtrType = koffi.pointer("BunPtr", koffi.opaque()) const NapiEnvType = koffi.opaque("NapiEnv") const NapiValueType = koffi.opaque("NapiValue") const BufferType = koffi.opaque("Buffer") @@ -421,7 +421,7 @@ export class JSCallback implements BunJSCallback { constructor(callback: (...args: any[]) => any, definition: FFIFunction) { const proto = koffi.proto(returnsToKoffiType(definition.returns), argsToKoffiTypes(definition.args)) - this.#registeredCallback = koffi.register(callback, proto) + this.#registeredCallback = koffi.register(callback, koffi.pointer(proto)) this.#threadsafe = definition.threadsafe ?? false } @@ -462,19 +462,35 @@ function ffiFunctionToKoffiFunction unknown>( return func as T & koffi.KoffiFunction } +const KoffiNativeAlloc = Symbol("KoffiNativeAlloc") +const NativeAllocRegistry = new FinalizationRegistry((val) => koffi.free(val)) + +function nativeAlloc(value: object, bytes: number) { + if (KoffiNativeAlloc in value) { + return value[KoffiNativeAlloc] + } + const ptr = koffi.alloc(koffi.types.uint8, bytes) + Object.defineProperty(value, KoffiNativeAlloc, { + value: ptr, + writable: false, + configurable: true, + enumerable: false, + }) + NativeAllocRegistry.register(value, ptr) + return ptr +} + /** * Bun returns the pointer to the data backing a TypedArray, ArrayBuffer, etc, * directly aliasing the data. koffi doesn't appear to expose such magicks, so * we have to settle for faking it. * - * TODO: don't re-allocate every time. - * TODO: don't leak. + * TODO: copy the data back from opaque to the target after a call to native function. */ export const ptr: typeof bunPtr = (value) => { - const uint8 = koffi.types.uint8 - const opaque = koffi.alloc(uint8, value.byteLength) + const opaque = nativeAlloc(value, value.byteLength) const encodable = isArrayBufferView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value - koffi.encode(opaque, uint8, encodable, encodable.byteLength) + koffi.encode(opaque, koffi.types.uint8, encodable, encodable.byteLength) const pointer = Number(koffi.address(opaque)) return pointer as Pointer } @@ -484,7 +500,10 @@ export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) if (offset) { ptrBigint += BigInt(offset) } - return koffi.view(ptrBigint, length ? length : -1) + if (length === undefined) { + throw new Error(`bun:ffi.toArrayBuffer requires a length argument`) + } + return koffi.view(ptrBigint, length) } function guessSuffix() { diff --git a/packages/core/src/nodejs/bunModules/test.ts b/packages/core/src/nodejs/bunModules/test.ts index fb09aff02..405f87103 100644 --- a/packages/core/src/nodejs/bunModules/test.ts +++ b/packages/core/src/nodejs/bunModules/test.ts @@ -1 +1,5 @@ +import { vi } from "vitest" export * from "vitest" + +export const mock = vi.mock +export const spyOn = vi.spyOn diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index 492bcc417..7a66026f2 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -1,32 +1,82 @@ import * as mod from "node:module" +const require = mod.createRequire(import.meta.url) + /** * Sets up Bun shims in a Node.js process. */ -export function install() { +export function setup() { Object.defineProperty(globalThis, "Bun", { configurable: true, enumerable: true, - get: () => require("./NodeBun"), + get: () => require("./NodeBun.js"), }) - mod.registerHooks({ resolve: resolveBun }) + mod.registerHooks({ resolve: resolveBun, load: loadBun }) + if (process.env.VITEST) { + mod.registerHooks({ resolve: resolveJsToTs }) + } } const BUN_PREFIX = "bun:" const resolveBun: mod.ResolveHookSync = (request, context, next) => { - console.log("resolveBun", request, context) - if (request.startsWith(BUN_PREFIX)) { - const name = request.slice(BUN_PREFIX.length) - const result = next(`./bunModules/${name}`, { + if (request.startsWith(BUN_PREFIX) || request === "bun") { + const name = request === "bun" ? "bun" : request.slice(BUN_PREFIX.length) + const extname = import.meta.url.split(".").pop() + const result = next(`./bunModules/${name}.${extname}`, { parentURL: import.meta.url, importAttributes: { type: "commonjs", }, }) - console.log("resolveBun result", result) return result } + return next(request, context) } + +const loadBun: mod.LoadHookSync = (url, context, next) => { + if (context.importAttributes?.type === "file") { + return { + shortCircuit: true, + format: "json", + source: JSON.stringify(url), + } + } + + const result = next(url, context) + if (result.source === undefined) { + return { ...result, shortCircuit: true } + } + return result +} + +const JS_EXTENSIONS = [".js", ".jsx", ".mjs", ".cjs"] +const TS_REPLACEMENTS: Record = { + ".js": ".ts", + ".jsx": ".tsx", + ".mjs": ".mts", + ".cjs": ".cts", +} + +const resolveJsToTs: mod.ResolveHookSync = (request, context, next) => { + // Only rewrite relative imports + if (!request.startsWith(".")) return next(request, context) + + const ext = JS_EXTENSIONS.find((e) => request.endsWith(e)) + if (!ext) return next(request, context) + + // Try the original .js first + try { + return next(request, context) + } catch { + // Fall through to .ts attempt + } + + const tsRequest = request.slice(0, -ext.length) + TS_REPLACEMENTS[ext] + return next(tsRequest, context) +} + +// Auto-setup when loaded via --import in vitest workers +if (process.env.VITEST) setup() From 02e570324d84a2965430b31bc3c2214e7b4337f4 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:48:56 -0400 Subject: [PATCH 05/56] improve node compat using .js/.d.ts --- packages/core/scripts/build.ts | 18 +++++----- packages/core/src/zig.ts | 63 ++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/packages/core/scripts/build.ts b/packages/core/scripts/build.ts index 198b8d726..603c8d47e 100644 --- a/packages/core/scripts/build.ts +++ b/packages/core/scripts/build.ts @@ -1,9 +1,8 @@ -import { spawnSync, type SpawnSyncReturns } from "node:child_process" import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs" -import { dirname, join, resolve } from "path" -import { fileURLToPath } from "url" +import { spawnSync, type SpawnSyncReturns } from "node:child_process" +import path, { dirname, join, resolve } from "path" import process from "process" -import path from "path" +import { fileURLToPath } from "url" interface Variant { platform: string @@ -136,11 +135,14 @@ if (buildNative) { continue } - const indexTsContent = `const module = await import("./${libraryFileName}", { with: { type: "file" } }) + const indexJsContent = `const module = await import("./${libraryFileName}", { with: { type: "file" } }) const path = module.default export default path; ` - writeFileSync(join(nativeDir, "index.ts"), indexTsContent) + const indexDtsContent = `declare const path: string +export default path;` + writeFileSync(join(nativeDir, "index.js"), indexJsContent) + writeFileSync(join(nativeDir, "index.d.ts"), indexDtsContent) writeFileSync( join(nativeDir, "package.json"), @@ -149,8 +151,8 @@ export default path; name: nativeName, version: packageJson.version, description: `Prebuilt ${platform}-${arch} binaries for ${packageJson.name}`, - main: "index.ts", - types: "index.ts", + main: "index.js", + types: "index.d.ts", license: packageJson.license, author: packageJson.author, homepage: packageJson.homepage, diff --git a/packages/core/src/zig.ts b/packages/core/src/zig.ts index 90f79ac1d..a70725f3e 100644 --- a/packages/core/src/zig.ts +++ b/packages/core/src/zig.ts @@ -1,58 +1,63 @@ -import { dlopen, toArrayBuffer, JSCallback, ptr, type Pointer } from "bun:ffi" -import { existsSync, writeFileSync } from "fs" +import { dlopen, JSCallback, ptr, toArrayBuffer, type Pointer } from "bun:ffi" import { EventEmitter } from "events" +import { existsSync, writeFileSync } from "fs" import { type CursorStyle, type CursorStyleOptions, - type TargetChannel, type DebugOverlayCorner, - type WidthMethod, type Highlight, type LineInfo, - type MousePointerStyle, + type TargetChannel, + type WidthMethod, } from "./types.js" -export type { LineInfo, AllocatorStats, BuildOptions } +export type { AllocatorStats, BuildOptions, LineInfo } -import { RGBA } from "./lib/RGBA.js" import { OptimizedBuffer } from "./buffer.js" -import { TextBuffer } from "./text-buffer.js" +import { isBunfsPath } from "./lib/bunfs.js" import { env, registerEnvVar } from "./lib/env.js" +import { RGBA } from "./lib/RGBA.js" +import { TextBuffer } from "./text-buffer.js" +import type { + AllocatorStats, + BuildOptions, + NativeSpanFeedOptions, + NativeSpanFeedStats, + ReserveInfo, +} from "./zig-structs.js" import { - StyledChunkStruct, - HighlightStruct, - LogicalCursorStruct, - VisualCursorStruct, - TerminalCapabilitiesStruct, - EncodedCharStruct, - LineInfoStruct, - MeasureResultStruct, + AllocatorStatsStruct, + BuildOptionsStruct, CursorStateStruct, CursorStyleOptionsStruct, + EncodedCharStruct, GridDrawOptionsStruct, + HighlightStruct, + LineInfoStruct, + LogicalCursorStruct, + MeasureResultStruct, NativeSpanFeedOptionsStruct, NativeSpanFeedStatsStruct, ReserveInfoStruct, - BuildOptionsStruct, - AllocatorStatsStruct, -} from "./zig-structs.js" -import type { - NativeSpanFeedOptions, - NativeSpanFeedStats, - ReserveInfo, - BuildOptions, - AllocatorStats, + StyledChunkStruct, + TerminalCapabilitiesStruct, + VisualCursorStruct, } from "./zig-structs.js" -import { isBunfsPath } from "./lib/bunfs.js" -const module = await import(`@opentui/core-${process.platform}-${process.arch}/index.ts`) +const module = await import(`@opentui/core-${process.platform}-${process.arch}/index.js`) let targetLibPath = module.default if (isBunfsPath(targetLibPath)) { targetLibPath = targetLibPath.replace("../", "") } +if (targetLibPath.startsWith("file://")) { + targetLibPath = targetLibPath.slice(7) +} + if (!existsSync(targetLibPath)) { - throw new Error(`opentui is not supported on the current platform: ${process.platform}-${process.arch}`) + throw new Error( + `opentui is not supported on the current platform: ${process.platform}-${process.arch}: not found: ${targetLibPath}`, + ) } registerEnvVar({ @@ -3871,7 +3876,7 @@ export function resolveRenderLib(): RenderLib { opentuiLib = new FFIRenderLib(opentuiLibPath) } catch (error) { throw new Error( - `Failed to initialize OpenTUI render library: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to initialize OpenTUI render library: ${error instanceof Error ? error.stack : "Unknown error"}`, ) } } From 43438480f59ae2fce6285387a85689c107d36612 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:51:03 -0400 Subject: [PATCH 06/56] parse.keypress -> parse.keypress.js (always use .js for import) --- packages/core/src/testing/mock-keys.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/testing/mock-keys.test.ts b/packages/core/src/testing/mock-keys.test.ts index f183ada2f..e926d4071 100644 --- a/packages/core/src/testing/mock-keys.test.ts +++ b/packages/core/src/testing/mock-keys.test.ts @@ -385,7 +385,7 @@ describe("mock-keys", () => { }) test("pressTab with shift modifier parses as shift+tab", async () => { - const { parseKeypress } = await import("../lib/parse.keypress") + const { parseKeypress } = await import("../lib/parse.keypress.js") const mockRenderer = new MockRenderer() const mockKeys = createMockKeys(mockRenderer as any) @@ -975,7 +975,7 @@ describe("mock-keys", () => { describe("modifyOtherKeys Mode (CSI u variant)", () => { test("modifyOtherKeys sequences can be parsed by parseKeypress", async () => { - const { parseKeypress } = await import("../lib/parse.keypress") + const { parseKeypress } = await import("../lib/parse.keypress.js") // Test that our generated sequences can be parsed correctly const tests = [ From 3ed3fcad2e737b4afb9aef5f6124f9220368ca98 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:51:17 -0400 Subject: [PATCH 07/56] import.meta.dir -> import.meta.dirname (node compat) --- .../core/src/lib/tree-sitter/cache.test.ts | 13 +++--- packages/core/src/testing/mock-keys.test.ts | 4 +- .../core/src/tests/destroy-on-exit.test.ts | 4 +- .../src/tests/runtime-plugin-support.test.ts | 4 +- .../core/src/tests/runtime-plugin.test.ts | 40 +++++++++---------- .../tests/runtime-plugin-support.test.ts | 4 +- .../solid/tests/destroy-race-repro.test.ts | 4 +- ...untime-plugin-support-node-modules.test.ts | 4 +- .../runtime-plugin-support-preload.test.ts | 4 +- .../tests/runtime-plugin-support.test.ts | 4 +- packages/solid/tests/solid-plugin.test.ts | 6 +-- packages/web/scripts/verify-doc-examples.ts | 8 ++-- 12 files changed, 51 insertions(+), 48 deletions(-) diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index cea0a4014..2b0a1f816 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -1,12 +1,15 @@ -import { test, expect, beforeEach, beforeAll, afterAll, describe } from "bun:test" -import { TreeSitterClient, addDefaultParsers } from "./client.js" +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { mkdir, readdir, stat, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join, resolve } from "node:path" -import { mkdir, readdir, stat, writeFile } from "node:fs/promises" -import { readFileSync } from "node:fs" +import { TreeSitterClient } from "./client.js" import type { FiletypeParserOptions } from "./types.js" -describe("TreeSitterClient Caching", () => { +const shouldSkip = Bun.serve === undefined +const describeFn = shouldSkip ? describe.skip : describe + +describeFn("TreeSitterClient Caching", () => { let dataPath: string let testServer: any const TEST_PORT = 55231 diff --git a/packages/core/src/testing/mock-keys.test.ts b/packages/core/src/testing/mock-keys.test.ts index e926d4071..55c6ef0fe 100644 --- a/packages/core/src/testing/mock-keys.test.ts +++ b/packages/core/src/testing/mock-keys.test.ts @@ -1,6 +1,6 @@ -import { describe, test, expect } from "bun:test" -import { createMockKeys, KeyCodes } from "./mock-keys.js" +import { describe, expect, test } from "bun:test" import { PassThrough } from "stream" +import { createMockKeys, KeyCodes } from "./mock-keys.js" class MockRenderer { public stdin: PassThrough diff --git a/packages/core/src/tests/destroy-on-exit.test.ts b/packages/core/src/tests/destroy-on-exit.test.ts index 5f66b9d97..0eac7f73d 100644 --- a/packages/core/src/tests/destroy-on-exit.test.ts +++ b/packages/core/src/tests/destroy-on-exit.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" -const fixturePath = join(import.meta.dir, "destroy-on-exit.fixture.ts") +const fixturePath = join(import.meta.dirname, "destroy-on-exit.fixture.ts") const runFixture = (code: number, mode: "idle" | "during-render" = "idle") => { const result = Bun.spawnSync([process.execPath, fixturePath, code.toString(), mode], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/core/src/tests/runtime-plugin-support.test.ts b/packages/core/src/tests/runtime-plugin-support.test.ts index 2d935ae8c..43d3794ef 100644 --- a/packages/core/src/tests/runtime-plugin-support.test.ts +++ b/packages/core/src/tests/runtime-plugin-support.test.ts @@ -3,9 +3,9 @@ import { join } from "node:path" describe("runtime plugin support", () => { it("installs exactly once via drop-in module", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-support.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/core/src/tests/runtime-plugin.test.ts b/packages/core/src/tests/runtime-plugin.test.ts index 70b964a71..17809dfc0 100644 --- a/packages/core/src/tests/runtime-plugin.test.ts +++ b/packages/core/src/tests/runtime-plugin.test.ts @@ -194,9 +194,9 @@ describe("runtime plugin", () => { }) it("resolves runtime modules end-to-end in a subprocess", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -209,9 +209,9 @@ describe("runtime plugin", () => { }) it("resolves bare imports from external runtime roots", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-resolve-roots.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-resolve-roots.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -224,9 +224,9 @@ describe("runtime plugin", () => { }) it("rewrites runtime specifiers in node_modules modules by default", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-runtime-specifier.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-runtime-specifier.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -239,9 +239,9 @@ describe("runtime plugin", () => { }) it("rewrites runtime specifiers in node_modules .mjs modules", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-mjs.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-mjs.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -254,9 +254,9 @@ describe("runtime plugin", () => { }) it("rewrites runtime specifiers across node_modules ESM cycles", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-cycle.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-cycle.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -271,9 +271,9 @@ describe("runtime plugin", () => { }) it("does not keep stale node_modules package type across plugin instances", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-package-type-cache.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-package-type-cache.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -286,9 +286,9 @@ describe("runtime plugin", () => { }) it("rewrites bare imports for scoped node_modules package siblings when enabled", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -303,9 +303,9 @@ describe("runtime plugin", () => { }) it("does not rewrite non-runtime bare imports in node_modules modules by default", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-node-modules-no-bare-rewrite.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-no-bare-rewrite.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -318,9 +318,9 @@ describe("runtime plugin", () => { }) it("rewrites runtime specifiers when Bun canonicalizes a symlinked import path", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-path-alias.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-path-alias.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, @@ -338,9 +338,9 @@ describe("runtime plugin", () => { return } - const fixturePath = join(import.meta.dir, "runtime-plugin-windows-file-url.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-windows-file-url.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, "..", ".."), + cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/react/tests/runtime-plugin-support.test.ts b/packages/react/tests/runtime-plugin-support.test.ts index fbd0c3b35..82a747d6d 100644 --- a/packages/react/tests/runtime-plugin-support.test.ts +++ b/packages/react/tests/runtime-plugin-support.test.ts @@ -3,9 +3,9 @@ import { join } from "node:path" describe("react runtime plugin support", () => { it("loads external modules against host runtime exports", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-support.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/solid/tests/destroy-race-repro.test.ts b/packages/solid/tests/destroy-race-repro.test.ts index cb9d4fbf2..c1238e0ac 100644 --- a/packages/solid/tests/destroy-race-repro.test.ts +++ b/packages/solid/tests/destroy-race-repro.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" -const fixturePath = join(import.meta.dir, "destroy-race.fixture.tsx") +const fixturePath = join(import.meta.dirname, "destroy-race.fixture.tsx") type Mode = "external" | "helper" | "external-onmount" | "helper-onmount" | "external-active" | "helper-active" const runFixture = (mode: Mode) => { const result = Bun.spawnSync([process.execPath, fixturePath, mode], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/solid/tests/runtime-plugin-support-node-modules.test.ts b/packages/solid/tests/runtime-plugin-support-node-modules.test.ts index 440ff6ef7..2754b53d4 100644 --- a/packages/solid/tests/runtime-plugin-support-node-modules.test.ts +++ b/packages/solid/tests/runtime-plugin-support-node-modules.test.ts @@ -3,9 +3,9 @@ import { join } from "node:path" describe("solid runtime plugin support in node_modules", () => { it("rewrites runtime module specifiers for external node_modules modules", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-support-node-modules.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-support-node-modules.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/solid/tests/runtime-plugin-support-preload.test.ts b/packages/solid/tests/runtime-plugin-support-preload.test.ts index a04eccfd0..a0acc7068 100644 --- a/packages/solid/tests/runtime-plugin-support-preload.test.ts +++ b/packages/solid/tests/runtime-plugin-support-preload.test.ts @@ -3,9 +3,9 @@ import { join } from "node:path" describe("solid runtime plugin support with preload", () => { it("rewrites external TSX modules even when the preload plugin is already active", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-support-preload.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-support-preload.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/solid/tests/runtime-plugin-support.test.ts b/packages/solid/tests/runtime-plugin-support.test.ts index 5d5f56e34..9ec8c095b 100644 --- a/packages/solid/tests/runtime-plugin-support.test.ts +++ b/packages/solid/tests/runtime-plugin-support.test.ts @@ -3,9 +3,9 @@ import { join } from "node:path" describe("solid runtime plugin support", () => { it("loads external TSX modules against host runtime modules", () => { - const fixturePath = join(import.meta.dir, "runtime-plugin-support.fixture.ts") + const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/solid/tests/solid-plugin.test.ts b/packages/solid/tests/solid-plugin.test.ts index 6c6ff6a85..f6191dd13 100644 --- a/packages/solid/tests/solid-plugin.test.ts +++ b/packages/solid/tests/solid-plugin.test.ts @@ -1,8 +1,8 @@ +import { runtimeModuleIdForSpecifier } from "@opentui/core/runtime-plugin" import { describe, expect, it } from "bun:test" import { mkdtempSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" -import { runtimeModuleIdForSpecifier } from "@opentui/core/runtime-plugin" import { createSolidTransformPlugin } from "../scripts/solid-plugin.js" type ResolveCallback = (args: { path: string; importer: string }) => unknown | Promise @@ -181,9 +181,9 @@ describe("solid transform plugin", () => { }) it("transforms runtime-resolved modules end-to-end in a subprocess", () => { - const fixturePath = join(import.meta.dir, "solid-plugin.fixture.ts") + const fixturePath = join(import.meta.dirname, "solid-plugin.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { - cwd: join(import.meta.dir, ".."), + cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", env: process.env, diff --git a/packages/web/scripts/verify-doc-examples.ts b/packages/web/scripts/verify-doc-examples.ts index 186e958e1..08df52638 100644 --- a/packages/web/scripts/verify-doc-examples.ts +++ b/packages/web/scripts/verify-doc-examples.ts @@ -11,12 +11,12 @@ * 3. Reports any type errors found */ -import { readFile, writeFile, mkdir, rm } from "node:fs/promises" -import { join, relative } from "node:path" import { existsSync } from "node:fs" +import { mkdir, readFile, rm, writeFile } from "node:fs/promises" +import { join, relative } from "node:path" -const DOCS_DIR = join(import.meta.dir, "../src/content/docs") -const CORE_PACKAGE = join(import.meta.dir, "../../core") +const DOCS_DIR = join(import.meta.dirname, "../src/content/docs") +const CORE_PACKAGE = join(import.meta.dirname, "../../core") const CORE_DIST = join(CORE_PACKAGE, "dist") const TEST_DIR = "/tmp/opentui-doc-verify" From 94b6651cff6059df2f0943c25d015ebd727ca93e Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:55:39 -0400 Subject: [PATCH 08/56] vitest config --- packages/core/vitest.config.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/core/vitest.config.ts diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts new file mode 100644 index 000000000..6eec70fce --- /dev/null +++ b/packages/core/vitest.config.ts @@ -0,0 +1,29 @@ +import { basename, dirname, join } from "node:path" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + // globalSetup: "./src/nodejs/compat.ts", + environment: "node", + isolate: false, + // alias: { + // "bun:ffi": "./src/nodejs/bunModules/ffi.ts", + // "bun:test": "./src/nodejs/bunModules/test.ts", + // }, + execArgv: [ + // "--experimental-transform-types", + // "--import=tsx", + // "--import=@swc-node/register/esm", + "--no-experimental-strip-types", + "--experimental-transform-types", + // "--import=esbuild-register/loader", + "--import=./src/nodejs/compat.ts", + ], + experimental: { + viteModuleRunner: false, + }, + // Create independent snapshots from bun:test + resolveSnapshotPath: (testPath, ext) => + join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs.${ext}`), + }, +}) From f0b898996b01e2687f15616d845a13c14d0b4802 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 11:57:05 -0400 Subject: [PATCH 09/56] add test:nodejs to test script --- packages/core/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 8ceeed910..e9e02d5c3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,7 +24,8 @@ "bench:ts": "bun src/benchmark/native-span-feed-benchmark.ts --suite=quick --json=src/benchmark/latest-quick-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=default --json=src/benchmark/latest-default-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=large --json=src/benchmark/latest-large-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=all --json=src/benchmark/latest-all-bench-run.json && bun src/benchmark/native-span-feed-async-benchmark.ts --json=src/benchmark/latest-async-bench-run.json", "publish": "bun scripts/publish.ts", "test:js": "bun test", - "test": "bun run test:native && bun run test:js" + "test:nodejs": "npx vitest", + "test": "bun run test:native && bun run test:js && bun run test:nodejs" }, "license": "MIT", "devDependencies": { From 462b2b6c9189d0233e19d7fe6cd2de08ad3f9968 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 12:51:43 -0400 Subject: [PATCH 10/56] improve ffi compat --- packages/core/src/nodejs/bunModules/ffi.ts | 144 ++++++++++++++++++--- 1 file changed, 126 insertions(+), 18 deletions(-) diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/nodejs/bunModules/ffi.ts index fd4340ed8..0df49e136 100644 --- a/packages/core/src/nodejs/bunModules/ffi.ts +++ b/packages/core/src/nodejs/bunModules/ffi.ts @@ -453,13 +453,76 @@ function returnsToKoffiType(returns: FFITypeOrString | undefined): koffi.TypeSpe return ffiTypeToKoffiType(returns ?? FFIType.void) } +function isPointerType(type: FFITypeOrString | undefined): boolean { + if (type === undefined) return false + const num = typeof type === "number" ? type : FFITypeStringToType[type as keyof typeof FFITypeStringToType] + return num === FFIType.ptr || num === FFIType.pointer +} + +function isBigIntType(type: FFITypeOrString | undefined): boolean { + if (type === undefined) return false + const num = typeof type === "number" ? type : FFITypeStringToType[type as keyof typeof FFITypeStringToType] + return num === FFIType.i64 || num === FFIType.u64 || num === FFIType.i64_fast || num === FFIType.u64_fast +} + +// Maps addresses returned by ptr() back to the original Uint8Array. +// When the address appears as an FFI function argument, the wrapper passes the +// Uint8Array directly to koffi — both Bun and koffi pass a TypedArray's underlying +// memory address verbatim, so the native side can read/write JS-owned memory +// (important for output parameters). +const ptrBackingArrays = new Map>() + +function resolvePointerArg(arg: unknown): unknown { + if (typeof arg === "number") { + const ref = ptrBackingArrays.get(arg) + if (ref) { + const arr = ref.deref() + if (arr) return arr + } + // Real native address (e.g. from JSCallback.ptr or read from output buffer) — + // koffi accepts BigInt for pointer params. + return BigInt(arg) + } + return arg +} + function ffiFunctionToKoffiFunction unknown>( lib: koffi.IKoffiLib, name: string, type: FFIFunction, ): T & koffi.KoffiFunction { const func = lib.func(name, returnsToKoffiType(type.returns), argsToKoffiTypes(type.args)) - return func as T & koffi.KoffiFunction + + const ptrArgIndices: number[] = [] + if (type.args) { + for (let i = 0; i < type.args.length; i++) { + if (isPointerType(type.args[i])) ptrArgIndices.push(i) + } + } + const returnsPtr = isPointerType(type.returns) + // koffi may return small u64/i64 values as number instead of bigint; + // Bun always returns bigint for these types. + const returnsBigInt = isBigIntType(type.returns) + + if (ptrArgIndices.length === 0 && !returnsPtr && !returnsBigInt) { + return func as T & koffi.KoffiFunction + } + + const wrapper = (...args: unknown[]) => { + for (const i of ptrArgIndices) { + args[i] = resolvePointerArg(args[i]) + } + const result = func(...args) + if (returnsPtr && typeof result === "object" && result !== null) { + return Number(koffi.address(result)) as unknown + } + if (returnsBigInt && typeof result === "number") { + return BigInt(result) as unknown + } + return result + } + Object.defineProperty(wrapper, "name", { value: name }) + return wrapper as T & koffi.KoffiFunction } const KoffiNativeAlloc = Symbol("KoffiNativeAlloc") @@ -469,41 +532,86 @@ function nativeAlloc(value: object, bytes: number) { if (KoffiNativeAlloc in value) { return value[KoffiNativeAlloc] } - const ptr = koffi.alloc(koffi.types.uint8, bytes) + const alloc = koffi.alloc(koffi.types.uint8, bytes) Object.defineProperty(value, KoffiNativeAlloc, { - value: ptr, + value: alloc, writable: false, configurable: true, enumerable: false, }) - NativeAllocRegistry.register(value, ptr) - return ptr + NativeAllocRegistry.register(value, alloc) + return alloc } /** - * Bun returns the pointer to the data backing a TypedArray, ArrayBuffer, etc, - * directly aliasing the data. koffi doesn't appear to expose such magicks, so - * we have to settle for faking it. + * Returns the address of a koffi-allocated copy of `value` — a real native + * address that can be embedded in packed structs for the native side to + * dereference. * - * TODO: copy the data back from opaque to the target after a call to native function. + * The original Uint8Array is also stored so that when this address is later + * passed as an FFI function argument, the wrapper can pass the original + * TypedArray to koffi instead (enabling write-back for output parameters). */ export const ptr: typeof bunPtr = (value) => { + const view = isArrayBufferView(value) + ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) + : new Uint8Array(value) + + // Allocate koffi memory and copy current data — gives a real native address + // that can be safely embedded in struct binary data. const opaque = nativeAlloc(value, value.byteLength) - const encodable = isArrayBufferView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value - koffi.encode(opaque, koffi.types.uint8, encodable, encodable.byteLength) - const pointer = Number(koffi.address(opaque)) - return pointer as Pointer + koffi.encode(opaque, koffi.types.uint8, view, view.byteLength) + const address = Number(koffi.address(opaque)) + + // Also store the original view so resolvePointerArg can pass it through + // to koffi for direct memory access (output parameter write-back). + ptrBackingArrays.set(address, new WeakRef(view)) + + return address as Pointer } -export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) => { - let ptrBigint = BigInt(pointer) - if (offset) { - ptrBigint += BigInt(offset) +// Lazy-loaded memcpy for copying from raw addresses +let _memcpy: ((dest: Uint8Array, src: bigint, n: number) => void) | undefined +function getMemcpy() { + if (!_memcpy) { + const libcName = + process.platform === "darwin" + ? "libSystem.B.dylib" + : process.platform === "win32" + ? "msvcrt.dll" + : "libc.so.6" + const libc = koffi.load(libcName) + const fn = libc.func("memcpy", "void*", ["void*", "void*", "size_t"]) + _memcpy = fn as unknown as (dest: Uint8Array, src: bigint, n: number) => void } + return _memcpy +} + +export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) => { if (length === undefined) { throw new Error(`bun:ffi.toArrayBuffer requires a length argument`) } - return koffi.view(ptrBigint, length) + + // If pointer is a koffi External, we can use koffi.view directly + if (typeof pointer === "object" && pointer !== null) { + if (offset) { + // Need to offset the pointer — convert to address and use memcpy path + const addr = koffi.address(pointer) + BigInt(offset) + const dest = new Uint8Array(length) + getMemcpy()(dest, addr, length) + return dest.buffer + } + return koffi.view(pointer, length) + } + + // For numeric addresses (Bun convention), use memcpy to copy into a new buffer + let ptrBigint = typeof pointer === "bigint" ? pointer : BigInt(pointer) + if (offset) { + ptrBigint += BigInt(offset) + } + const dest = new Uint8Array(length) + getMemcpy()(dest, ptrBigint, length) + return dest.buffer } function guessSuffix() { From 833d9819df2c03922a78a627cd10ae9545b3a634 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:04:56 -0400 Subject: [PATCH 11/56] fix circular import under nodejs --- packages/core/src/lib/selection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/selection.ts b/packages/core/src/lib/selection.ts index fe42d0f46..a2428ec7b 100644 --- a/packages/core/src/lib/selection.ts +++ b/packages/core/src/lib/selection.ts @@ -1,4 +1,5 @@ -import { Renderable, type ViewportBounds } from "../index.js" +import { Renderable } from "../Renderable.js" +import type { ViewportBounds } from "../types.js" import { coordinateToCharacterIndex, fonts } from "./ascii.font.js" class SelectionAnchor { From 5afa7712d8f6fc3b54aa3b2feb05b041cd2ff9ee Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:05:32 -0400 Subject: [PATCH 12/56] remove double .. in foo.test.ts.nodejs..snap --- packages/core/vitest.config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 6eec70fce..eb4524588 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -1,3 +1,8 @@ +/** + * Vitest is used to run tests under Node.js. + * bun:test imports are replaced with Vitest in nodejs/compat.ts. + */ + import { basename, dirname, join } from "node:path" import { defineConfig } from "vitest/config" @@ -20,10 +25,11 @@ export default defineConfig({ "--import=./src/nodejs/compat.ts", ], experimental: { + // Disable Vite bundling entirely so we exersize the nodejs/compat.ts shim. viteModuleRunner: false, }, // Create independent snapshots from bun:test resolveSnapshotPath: (testPath, ext) => - join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs.${ext}`), + join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs${ext}`), }, }) From 77c586de959c4e246a8e53ac260371190efcc23e Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:06:23 -0400 Subject: [PATCH 13/56] NodeBun.spawnSync --- packages/core/src/nodejs/NodeBun.ts | 47 +++++++++++++++++++++++++++-- packages/core/src/nodejs/compat.ts | 2 +- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts index b40b8a5be..02a9aeca1 100644 --- a/packages/core/src/nodejs/NodeBun.ts +++ b/packages/core/src/nodejs/NodeBun.ts @@ -1,6 +1,7 @@ import type { WriteFileOptions } from "node:fs" import * as fs from "node:fs/promises" import * as path from "node:path" +import * as cp from "node:child_process" import { isArrayBufferView } from "node:util/types" /** @@ -28,6 +29,12 @@ type BunWrite = typeof Bun.write type BunFileLike = { name: string | undefined } type BunPathLike = string | NodeJS.TypedArray | ArrayBufferLike | URL +type SpawnSyncOptions = Bun.SpawnOptions.SpawnSyncOptions< + Bun.SpawnOptions.Writable, + Bun.SpawnOptions.Readable, + Bun.SpawnOptions.Readable +> + class NodeBunError extends Error { constructor(message: string) { super(message) @@ -106,9 +113,43 @@ class NodeBun implements NodeBunInterface { throw new NodeBunError("Bun.spawn is not supported in Node.js") } - get spawnSync(): typeof Bun.spawnSync { - throw new NodeBunError("Bun.spawnSync is not supported in Node.js") - } + spawnSync: typeof Bun.spawnSync = (( + cmdsOrOptions: string[] | (SpawnSyncOptions & { cmd: string[] }), + options?: SpawnSyncOptions, + ): Bun.SyncSubprocess => { + let cmd: string[] + let opts: SpawnSyncOptions + if (Array.isArray(cmdsOrOptions)) { + cmd = cmdsOrOptions + opts = options ?? {} + } else { + cmd = cmdsOrOptions.cmd + opts = cmdsOrOptions + } + + const [file, ...args] = cmd + const result = cp.spawnSync(file, args, { + cwd: opts.cwd, + env: opts.env as NodeJS.ProcessEnv | undefined, + stdio: [ + opts.stdin === "pipe" ? "pipe" : "ignore", + opts.stdout === "pipe" ? "pipe" : "ignore", + opts.stderr === "pipe" ? "pipe" : "ignore", + ], + timeout: opts.timeout, + maxBuffer: opts.maxBuffer, + }) + + return { + stdout: result.stdout ?? Buffer.alloc(0), + stderr: result.stderr ?? Buffer.alloc(0), + exitCode: result.status ?? 1, + success: result.status === 0, + pid: result.pid ?? 0, + signalCode: result.signal ?? undefined, + resourceUsage: undefined!, + } as Bun.SyncSubprocess + }) as typeof Bun.spawnSync get build(): typeof Bun.build { throw new NodeBunError("Bun.build is not supported in Node.js") diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index 7a66026f2..53342ad56 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -9,7 +9,7 @@ export function setup() { Object.defineProperty(globalThis, "Bun", { configurable: true, enumerable: true, - get: () => require("./NodeBun.js"), + get: () => require("./NodeBun.js").default, }) mod.registerHooks({ resolve: resolveBun, load: loadBun }) From 9adca2b941542a39a9628ed44809fd7f6f70336e Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:11:08 -0400 Subject: [PATCH 14/56] shim Worker --- packages/core/src/nodejs/compat.ts | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index 53342ad56..30eac1511 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -1,7 +1,29 @@ import * as mod from "node:module" +import { Worker as NodeWorker, isMainThread, parentPort } from "node:worker_threads" const require = mod.createRequire(import.meta.url) +/** + * Wraps node:worker_threads Worker to match the Web Worker API surface + * used by this project (constructor with URL string, .onmessage, .onerror, + * .postMessage, .terminate). + */ +class WebWorkerShim extends NodeWorker { + onmessage: ((event: { data: unknown }) => void) | null = null + onerror: ((event: { message: string }) => void) | null = null + + constructor(url: string | URL) { + const resolved = typeof url === "string" && url.startsWith("file://") ? new URL(url) : url + super(resolved, { execArgv: [...process.execArgv, `--import=${import.meta.url}`] }) + this.on("message", (data: unknown) => { + this.onmessage?.({ data }) + }) + this.on("error", (error: Error) => { + this.onerror?.({ message: error.message }) + }) + } +} + /** * Sets up Bun shims in a Node.js process. */ @@ -12,6 +34,24 @@ export function setup() { get: () => require("./NodeBun.js").default, }) + if (globalThis.Worker === undefined) { + ;(globalThis as any).Worker = WebWorkerShim + } + + // Inside a worker thread, bridge Web Worker messaging API to parentPort + if (!isMainThread && parentPort) { + ;(globalThis as any).postMessage = (msg: unknown) => parentPort!.postMessage(msg) + Object.defineProperty(globalThis, "onmessage", { + configurable: true, + set(handler: ((event: { data: unknown }) => void) | null) { + parentPort!.removeAllListeners("message") + if (handler) { + parentPort!.on("message", (data: unknown) => handler({ data })) + } + }, + }) + } + mod.registerHooks({ resolve: resolveBun, load: loadBun }) if (process.env.VITEST) { mod.registerHooks({ resolve: resolveJsToTs }) From dec884feccdc741e278702f1e0bc721b930615af Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:14:28 -0400 Subject: [PATCH 15/56] type: wasm -> returns path to wasm file --- packages/core/src/nodejs/compat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index 30eac1511..75d8c9e13 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -77,7 +77,7 @@ const resolveBun: mod.ResolveHookSync = (request, context, next) => { } const loadBun: mod.LoadHookSync = (url, context, next) => { - if (context.importAttributes?.type === "file") { + if (context.importAttributes?.type === "file" || context.importAttributes?.type === "wasm") { return { shortCircuit: true, format: "json", From 7c5ce5540f005a027c456ab31969f6a42c98aeb4 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:22:14 -0400 Subject: [PATCH 16/56] fix stripANSI and stringWidth --- packages/core/src/nodejs/NodeBun.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts index 02a9aeca1..46959d222 100644 --- a/packages/core/src/nodejs/NodeBun.ts +++ b/packages/core/src/nodejs/NodeBun.ts @@ -1,8 +1,10 @@ +import * as cp from "node:child_process" import type { WriteFileOptions } from "node:fs" import * as fs from "node:fs/promises" import * as path from "node:path" -import * as cp from "node:child_process" import { isArrayBufferView } from "node:util/types" +import stringWidth from "string-width" +import stripANSI from "strip-ansi" /** * ```bash @@ -57,15 +59,8 @@ class NodeBun implements NodeBunInterface { return new Promise((resolve) => setTimeout(resolve, ms)) } - stringWidth(text: string): number { - const stringWidth = import.meta.require("string-width") - return stringWidth(text) - } - - stripANSI(text: string): string { - const stripANSI = import.meta.require("strip-ansi") - return stripANSI(text) - } + stringWidth = stringWidth + stripANSI = stripANSI write: typeof Bun.write = (destination, data, options): Promise => { let dest: string | URL From 4abe3a8e309980f32d6d14410f4a8f4458c78629 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:54:56 -0400 Subject: [PATCH 17/56] fix test -> use extension --- packages/core/src/lib/parse.mouse.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/parse.mouse.test.ts b/packages/core/src/lib/parse.mouse.test.ts index 4bc42779a..7e99fc383 100644 --- a/packages/core/src/lib/parse.mouse.test.ts +++ b/packages/core/src/lib/parse.mouse.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach } from "bun:test" -import { MouseParser, type RawMouseEvent } from "./parse.mouse" +import { MouseParser, type RawMouseEvent } from "./parse.mouse.js" // Encode a basic/X10 mouse event: ESC [ M Cb Cx Cy // buttonByte is the logical value (before the +32 wire offset), x/y are 0-based. From 134b0918cb9aebb02cec61f36a9fde2a4aa24e97 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:55:11 -0400 Subject: [PATCH 18/56] inject compat into subprocess execSync --- packages/core/src/nodejs/NodeBun.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts index 46959d222..58b02d2bf 100644 --- a/packages/core/src/nodejs/NodeBun.ts +++ b/packages/core/src/nodejs/NodeBun.ts @@ -122,7 +122,14 @@ class NodeBun implements NodeBunInterface { opts = cmdsOrOptions } - const [file, ...args] = cmd + const [file, ...rawArgs] = cmd + // When spawning node, inject compat shims so the child process can load .ts + // files and use Bun APIs (bun:ffi, bun:test, etc.) + let args = rawArgs + if (file === process.execPath || file === "node") { + const compatPath = new URL("./compat.ts", import.meta.url).pathname + args = ["--experimental-transform-types", `--import=${compatPath}`, ...rawArgs] + } const result = cp.spawnSync(file, args, { cwd: opts.cwd, env: opts.env as NodeJS.ProcessEnv | undefined, From 7f74456cdc0c1fc886a4694f04d64abf3aabae50 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:55:54 -0400 Subject: [PATCH 19/56] Improve pointer bi-directional mapping in callbacks etc --- packages/core/src/nodejs/bunModules/ffi.ts | 54 ++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/nodejs/bunModules/ffi.ts index 0df49e136..4bb42d472 100644 --- a/packages/core/src/nodejs/bunModules/ffi.ts +++ b/packages/core/src/nodejs/bunModules/ffi.ts @@ -420,8 +420,31 @@ export class JSCallback implements BunJSCallback { #registeredCallback: koffi.IKoffiRegisteredCallback | null constructor(callback: (...args: any[]) => any, definition: FFIFunction) { + // Wrap callback to convert koffi External pointer args → numbers (Bun convention), + // mirroring the conversion done for FFI function return values. + const ptrArgIndices: number[] = [] + if (definition.args) { + for (let i = 0; i < definition.args.length; i++) { + if (isPointerType(definition.args[i])) ptrArgIndices.push(i) + } + } + const wrappedCallback = + ptrArgIndices.length > 0 + ? (...args: any[]) => { + for (const i of ptrArgIndices) { + const arg = args[i] + if (typeof arg === "object" && arg !== null) { + const addr = Number(koffi.address(arg)) + ptrExternals.set(addr, new WeakRef(arg)) + args[i] = addr + } + } + return callback(...args) + } + : callback + const proto = koffi.proto(returnsToKoffiType(definition.returns), argsToKoffiTypes(definition.args)) - this.#registeredCallback = koffi.register(callback, koffi.pointer(proto)) + this.#registeredCallback = koffi.register(wrappedCallback, koffi.pointer(proto)) this.#threadsafe = definition.threadsafe ?? false } @@ -472,6 +495,11 @@ function isBigIntType(type: FFITypeOrString | undefined): boolean { // (important for output parameters). const ptrBackingArrays = new Map>() +// Maps numeric pointer addresses (from FFI return values) back to their koffi External. +// toArrayBuffer needs the External to create a live koffi.view() (zero-copy alias of +// native memory) rather than a one-time memcpy snapshot. +const ptrExternals = new Map>() + function resolvePointerArg(arg: unknown): unknown { if (typeof arg === "number") { const ref = ptrBackingArrays.get(arg) @@ -514,7 +542,9 @@ function ffiFunctionToKoffiFunction unknown>( } const result = func(...args) if (returnsPtr && typeof result === "object" && result !== null) { - return Number(koffi.address(result)) as unknown + const addr = Number(koffi.address(result)) + ptrExternals.set(addr, new WeakRef(result)) + return addr as unknown } if (returnsBigInt && typeof result === "number") { return BigInt(result) as unknown @@ -604,7 +634,25 @@ export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) return koffi.view(pointer, length) } - // For numeric addresses (Bun convention), use memcpy to copy into a new buffer + // Check if we have the koffi External for this address — if so, use koffi.view + // for a live zero-copy alias (matching Bun's toArrayBuffer behavior). + if (typeof pointer === "number") { + const ref = ptrExternals.get(pointer) + if (ref) { + const external = ref.deref() + if (external) { + if (offset) { + const addr = koffi.address(external) + BigInt(offset) + const dest = new Uint8Array(length) + getMemcpy()(dest, addr, length) + return dest.buffer + } + return koffi.view(external, length) + } + } + } + + // Fallback: memcpy for addresses we don't have an External for let ptrBigint = typeof pointer === "bigint" ? pointer : BigInt(pointer) if (offset) { ptrBigint += BigInt(offset) From 510b8b826931dff3732751536a1748eec81fea6e Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:56:05 -0400 Subject: [PATCH 20/56] improve test compat --- packages/core/src/nodejs/bunModules/test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/core/src/nodejs/bunModules/test.ts b/packages/core/src/nodejs/bunModules/test.ts index 405f87103..cc084bee7 100644 --- a/packages/core/src/nodejs/bunModules/test.ts +++ b/packages/core/src/nodejs/bunModules/test.ts @@ -1,5 +1,14 @@ -import { vi } from "vitest" +import { vi, expect } from "vitest" export * from "vitest" -export const mock = vi.mock +export const mock = vi.fn export const spyOn = vi.spyOn + +// Bun's toInclude → vitest's toContain +expect.extend({ + toInclude(received: unknown, expected: unknown) { + const pass = + typeof received === "string" ? received.includes(expected as string) : Array.isArray(received) ? received.includes(expected) : false + return { pass, message: () => `expected ${this.utils.printReceived(received)} to include ${this.utils.printExpected(expected)}` } + }, +}) From 1a1189ceed916f1adeeedd4c2fd436ccb3e750f4 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 13:56:19 -0400 Subject: [PATCH 21/56] renderable.test: async import instead of require --- packages/core/src/tests/renderable.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tests/renderable.test.ts b/packages/core/src/tests/renderable.test.ts index 39454c99a..6fff14df2 100644 --- a/packages/core/src/tests/renderable.test.ts +++ b/packages/core/src/tests/renderable.test.ts @@ -118,8 +118,8 @@ describe("Renderable", () => { expect(renderable.liveCount).toBe(0) }) - test("isRenderable", () => { - const { isRenderable } = require("../Renderable") + test("isRenderable", async () => { + const { isRenderable } = await import("../Renderable.js") const renderable = new TestBaseRenderable({}) expect(isRenderable(renderable)).toBe(true) expect(isRenderable({})).toBe(false) From 508de9bb7f0930f24a87659a42b547f6105dcf92 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:02:43 -0400 Subject: [PATCH 22/56] skip testing bun-only plugin under node --- .../src/tests/runtime-plugin-support.test.ts | 5 +++- .../core/src/tests/runtime-plugin.test.ts | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/core/src/tests/runtime-plugin-support.test.ts b/packages/core/src/tests/runtime-plugin-support.test.ts index 43d3794ef..b30127632 100644 --- a/packages/core/src/tests/runtime-plugin-support.test.ts +++ b/packages/core/src/tests/runtime-plugin-support.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" -describe("runtime plugin support", () => { +// Fixtures require `import { plugin } from "bun"` — no Node.js equivalent. +const _describe = process.versions.bun ? describe : describe.skip + +_describe("runtime plugin support", () => { it("installs exactly once via drop-in module", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { diff --git a/packages/core/src/tests/runtime-plugin.test.ts b/packages/core/src/tests/runtime-plugin.test.ts index 17809dfc0..6b12c487d 100644 --- a/packages/core/src/tests/runtime-plugin.test.ts +++ b/packages/core/src/tests/runtime-plugin.test.ts @@ -193,7 +193,11 @@ describe("runtime plugin", () => { ) }) - it("resolves runtime modules end-to-end in a subprocess", () => { + // Subprocess fixture tests require `import { plugin } from "bun"` which has + // no Node.js equivalent. + const bunIt = process.versions.bun ? it : it.skip + + bunIt("resolves runtime modules end-to-end in a subprocess", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -208,7 +212,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("core=core-value;coreTesting=true;sync=sync-value;async=async-value") }) - it("resolves bare imports from external runtime roots", () => { + bunIt("resolves bare imports from external runtime roots", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-resolve-roots.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -223,7 +227,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("marker=resolved-from-external-root") }) - it("rewrites runtime specifiers in node_modules modules by default", () => { + bunIt("rewrites runtime specifiers in node_modules modules by default", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-runtime-specifier.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -238,7 +242,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("marker=resolved-from-node-modules-runtime-specifier") }) - it("rewrites runtime specifiers in node_modules .mjs modules", () => { + bunIt("rewrites runtime specifiers in node_modules .mjs modules", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-mjs.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -253,7 +257,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("marker=resolved-from-node-modules-mjs") }) - it("rewrites runtime specifiers across node_modules ESM cycles", () => { + bunIt("rewrites runtime specifiers across node_modules ESM cycles", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-cycle.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -270,7 +274,7 @@ describe("runtime plugin", () => { ) }) - it("does not keep stale node_modules package type across plugin instances", () => { + bunIt("does not keep stale node_modules package type across plugin instances", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-package-type-cache.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -285,7 +289,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("marker=resolved-after-package-type-change") }) - it("rewrites bare imports for scoped node_modules package siblings when enabled", () => { + bunIt("rewrites bare imports for scoped node_modules package siblings when enabled", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -302,7 +306,7 @@ describe("runtime plugin", () => { ) }) - it("does not rewrite non-runtime bare imports in node_modules modules by default", () => { + bunIt("does not rewrite non-runtime bare imports in node_modules modules by default", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-no-bare-rewrite.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -317,7 +321,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("errorContainsMissingBareDependency=true") }) - it("rewrites runtime specifiers when Bun canonicalizes a symlinked import path", () => { + bunIt("rewrites runtime specifiers when Bun canonicalizes a symlinked import path", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-path-alias.fixture.ts") const result = Bun.spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), @@ -333,7 +337,7 @@ describe("runtime plugin", () => { expect(stdout).toContain("marker=resolved-from-path-alias") }) - it("rewrites runtime specifiers for file URL imports on Windows", () => { + bunIt("rewrites runtime specifiers for file URL imports on Windows", () => { if (process.platform !== "win32") { return } @@ -351,4 +355,5 @@ describe("runtime plugin", () => { expect(result.exitCode).toBe(0) expect(stdout).toContain("marker=resolved-from-windows-file-url") }) + }) From bf4bcd3dea677669a4ccca96ec92fc518aed2f12 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:10:41 -0400 Subject: [PATCH 23/56] fix type: file to return abs path, not url --- packages/core/src/nodejs/compat.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index 75d8c9e13..fee7bb2ae 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -1,4 +1,5 @@ import * as mod from "node:module" +import { fileURLToPath } from "node:url" import { Worker as NodeWorker, isMainThread, parentPort } from "node:worker_threads" const require = mod.createRequire(import.meta.url) @@ -78,10 +79,13 @@ const resolveBun: mod.ResolveHookSync = (request, context, next) => { const loadBun: mod.LoadHookSync = (url, context, next) => { if (context.importAttributes?.type === "file" || context.importAttributes?.type === "wasm") { + // Bun's `import ... with { type: "file" }` returns the absolute file path. + // Convert file:// URL to a path to match. + const filePath = url.startsWith("file://") ? fileURLToPath(url) : url return { shortCircuit: true, format: "json", - source: JSON.stringify(url), + source: JSON.stringify(filePath), } } From d9dc3894d131df27679ae1d37637d12dd6f5667a Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:10:58 -0400 Subject: [PATCH 24/56] work around difference in toEqual w/ array + properties --- packages/core/src/renderables/__tests__/markdown-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/renderables/__tests__/markdown-parser.test.ts b/packages/core/src/renderables/__tests__/markdown-parser.test.ts index d8c45e668..d98056395 100644 --- a/packages/core/src/renderables/__tests__/markdown-parser.test.ts +++ b/packages/core/src/renderables/__tests__/markdown-parser.test.ts @@ -46,7 +46,7 @@ test("handles empty content", () => { const state = parseMarkdownIncremental("", null) expect(state.content).toBe("") - expect(state.tokens).toEqual([]) + expect(Array.from(state.tokens)).toEqual([]) }) test("handles empty previous state", () => { From 236af0be9c0d2733bd3895bf2f23c582556b9f67 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:24:31 -0400 Subject: [PATCH 25/56] fix const enum using node built in ts transform --- packages/core/vitest.config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index eb4524588..bbb19a520 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -25,8 +25,13 @@ export default defineConfig({ "--import=./src/nodejs/compat.ts", ], experimental: { - // Disable Vite bundling entirely so we exersize the nodejs/compat.ts shim. + // Disable Vite bundling entirely so we exercise the nodejs/compat.ts shim. viteModuleRunner: false, + // Disable vitest's native Node loader hooks — they call + // module.stripTypeScriptTypes() (strip-only mode) which doesn't support + // const enum or parameter properties. We rely on Node.js's + // --experimental-transform-types instead. + nodeLoader: false, }, // Create independent snapshots from bun:test resolveSnapshotPath: (testPath, ext) => From 0e4b4920551a9679613f6deddb8fa368f1c3f9fd Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:43:20 -0400 Subject: [PATCH 26/56] try out some examples --- packages/core/src/nodejs/compat.ts | 7 ++----- scripts/run-node.sh | 10 ++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100755 scripts/run-node.sh diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index fee7bb2ae..fab3122bc 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -54,9 +54,7 @@ export function setup() { } mod.registerHooks({ resolve: resolveBun, load: loadBun }) - if (process.env.VITEST) { - mod.registerHooks({ resolve: resolveJsToTs }) - } + mod.registerHooks({ resolve: resolveJsToTs }) } const BUN_PREFIX = "bun:" @@ -122,5 +120,4 @@ const resolveJsToTs: mod.ResolveHookSync = (request, context, next) => { return next(tsRequest, context) } -// Auto-setup when loaded via --import in vitest workers -if (process.env.VITEST) setup() +setup() diff --git a/scripts/run-node.sh b/scripts/run-node.sh new file mode 100755 index 000000000..d61a5606d --- /dev/null +++ b/scripts/run-node.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Run a TypeScript file with Node.js using the opentui compat shim. +# Usage: ./run-node.sh src/examples/simple-layout-example.ts +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export OPENTUI_NODE_COMPAT=1 +exec node \ + --experimental-transform-types \ + "--import=${SCRIPT_DIR}/../packages/core/src/nodejs/compat.ts" \ + "$@" From 19226bd99f126b04e7bfca82ba9495f8596c1d87 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 14:47:17 -0400 Subject: [PATCH 27/56] replace Bun.serve w/ node createServer --- .../core/src/lib/tree-sitter/cache.test.ts | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index 2b0a1f816..0e7d88701 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test" +import { createServer, type Server } from "node:http" import { readFileSync } from "node:fs" import { mkdir, readdir, stat, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" @@ -6,30 +7,34 @@ import { join, resolve } from "node:path" import { TreeSitterClient } from "./client.js" import type { FiletypeParserOptions } from "./types.js" -const shouldSkip = Bun.serve === undefined -const describeFn = shouldSkip ? describe.skip : describe - -describeFn("TreeSitterClient Caching", () => { +describe("TreeSitterClient Caching", () => { let dataPath: string - let testServer: any + let testServer: Server const TEST_PORT = 55231 const BASE_URL = `http://localhost:${TEST_PORT}` beforeAll(async () => { - const assetsDir = resolve(__dirname, "assets") - testServer = Bun.serve({ - port: TEST_PORT, - fetch(req) { - const url = new URL(req.url) - const filePath = join(assetsDir, url.pathname) - return new Response(readFileSync(filePath)) - }, + const assetsDir = resolve(import.meta.dirname, "assets") + testServer = createServer((req, res) => { + const filePath = join(assetsDir, req.url ?? "/") + try { + const data = readFileSync(filePath) + res.writeHead(200) + res.end(data) + } catch { + res.writeHead(404) + res.end("Not found") + } + }) + await new Promise((resolve, reject) => { + testServer.on("error", reject) + testServer.listen(TEST_PORT, resolve) }) }) afterAll(async () => { if (testServer) { - testServer.stop() + await new Promise((resolve) => testServer.close(() => resolve())) } }) From 1762f663b48f4818fc23baad491826f95a259416 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:14:42 -0400 Subject: [PATCH 28/56] fix test failure under vitest if describe has no tests --- packages/core/src/edit-buffer.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/edit-buffer.test.ts b/packages/core/src/edit-buffer.test.ts index da35d2439..ca1010dce 100644 --- a/packages/core/src/edit-buffer.test.ts +++ b/packages/core/src/edit-buffer.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeEach, afterEach } from "bun:test" +import { afterEach, beforeEach, describe, expect, it } from "bun:test" import { EditBuffer } from "./edit-buffer.js" describe("EditBuffer", () => { @@ -742,6 +742,11 @@ describe("EditBuffer Placeholder", () => { afterEach(() => { buffer.destroy() }) + + if (!process.versions.bun) { + // nodejs vitest fails unless there's at least one test in a describe block + it.todo("placeholder tests", () => {}) + } }) describe("EditBuffer Events", () => { From d85a814b2f1c0289ca2c07468ac7507b5b7acdfe Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:15:18 -0400 Subject: [PATCH 29/56] Fix FFI difference w/ empty ArrayBuffer --- packages/core/src/nodejs/bunModules/ffi.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/nodejs/bunModules/ffi.ts index 4bb42d472..31615f1db 100644 --- a/packages/core/src/nodejs/bunModules/ffi.ts +++ b/packages/core/src/nodejs/bunModules/ffi.ts @@ -479,7 +479,7 @@ function returnsToKoffiType(returns: FFITypeOrString | undefined): koffi.TypeSpe function isPointerType(type: FFITypeOrString | undefined): boolean { if (type === undefined) return false const num = typeof type === "number" ? type : FFITypeStringToType[type as keyof typeof FFITypeStringToType] - return num === FFIType.ptr || num === FFIType.pointer + return num === FFIType.ptr } function isBigIntType(type: FFITypeOrString | undefined): boolean { @@ -500,6 +500,11 @@ const ptrBackingArrays = new Map>() // native memory) rather than a one-time memcpy snapshot. const ptrExternals = new Map>() +// koffi passes null for 0-length TypedArrays, but Bun passes a valid non-null +// address. Native code may treat null as "no data" even when length is also +// passed as 0. Use a static 1-byte sentinel so the pointer is always non-null. +const emptyPtrSentinel = new Uint8Array(1) + function resolvePointerArg(arg: unknown): unknown { if (typeof arg === "number") { const ref = ptrBackingArrays.get(arg) @@ -511,6 +516,9 @@ function resolvePointerArg(arg: unknown): unknown { // koffi accepts BigInt for pointer params. return BigInt(arg) } + if (isArrayBufferView(arg) && arg.byteLength === 0) { + return emptyPtrSentinel + } return arg } @@ -544,10 +552,10 @@ function ffiFunctionToKoffiFunction unknown>( if (returnsPtr && typeof result === "object" && result !== null) { const addr = Number(koffi.address(result)) ptrExternals.set(addr, new WeakRef(result)) - return addr as unknown + return addr } if (returnsBigInt && typeof result === "number") { - return BigInt(result) as unknown + return BigInt(result) } return result } @@ -605,11 +613,7 @@ let _memcpy: ((dest: Uint8Array, src: bigint, n: number) => void) | undefined function getMemcpy() { if (!_memcpy) { const libcName = - process.platform === "darwin" - ? "libSystem.B.dylib" - : process.platform === "win32" - ? "msvcrt.dll" - : "libc.so.6" + process.platform === "darwin" ? "libSystem.B.dylib" : process.platform === "win32" ? "msvcrt.dll" : "libc.so.6" const libc = koffi.load(libcName) const fn = libc.func("memcpy", "void*", ["void*", "void*", "size_t"]) _memcpy = fn as unknown as (dest: Uint8Array, src: bigint, n: number) => void From 321502e3cfd70677f54d913e5524eebcd6260454 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:15:31 -0400 Subject: [PATCH 30/56] Fix Worker shim to throw error async --- packages/core/src/nodejs/compat.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts index fab3122bc..00070ac68 100644 --- a/packages/core/src/nodejs/compat.ts +++ b/packages/core/src/nodejs/compat.ts @@ -15,13 +15,26 @@ class WebWorkerShim extends NodeWorker { constructor(url: string | URL) { const resolved = typeof url === "string" && url.startsWith("file://") ? new URL(url) : url - super(resolved, { execArgv: [...process.execArgv, `--import=${import.meta.url}`] }) + let constructorError: Error | null = null + try { + super(resolved, { execArgv: [...process.execArgv, `--import=${import.meta.url}`] }) + } catch (e) { + // Bun defers worker errors instead of throwing synchronously. + // Allocate a dummy worker and fire the error async. + super(new URL(import.meta.url), { execArgv: [] }) + constructorError = e as Error + this.terminate() + } this.on("message", (data: unknown) => { this.onmessage?.({ data }) }) this.on("error", (error: Error) => { this.onerror?.({ message: error.message }) }) + if (constructorError) { + const err = constructorError + queueMicrotask(() => this.emit("error", err)) + } } } From d2f45ada11c7e88112dc4f07f4626eacb4fcc3d2 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:16:27 -0400 Subject: [PATCH 31/56] SPOOKY: potentially double-render if Yoga layout dirtied by onSizeChange once --- packages/core/src/Renderable.ts | 11 +++++++++++ packages/core/src/renderables/ScrollBox.ts | 13 +------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/Renderable.ts b/packages/core/src/Renderable.ts index 993ba357c..b06d77795 100644 --- a/packages/core/src/Renderable.ts +++ b/packages/core/src/Renderable.ts @@ -1648,6 +1648,17 @@ export class RootRenderable extends Renderable { this.renderList.length = 0 this.updateLayout(deltaTime, this.renderList) + // 2b. onSizeChange callbacks during updateLayout may dirty the yoga tree + // (e.g. ScrollBox hides a scrollbar). Re-layout so dimensions converge + // within this frame instead of relying on a deferred second render, which + // has different timing between Bun and Node.js (process.nextTick fires + // during an await in Bun but after it resolves in Node.js). + if (this.yogaNode.isDirty()) { + this.calculateLayout() + this.renderList.length = 0 + this.updateLayout(deltaTime, this.renderList) + } + // 3. Render all collected renderables this._ctx.clearHitGridScissorRects() for (let i = 1; i < this.renderList.length; i++) { diff --git a/packages/core/src/renderables/ScrollBox.ts b/packages/core/src/renderables/ScrollBox.ts index 528379ac2..7afe51b21 100644 --- a/packages/core/src/renderables/ScrollBox.ts +++ b/packages/core/src/renderables/ScrollBox.ts @@ -770,18 +770,7 @@ export class ScrollBoxRenderable extends BoxRenderable { this._isApplyingStickyScroll = wasApplyingStickyScroll } - // NOTE: This is obviously a workaround for something, - // which is that the bar props are recalculated when the viewport is resized, - // which intially happens onUpdate but is the viewport does not have the correct dimensions yet, - // then when it does, no update is triggered and when we do we are in the middle of a render, - // which just ignores the request. ¯\_(ツ)_/¯ - // TODO: Fix this properly. How? Move yoga to native, get all changes for elements in one go - // and update all renderables in one go before rendering. - // OR: Move this logic to the viewport. IMHO the wrapper and viewport are overkill and not necessary. - // The Scrollbox can be the viewport, we are using translations on the content anyway. - process.nextTick(() => { - this.requestRender() - }) + this.requestRender() } // Setters for reactive properties From 6c69fd611edd801d4e89d5d195d6b7d54629d436 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:17:41 -0400 Subject: [PATCH 32/56] check in nodejs snapshots --- .../__snapshots__/buffer.test.ts.nodejs.snap | 28 + .../__snapshots__/Code.test.ts.nodejs.snap | 13 + .../__snapshots__/Diff.test.ts.nodejs.snap | 785 ++++++++++++++++++ .../__snapshots__/Text.test.ts.nodejs.snap | 421 ++++++++++ .../TextTable.test.ts.nodejs.snap | 215 +++++ ...rable.scrollbox-simple.test.ts.nodejs.snap | 89 ++ ...erRenderable.scrollbox.test.ts.nodejs.snap | 457 ++++++++++ .../LineNumberRenderable.test.ts.nodejs.snap | 158 ++++ .../Textarea.rendering.test.ts.nodejs.snap | 387 +++++++++ ...e-positioning.snapshot.test.ts.nodejs.snap | 481 +++++++++++ .../renderable.snapshot.test.ts.nodejs.snap | 19 + .../scrollbox.test.ts.nodejs.snap | 29 + 12 files changed, 3082 insertions(+) create mode 100644 packages/core/src/__snapshots__/buffer.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__snapshots__/Code.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__snapshots__/Diff.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__snapshots__/Text.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__snapshots__/TextTable.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.nodejs.snap create mode 100644 packages/core/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.nodejs.snap create mode 100644 packages/core/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.nodejs.snap create mode 100644 packages/core/src/tests/__snapshots__/renderable.snapshot.test.ts.nodejs.snap create mode 100644 packages/core/src/tests/__snapshots__/scrollbox.test.ts.nodejs.snap diff --git a/packages/core/src/__snapshots__/buffer.test.ts.nodejs.snap b/packages/core/src/__snapshots__/buffer.test.ts.nodejs.snap new file mode 100644 index 000000000..a7399f140 --- /dev/null +++ b/packages/core/src/__snapshots__/buffer.test.ts.nodejs.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`OptimizedBuffer > snapshot tests with unicode encoding > should handle multiline text with unicode > Multiline unicode rendering 1`] = ` +"Hi 世界 +🌟 Star + + + +" +`; + +exports[`OptimizedBuffer > snapshot tests with unicode encoding > should render ASCII text correctly > ASCII text rendering 1`] = ` +"Hello + + + + +" +`; + +exports[`OptimizedBuffer > snapshot tests with unicode encoding > should render emoji text correctly > Emoji text rendering 1`] = ` +"Hi 👋 🌍 + + + + +" +`; diff --git a/packages/core/src/renderables/__snapshots__/Code.test.ts.nodejs.snap b/packages/core/src/renderables/__snapshots__/Code.test.ts.nodejs.snap new file mode 100644 index 000000000..30aaf800d --- /dev/null +++ b/packages/core/src/renderables/__snapshots__/Code.test.ts.nodejs.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CodeRenderable - text renders immediately before highlighting completes > text visible after highlighting completes 1`] = ` +"const message = 'hello world'; + +" +`; + +exports[`CodeRenderable - text renders immediately before highlighting completes > text visible before highlighting completes 1`] = ` +"const message = 'hello world'; + +" +`; diff --git a/packages/core/src/renderables/__snapshots__/Diff.test.ts.nodejs.snap b/packages/core/src/renderables/__snapshots__/Diff.test.ts.nodejs.snap new file mode 100644 index 000000000..890bc16ed --- /dev/null +++ b/packages/core/src/renderables/__snapshots__/Diff.test.ts.nodejs.snap @@ -0,0 +1,785 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DiffRenderable - add-only diff split view > split view add-only diff 1`] = ` +" 1 + function newFunction() { + 2 + return true; + 3 + } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - add-only diff unified view > unified view add-only diff 1`] = ` +" 1 + function newFunction() { + 2 + return true; + 3 + } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - asymmetric block with more adds than removes in split view > split view asymmetric block more adds 1`] = ` +" 1 context_before 1 context_before + 2 - remove1 2 + add1 + 3 - remove2 3 + add2 + 4 + add3 + 5 + add4 + 6 + add5 + 4 context_after 7 context_after + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - asymmetric block with more removes than adds in split view > split view asymmetric block more removes 1`] = ` +" 1 context_before 1 context_before + 2 - remove1 2 + add1 + 3 - remove2 3 + add2 + 4 - remove3 + 5 - remove4 + 6 - remove5 + 7 context_after 4 context_after + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - back-to-back change blocks without context lines in split view > split view back-to-back blocks 1`] = ` +" 1 - remove1 1 + add1 + 2 - remove2 2 + add2 + 3 - remove3 3 + add3 + 4 - remove4 4 + add4 + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - can toggle conceal with markdown diff > markdown diff with conceal disabled 1`] = ` +" 1 First line + 2 - Some text **old** + 2 + Some text **boldtext** and *italic* + 3 End line + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - can toggle conceal with markdown diff > markdown diff with conceal enabled 1`] = ` +" 1 First line + 2 - Some text old** + 2 + So text**boldext** and *italic* + 3 End line + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - conceal works in split view > split view markdown diff with conceal disabled 1`] = ` +" 1 First line 1 First line + 2 - Some **old** text 2 + Some **new** text + 3 End line 3 End line + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - conceal works in split view > split view markdown diff with conceal enabled 1`] = ` +" 1 First line 1 First line + 2 - Some old text 2 + Some new text + 3 End line 3 End line + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - consistent left padding for line numbers > 9 > unified view with double-digit line numbers 1`] = ` +" 8 line8 + 9 line9 + 10 - line10_old + 10 + line10_new + 11 line11 + 12 + line12_added + 13 + line13_added + 14 line14 + 15 line15 + 14 - line16_old + 16 + line16_new + + + + + + + + + +" +`; + +exports[`DiffRenderable - diff with only context lines (no changes) > diff with only context lines 1`] = ` +" 1 line1 + 2 line2 + 3 line3 + 4 line4 + 5 line5 + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - invalid diff format shows error with raw diff > invalid diff format with error 1`] = ` +"Error parsing diff: Unknown line 5 "- console.log(\\"Hello\\");" + +--- a/test.js ++++ b/test.js +@@ -a,b +c,d @@ + function hello() { +- console.log("Hello"); ++ console.log("Hello, World!"); + } + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - large line numbers displayed correctly > unified view large line numbers 1`] = ` +" 42 const line42 = 'context'; + 43 const line43 = 'context'; + 44 - const line44 = 'removed'; + 44 + const line44 = 'added'; + 45 const line45 = 'context'; + 46 + const line46 = 'added'; + 47 const line47 = 'context'; + 48 const line48 = 'context'; + 48 - const line49 = 'removed'; + 49 + const line49 = 'changed'; + 50 const line50 = 'context'; + 51 const line51 = 'context'; + + + + + + + + +" +`; + +exports[`DiffRenderable - line numbers hidden for empty alignment lines in split view > split view with hidden line numbers for empty lines 1`] = ` +" 1 + function newFunction() { + 2 + return true; + 3 + } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - line numbers update correctly after resize causes wrapping changes > after resize - line numbers with wrapping 1`] = ` +" 1 function + calculateSomethingVeryComplexWithALongFunctionNameThat + WillWrap() { + 2 - const + oldResultWithAVeryLongVariableNameThatWillDefinitelyWr + apWhenRenderedInASmallerTerminal = 42; + 2 + const + newResultWithAVeryLongVariableNameThatWillDefinitelyWr + apWhenRenderedInASmallerTerminal = 100; + 3 return result; + 4 } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - line numbers update correctly after resize causes wrapping changes > before resize - line numbers with no wrapping 1`] = ` +" 1 function calculateSomethingVeryComplexWithALongFunctionNameThatWillWrap() { + 2 - const oldResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 42; + 2 + const newResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 100; + 3 return result; + 4 } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - multi-line diff split view > split view multi-line diff 1`] = ` +" 1 function add(a, b) { 1 function add(a, b) { + 2 return a + b; 2 return a + b; + 3 } 3 } + 4 4 + 5 + function subtract(a, b) { + 6 + return a - b; + 7 + } + 8 + + 5 function multiply(a, b) { 9 function multiply(a, b) { + 6 - return a * b; 10 + return a * b * 1; + 7 } 11 } + + + + + + + + + +" +`; + +exports[`DiffRenderable - multi-line diff unified view > unified view multi-line diff 1`] = ` +" 1 function add(a, b) { + 2 return a + b; + 3 } + 4 + 5 + function subtract(a, b) { + 6 + return a - b; + 7 + } + 8 + + 9 function multiply(a, b) { + 6 - return a * b; + 10 + return a * b * 1; + 11 } + + + + + + + + +" +`; + +exports[`DiffRenderable - multiple hunks in split view > split view multiple hunks 1`] = ` +" 1 function first() { 1 function first() { + 2 - return 1; 2 + return "one"; + 3 } 3 } + 15 function second() { 15 function second() { + 16 var x = 10; 16 var x = 10; + 17 + var y = 20; + 17 return x; 18 return x; + 18 } 19 } + 30 function third() { 31 function third() { + 31 - console.log("old"); 32 + console.log("new"); + 32 } 33 } + + + + + + + + + +" +`; + +exports[`DiffRenderable - multiple hunks in unified view > unified view multiple hunks 1`] = ` +" 1 function first() { + 2 - return 1; + 2 + return "one"; + 3 } + 15 function second() { + 16 var x = 10; + 17 + var y = 20; + 18 return x; + 19 } + 31 function third() { + 31 - console.log("old"); + 32 + console.log("new"); + 33 } + + + + + + + +" +`; + +exports[`DiffRenderable - no newline at end of file in split view > split view with no newline marker 1`] = ` +" 1 line1 1 line1 + 2 line2 2 line2 + 3 - line3 + 3 + line3_modified + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - no newline at end of file in unified view > unified view with no newline marker 1`] = ` +" 1 line1 + 2 line2 + 3 - line3 + 3 + line3_modified + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - remove-only diff split view > split view remove-only diff 1`] = ` +" 1 - function oldFunction() { + 2 - return false; + 3 - } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - remove-only diff unified view > unified view remove-only diff 1`] = ` +" 1 - function oldFunction() { + 2 - return false; + 3 - } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - split view alignment with empty lines > split view alignment 1`] = ` +" 1 line1 1 line1 + 2 + line2_added + 3 + line3_added + 4 + line4_added + 2 line5 5 line5 + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - split view renders correctly > split view simple diff 1`] = ` +" 1 function hello() { 1 function hello() { + 2 - console.log("Hello"); 2 + console.log("Hello, World!"); + 3 } 3 } + + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - unified view renders correctly > unified view simple diff 1`] = ` +" 1 function hello() { + 2 - console.log("Hello"); + 2 + console.log("Hello, World!"); + 3 } + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - very long lines wrapping multiple times in split view > split view multi-wrap lines 1`] = ` +" 1 short line 1 short line + 2 - This is an extremely long line 2 + This is an extremely long line + that will definitely wrap multiple that has been modified and will + times when rendered in a split definitely wrap multiple times + view with word wrapping enabled when rendered in a split view with + because it contains so many words word wrapping enabled because it + and characters contains so many words and + characters and even more content + 3 another short line 3 another short line + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - wrapMode works in unified view > wrapMode-none 1`] = ` +" 1 function hello() { + 2 - console.log("This is a very long line that should wrap when wrapMode is s + 2 + console.log("This is a very long line that has been modified and should w + 3 } + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - wrapMode works in unified view > wrapMode-none 2`] = ` +" 1 function hello() { + 2 - console.log("This is a very long line that should wrap when wrapMode is s + 2 + console.log("This is a very long line that has been modified and should w + 3 } + + + + + + + + + + + + + + + + +" +`; + +exports[`DiffRenderable - wrapMode works in unified view > wrapMode-word 1`] = ` +" 1 function hello() { + 2 - console.log("This is a very long line that should wrap when wrapMode is + set to word but not when it is set to none"); + 2 + console.log("This is a very long line that has been modified and should + wrap when wrapMode is set to word but not when it is set to none"); + 3 } + + + + + + + + + + + + + + +" +`; diff --git a/packages/core/src/renderables/__snapshots__/Text.test.ts.nodejs.snap b/packages/core/src/renderables/__snapshots__/Text.test.ts.nodejs.snap new file mode 100644 index 000000000..47d067a66 --- /dev/null +++ b/packages/core/src/renderables/__snapshots__/Text.test.ts.nodejs.snap @@ -0,0 +1,421 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TextRenderable Selection > Absolute Positioned Box with Text > should handle width:100% text in absolute positioned box with constrained maxWidth 1`] = ` +" + + + + + + + This is an extremely long piece of text + that needs to wrap multiple times within + the constrained width of the absolutely + positioned container box with significant + padding on all sides. + + + +" +`; + +exports[`TextRenderable Selection > Absolute Positioned Box with Text > should render multiple text elements in absolute positioned box with proper spacing 1`] = ` +" + + + ┌───────────────────────────────────────────┐ + │ │ + │ System Update │ + │ │ + │ A new version is available with bug │ + │ fixes and performance improvements. │ + │ │ + │ Click to install │ + │ │ + └───────────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`TextRenderable Selection > Absolute Positioned Box with Text > should render text fully visible in absolute positioned box at various positions 1`] = ` +" + ┌──────────────────────────────────────┐ + │ Error: File not found in the │ + │ specified directory path │ + └──────────────────────────────────────┘ + + + + + + + + + + + + + + + + ─────────────────────────────────── + Success: Operation completed + successfully! + ─────────────────────────────────── + +" +`; + +exports[`TextRenderable Selection > Absolute Positioned Box with Text > should render text in absolute positioned box with padding and borders correctly 1`] = ` +" + + │ │ + │ │ + │ Important Notification │ + │ │ + │ │ + │ This is a longer message that should wrap properly within + │ │ + │ │ + + + + + + + + + + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render TextNode text composition correctly 1`] = ` +"First Second Third + + + + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render basic text content correctly 1`] = ` +" + + + Hello World + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render empty buffer correctly 1`] = ` +" + + + + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render multiline text content correctly 1`] = ` +" + Line 1: Hello + Line 2: World + Line 3: Testing + Line 4: Multiline +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render text positioning correctly 1`] = ` +"Top + + Mid + + Bot +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render text with character wrapping correctly 1`] = ` +"This is a very +long text that +should wrap to +multiple lines +when wrap is en +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render text with graphemes/emojis correctly 1`] = ` +" + +Hello 🌍 World 👋 + Test 🚀 Emoji + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render text with tab indicator correctly 1`] = ` +"Line 1→ Tabbed +Line 2→ → Double tab + + + +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render wrapped multiline text correctly 1`] = ` +" +First li +ne with +long con +tent +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render wrapped text with different content 1`] = ` +" + ABCDEFGHIJ + KLMNOPQRST + UVWXYZ abc + defghijklm +" +`; + +exports[`TextRenderable Selection > Text Content Snapshots > should render wrapped text with emojis and graphemes 1`] = ` +" Hello 🌍 Wor + ld 👋 This i + s a test wit + h emojis 🚀 + that should +" +`; + +exports[`TextRenderable Selection > Text Node Dimension Updates > should handle multiple text node updates with complex layout changes 1`] = ` +"First part +Middle text +Bottom text + + + + + + + +" +`; + +exports[`TextRenderable Selection > Text Node Dimension Updates > should handle multiple text node updates with complex layout changes 2`] = ` +"First of +a +sentence +partthat +will wrap +Middle text +Bottom text + + + +" +`; + +exports[`TextRenderable Selection > Text Node Dimension Updates > should update dimensions and reposition subsequent elements when text nodes expand 1`] = ` +"Short +Second text + + + +" +`; + +exports[`TextRenderable Selection > Text Node Dimension Updates > should update dimensions and reposition subsequent elements when text nodes expand 2`] = ` +"Short text that will + definitely wrap +Second text + + +" +`; + +exports[`TextRenderable Selection > Width/Height Setter Layout Tests > should not shrink box when height is set via setter in column layout with text 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`TextRenderable Selection > Width/Height Setter Layout Tests > should not shrink box when minHeight is set via setter in column layout with text 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`TextRenderable Selection > Width/Height Setter Layout Tests > should not shrink box when minWidth is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection > Width/Height Setter Layout Tests > should not shrink box when width is set from undefined via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection > Width/Height Setter Layout Tests > should not shrink box when width is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should compare char vs word wrapping with same content 1`] = ` +"Hello +wonderful +world of +text +wrapping +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should correctly wrap text when updating content via text.content 1`] = ` +"Short text + + + + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should correctly wrap text when updating content via text.content 2`] = ` +"This is a much +longer text that +should definitely +wrap to multiple +lines +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should dynamically change wrap mode 1`] = ` +"The quick +brown fox +jumps + + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should handle long words that exceed wrap width in word mode 1`] = ` +"ABCDEFGHIJ +KLMNOPQRST +UVWXYZ + + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should handle word wrapping with hyphens and dashes 1`] = ` +"self- +contained +multi-line +text- +wrapping +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should handle word wrapping with punctuation 1`] = ` +"Hello, +World. +Test- +Example/ +Path +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should handle word wrapping with single character words 1`] = ` +"a b c d +e f g h +i j k l +m n o p + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should preserve empty lines with word wrapping 1`] = ` +"First +line + +Third +line +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should wrap at character boundaries when using char mode 1`] = ` +"The quick brown + fox jumps over + the lazy dog + + +" +`; + +exports[`TextRenderable Selection > Word Wrapping > should wrap at word boundaries when using word mode 1`] = ` +"The quick +brown fox +jumps over the +lazy dog + +" +`; diff --git a/packages/core/src/renderables/__snapshots__/TextTable.test.ts.nodejs.snap b/packages/core/src/renderables/__snapshots__/TextTable.test.ts.nodejs.snap new file mode 100644 index 000000000..e9925bb85 --- /dev/null +++ b/packages/core/src/renderables/__snapshots__/TextTable.test.ts.nodejs.snap @@ -0,0 +1,215 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TextTableRenderable > balanced fitter keeps constrained columns visually closer > fitter balanced constrained 1`] = ` +"┌────────┬────────┬─────────┬─────────┬────────┬─────────┐ +│Provider│Compute │Storage │Pricing │Regions │Use Cases│ +│ │Services│Solutions│Model │ │ │ +├────────┼────────┼─────────┼─────────┼────────┼─────────┤ +│Amazon │EC2 │S3 tiers,│Pay as │Global │Enterpris│ +│Web │instance│ EBS, │you go, │regions │e migrati│ +│Services│s with e│EFS, and │reserved │and │on, analy│ +│ │xtensive│archive │terms, │many │tics, ML,│ +│ │ options│classes │and │edge │ and back│ +│ │ for gen│for long │discounte│location│end servi│ +│ │eral, me│retention│d spot ca│s │ces │ +│ │mory, an│ │pacity │ │ │ +│ │d accele│ │ │ │ │ +│ │rated wo│ │ │ │ │ +│ │rkloads │ │ │ │ │ +└────────┴────────┴─────────┴─────────┴────────┴─────────┘ +" +`; + +exports[`TextTableRenderable > balanced fitter keeps constrained columns visually closer > fitter proportional constrained 1`] = ` +"┌────┬─────────────┬─────────┬─────────┬───────┬─────────┐ +│Prov│Compute │Storage │Pricing │Regions│Use Cases│ +│ider│Services │Solutions│Model │ │ │ +├────┼─────────────┼─────────┼─────────┼───────┼─────────┤ +│Amaz│EC2 │S3 tiers,│Pay as │Global │Enterpris│ +│on W│instances │ EBS, │you go, │regions│e migrati│ +│eb S│with │EFS, and │reserved │ and ma│on, analy│ +│ervi│extensive │archive │terms, │ny edge│tics, ML,│ +│ces │options for │classes │and │ locati│ and back│ +│ │general, │for long │discounte│ons │end servi│ +│ │memory, and │retention│d spot ca│ │ces │ +│ │accelerated │ │pacity │ │ │ +│ │workloads │ │ │ │ │ +└────┴─────────────┴─────────┴─────────┴───────┴─────────┘ + + +" +`; + +exports[`TextTableRenderable > keeps borders aligned with CJK and emoji content > unicode border alignment 1`] = ` +"┌──────┬──────────────┐ +│Locale│Sample │ +├──────┼──────────────┤ +│ja-JP │東京で寿司 🍣 │ +├──────┼──────────────┤ +│zh-CN │你好世界 🚀 │ +├──────┼──────────────┤ +│ko-KR │한글 테스트 😄│ +└──────┴──────────────┘ + + + + + + + +" +`; + +exports[`TextTableRenderable > keeps full wrapped table layouts after a wide-to-narrow demo-style resize > demo resize expected primary table 1`] = ` +"┌─────────────────────────┬─────────────┬────────────────────────────┐ +│Task │Owner │ETA │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Wrap regression in │core │done after validating none, │ +│operational status │platform and │word, and char wrap modes │ +│dashboard with dynamic │runtime │across narrow, medium, wide,│ +│row heights and │reliability │ and ultra-wide terminal │ +│constrained layout │squad │widths │ +│validation │ │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Unicode layout │render │in review with follow-up │ +│stabilization for mixed │pipeline │checks for border style │ +│Latin, punctuation, │maintainers │transitions, cell padding │ +│symbols, and long │with │variants, and selection │ +│identifiers in adjacent │fallback │range consistency │ +│columns │shaping │ │ +│ │support │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Snapshot pass for table │qa │today pending final │ +│rendering in content │automation │baseline updates for │ +│mode and full mode with │and visual │oversized fixtures that │ +│heavy and double border │diff triage │intentionally stress │ +│combinations │group │wrapping behavior on high- │ +│ │ │resolution terminals │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Document edge cases │developer │planned for this sprint │ +│where long tokens │experience │once final reproducible │ +│without spaces force │and docs │examples are captured and │ +│char wrapping and reveal │tooling │linked to regression │ +│per-cell clipping │ │tracking tickets │ +│regressions │ │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Performance sweep of │runtime │scheduled after review, │ +│wrapping algorithm under │performance │with benchmark runs on │ +│large datasets to │task force │laptop and desktop │ +│confirm stable frame │ │terminals at 200-plus │ +│times during rapid key │ │column widths │ +│toggling │ │ │ +└─────────────────────────┴─────────────┴────────────────────────────┘ +" +`; + +exports[`TextTableRenderable > keeps full wrapped table layouts after a wide-to-narrow demo-style resize > demo resize expected unicode table 1`] = ` +"┌─────┬──────────────────────────────────────────────────────────────┐ +│Colum│Wrapped Text │ +│n │ │ +├─────┼──────────────────────────────────────────────────────────────┤ +│mixed│CJK and emoji wrapping stress case: こんにちは世界 and │ +│-lang│안녕하세요 세계 and 你好,世界 followed by long English prose │ +│uages│that keeps flowing to test whether each cell wraps naturally │ +│ │even when the terminal is extremely wide and the row still │ +│ │needs multiple visual lines for readability 🌍🚀 │ +├─────┼──────────────────────────────────────────────────────────────┤ +│emoji│Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version │ +│-and-│tags like release-candidate-build-2026-02-very-long-token- │ +│symbo│without-breaks to ensure char wrapping remains stable and no │ +│ls │glyph alignment issues appear at column boundaries │ +├─────┼──────────────────────────────────────────────────────────────┤ +│long-│長文の日本語テキストと中文段落和한국어문장을連続して配置し、 │ +│cjk- │その後に additional English context describing renderer │ +│phras│behavior, border intersection handling, and selection │ +│e │extraction so that this single cell remains a reliable │ +│ │wrapping torture test. │ +├─────┼──────────────────────────────────────────────────────────────┤ +│mixed│Wrap behavior with punctuation-heavy content: [alpha]{beta}( │ +│-punc│gamma)|epsilon| then repeated fragments, commas, │ +│tuati│semicolons, and slashes to verify token boundaries do not │ +│on │break border drawing logic or spacing consistency in │ +│ │neighboring columns. │ +└─────┴──────────────────────────────────────────────────────────────┘ +" +`; + +exports[`TextTableRenderable > rebuilds table when content setter is used > content setter update 1`] = ` +"┌─────┬───────┐ +│Col 1│Col 2 │ +├─────┼───────┤ +│row-1│updated│ +├─────┼───────┤ +│row-2│active │ +└─────┴───────┘ + + + + + + + + + +" +`; + +exports[`TextTableRenderable > renders a basic table with styled cell chunks > basic table 1`] = ` +" + ┌─────┬──────┬───────────────────┐ + │Name │Status│Notes │ + ├─────┼──────┼───────────────────┤ + │Alpha│OK │All systems nominal│ + ├─────┼──────┼───────────────────┤ + │Bravo│WARN │Pending checks │ + └─────┴──────┴───────────────────┘ + + + + + + + + +" +`; + +exports[`TextTableRenderable > wraps CJK and emoji without grapheme duplication > unicode wrapping 1`] = ` +"┌───┬────────────────────────┐ +│Ite│Details │ +│m │ │ +├───┼────────────────────────┤ +│mix│東京界 🌍 emoji │ +│ed │wrapping continues │ +│ │across lines for width │ +│ │checks │ +├───┼────────────────────────┤ +│emo│Faces 😀😃😄 should │ +│ji │remain stable │ +└───┴────────────────────────┘ + + + + +" +`; + +exports[`TextTableRenderable > wraps content and fits columns when width is constrained > wrapped constrained width 1`] = ` +"┌──┬─────────────────────────────┐ +│ID│Description │ +├──┼─────────────────────────────┤ +│1 │This is a long sentence that │ +│ │should wrap across multiple │ +│ │visual lines │ +├──┼─────────────────────────────┤ +│2 │Short │ +└──┴─────────────────────────────┘ + + + + + + + +" +`; diff --git a/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.nodejs.snap b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.nodejs.snap new file mode 100644 index 000000000..7f74147f0 --- /dev/null +++ b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.nodejs.snap @@ -0,0 +1,89 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LineNumber in ScrollBox - Simple Core Test > LineNumber with Code in ScrollBox should wrap content height 1`] = ` +" 1 function test() { + 2 return true; + 3 } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Simple Core Test > Multiple LineNumber blocks in ScrollBox should each wrap content 1`] = ` +" 1 const x = 1; + 1 const y = 2; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; diff --git a/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.nodejs.snap b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.nodejs.snap new file mode 100644 index 000000000..25f38822c --- /dev/null +++ b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.nodejs.snap @@ -0,0 +1,457 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LineNumberRenderable in ScrollBox > ScrollBox with horizontal and vertical scrolling - dimensions stable 1`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 const veryLongVariableName1 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 2 const veryLongVariableName2 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 3 const veryLongVariableName3 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 4 const veryLongVariableName4 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 5 const veryLongVariableName5 = "This is a │ +└────────────────────────────────────────────────┘ + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > ScrollBox with horizontal and vertical scrolling - dimensions stable 2`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 const veryLongVariableName1 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 2 const veryLongVariableName2 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 3 const veryLongVariableName3 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 4 const veryLongVariableName4 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 5 const veryLongVariableName5 = "This is a │ +└────────────────────────────────────────────────┘ + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > ScrollBox with horizontal and vertical scrolling - dimensions stable 3`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 const veryLongVariableName1 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 2 const veryLongVariableName2 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 3 const veryLongVariableName3 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 4 const veryLongVariableName4 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 5 const veryLongVariableName5 = "This is a │ +└────────────────────────────────────────────────┘ + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > ScrollBox with horizontal and vertical scrolling - dimensions stable 4`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 const veryLongVariableName1 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 2 const veryLongVariableName2 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 3 const veryLongVariableName3 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 4 const veryLongVariableName4 = "This is a │ +│ very long line that should require │ +│ horizontal scrolling to view completely"; │ +│ 5 const veryLongVariableName5 = "This is a │ +└────────────────────────────────────────────────┘ + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > gutter width changes with line count - verify remeasure 1`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ │ +│ │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > gutter width changes with line count - verify remeasure 2`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > gutter width changes with line count - verify remeasure 3`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > line colors span full width in ScrollBox 1`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└────────────────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > line colors span full width in ScrollBox 2`] = ` +"┌────────────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└────────────────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > multiple Code renderables with line numbers in ScrollBox - correct dimensions 1`] = ` +"┌────────────────────────────────────────────────┐ █ +│ 1 function test1() { │ █ +│ 2 console.log("Line 1"); │ █ +│ 3 return 1; │ █ +│ 4 } │ █ +│ 5 function test2() { │ █ +│ 6 console.log("Line 2"); │ █ +└────────────────────────────────────────────────┘ █ + █ + █ +┌────────────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +└────────────────────────────────────────────────┘ + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > multiple Code renderables with line numbers in ScrollBox - correct dimensions 2`] = ` +"│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +└────────────────────────────────────────────────┘ + + +┌────────────────────────────────────────────────┐ █ +│ 1 function test1() { │ █ +│ 2 console.log("Line 1"); │ █ +│ 3 return 1; │ █ +│ 4 } │ █ +│ 5 function test2() { │ █ +│ 6 console.log("Line 2"); │ █ +└────────────────────────────────────────────────┘ █ + █ + █ +┌────────────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +" +`; + +exports[`LineNumberRenderable in ScrollBox > nested boxes with different border styles - dimensions correct 1`] = ` +"╔═════════════════════════════════════════════════════╗ +║ ║ +║ ║ +║ ╭───────────────────────────────────────────╮ ║ +║ │ │ ║ +║ │ 1 function test1() { │ ║ +║ │ 2 console.log("Line 1"); │ ║ +║ │ 3 return 1; │ ║ +║ │ 4 } │ ║ +║ │ 5 function test2() { │ ║ +║ │ 6 console.log("Line 2"); │ ║ +║ │ 7 return 2; │ ║ +║ │ 8 } │ ║ +║ │ 9 function test3() { │ ║ +║ │ 10 console.log("Line 3"); │ ║ +║ │ 11 return 3; │ ║ +║ │ │ ║ +║ ╰───────────────────────────────────────────╯ ║ +╚═════════════════════════════════════════════════════╝ + +" +`; + +exports[`LineNumberRenderable in ScrollBox > nested boxes with different border styles - dimensions correct 2`] = ` +"╔═════════════════════════════════════════════════════╗ +║ ║ +║ ║ +║ ╭───────────────────────────────────────────╮ ║ +║ │ │ ║ +║ │ 1 function test1() { │ ║ +║ │ 2 console.log("Line 1"); │ ║ +║ │ 3 return 1; │ ║ +║ │ 4 } │ ║ +║ │ 5 function test2() { │ ║ +║ │ 6 console.log("Line 2"); │ ║ +║ │ 7 return 2; │ ║ +║ │ 8 } │ ║ +║ │ 9 function test3() { │ ║ +║ │ 10 console.log("Line 3"); │ ║ +║ │ 11 return 3; │ ║ +║ │ │ ║ +║ ╰───────────────────────────────────────────╯ ║ +╚═════════════════════════════════════════════════════╝ + +" +`; + +exports[`LineNumberRenderable in ScrollBox > single Code renderable in ScrollBox - scroll and verify dimensions 1`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > single Code renderable in ScrollBox - scroll and verify dimensions 2`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > single Code renderable in ScrollBox - scroll and verify dimensions 3`] = ` +"┌──────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line 2"); │ +│ 7 return 2; │ +│ 8 } │ +│ 9 function test3() { │ +│ 10 console.log("Line 3"); │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > single Code renderable with line numbers in ScrollBox - correct dimensions 1`] = ` +"┌────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line │ +│ 1"); │ +│ 3 return 1; │ +│ 4 } │ +│ 5 function test2() { │ +│ 6 console.log("Line │ +│ 2"); │ +└────────────────────────────┘ + + + + + + + + + + +" +`; + +exports[`LineNumberRenderable in ScrollBox > viewport culling with line numbers - dimensions stable 1`] = ` +"┌───────────────────────────────────────────┐ ▀ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +└───────────────────────────────────────────┘ + +┌───────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +└───────────────────────────────────────────┘ + +┌───────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +└───────────────────────────────────────────┘ +" +`; + +exports[`LineNumberRenderable in ScrollBox > viewport culling with line numbers - dimensions stable 2`] = ` +"│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +└───────────────────────────────────────────┘ + +┌───────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ +└───────────────────────────────────────────┘ + +┌───────────────────────────────────────────┐ +│ 1 function test1() { │ +│ 2 console.log("Line 1"); │ +│ 3 return 1; │ +│ 4 } │ ▄ +└───────────────────────────────────────────┘ + +┌───────────────────────────────────────────┐ +" +`; diff --git a/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.nodejs.snap b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.nodejs.snap new file mode 100644 index 000000000..e20854b32 --- /dev/null +++ b/packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.nodejs.snap @@ -0,0 +1,158 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LineNumberRenderable > combines line number offset with hidden line numbers 1`] = ` +" 42 Line 1 + Line 2 + 44 Line 3 + Line 4 + 46 Line 5 + + + + + +" +`; + +exports[`LineNumberRenderable > hides line numbers for specific lines 1`] = ` +" 1 Line 1 + Line 2 + 3 Line 3 + Line 4 + 5 Line 5 + + + + + +" +`; + +exports[`LineNumberRenderable > maintains consistent left padding for all line numbers 1`] = ` +" 1 Line 1 + 2 Line 2 + 3 Line 3 + 4 Line 4 + 5 Line 5 + 6 Line 6 + 7 Line 7 + 8 Line 8 + 9 Line 9 + 10 Line 10 + 11 Line 11 + 12 Line 12 + + + +" +`; + +exports[`LineNumberRenderable > maintains stable visual line count when scrolling and typing with word wrap 1`] = ` +" + ┌───────────────────────────────┐ + │ Ctrl+Y to redo │ + │ 36 │ + │ 37 VIEW: │ + │ 38 • Shift+W to toggle │ + │ wrap mode (word/char/ │ + │ none) │ + │ 39 • Shift+L to toggle │ + │ line numbers │ + │ 40 │ + │ 41 FEATURES: │ + │ 42 ✓ Grapheme-aware │ + │ cursor movement │ + │ 43 ✓ Unicode (emoji 🌟 │ + │ and CJK 世界, 你好世界, │ + │ 中文, 한글) │ + │ 44 ✓ Incremental editing │ + │ 45 ✓ Text wrapping and │ + │ viewport management │ + │ 46 ✓ Undo/redo support │ + │ 47 ✓ Word-based │ + │ navigation and deletion │ + │ 48 ✓ Text selection with │ + │ shift keys │ + │ 49 │ + │ 50 Press ESC to return to │ + │ main menu │ + └───────────────────────────────┘ + +" +`; + +exports[`LineNumberRenderable > maintains stable visual line count when scrolling and typing with word wrap 2`] = ` +" + ┌───────────────────────────────┐ + │ Ctrl+Y to redo │ + │ 36 │ + │ 37 VIEW: │ + │ 38 • Shift+W to toggle │ + │ wrap mode (word/char/ │ + │ none) │ + │ 39 • Shift+L to toggle │ + │ line numbers │ + │ 40 │ + │ 41 FEATURES: │ + │ 42 ✓ Grapheme-aware │ + │ cursor movement │ + │ 43 ✓ Unicode (emoji 🌟 │ + │ and CJK 世界, 你好世界, │ + │ 中文, 한글) │ + │ 44 ✓ Incremental editing │ + │ 45 ✓ Text wrapping and │ + │ viewport management │ + │ 46 ✓ Undo/redo support │ + │ 47 ✓ Word-based │ + │ navigation and deletion │ + │ 48 ✓ Text selection with │ + │ shift keys │ + │ 49 a │ + │ 50 Press ESC to return to │ + │ main menu │ + └───────────────────────────────┘ + +" +`; + +exports[`LineNumberRenderable > renders line numbers correctly 1`] = ` +" 1 Line 1 + 2 Line 2 + 3 Line 3 + + + + + + + +" +`; + +exports[`LineNumberRenderable > renders line numbers for wrapping text 1`] = ` +" 1 Line 1 is very l + ong and should w + rap around multi + ple lines + + + + + + +" +`; + +exports[`LineNumberRenderable > renders line numbers with offset 1`] = ` +" 42 Line 1 + 43 Line 2 + 44 Line 3 + + + + + + + +" +`; diff --git a/packages/core/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.nodejs.snap b/packages/core/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.nodejs.snap new file mode 100644 index 000000000..09685c112 --- /dev/null +++ b/packages/core/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.nodejs.snap @@ -0,0 +1,387 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Textarea - Rendering Tests > Absolute Positioned Box with Textarea > should handle width:100% textarea in absolute positioned box with constrained maxWidth 1`] = ` +" + + + + + + + This is an extremely long piece of text + that needs to wrap multiple times within + the constrained width of the absolutely + positioned container box with significant + padding on all sides. + + + +" +`; + +exports[`Textarea - Rendering Tests > Absolute Positioned Box with Textarea > should render multiple textarea elements in absolute positioned box with proper spacing 1`] = ` +" + + + ┌───────────────────────────────────────────┐ + │ │ + │ System Update │ + │ │ + │ A new version is available with bug │ + │ fixes and performance improvements. │ + │ │ + │ Click to install │ + │ │ + └───────────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Absolute Positioned Box with Textarea > should render textarea fully visible in absolute positioned box at various positions 1`] = ` +" + ┌──────────────────────────────────────┐ + │ Error: File not found in the │ + │ specified directory path │ + └──────────────────────────────────────┘ + + + + + + + + + + + + + + + + ─────────────────────────────────── + Success: Operation completed + successfully! + ─────────────────────────────────── + +" +`; + +exports[`Textarea - Rendering Tests > Absolute Positioned Box with Textarea > should render textarea in absolute positioned box with padding and borders correctly 1`] = ` +" + + │ │ + │ │ + │ Important Notification │ + │ │ + │ │ + │ This is a longer message that should wrap properly within + │ │ + │ │ + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render basic text content correctly 1`] = ` +" + + + Hello World + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render multiline text content correctly 1`] = ` +" + Line 1: Hello + Line 2: World + Line 3: Testing + Line 4: Multiline + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render placeholder when creating textarea with placeholder directly 1`] = ` +" + Enter text here... + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render placeholder when set programmatically after creation 1`] = ` +" + Type something... + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render text with character wrapping correctly 1`] = ` +"This is a very +long text that +should wrap to +multiple lines +when wrap is en +abled + + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should render text with word wrapping and punctuation 1`] = ` +"Hello,World. +Test- +Example/ +Path with +various +punctuation +marks! + + + + + + + + + + + + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Textarea Content Snapshots > should resize correctly when typing return as first input with placeholder 1`] = ` +" + ┌────────────────────────────────────── + │ + │ + └────────────────────────────────────── + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Width/Height Setter Layout Tests > should not shrink box when height is set via setter in column layout with textarea 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Width/Height Setter Layout Tests > should not shrink box when minHeight is set via setter in column layout with textarea 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Width/Height Setter Layout Tests > should not shrink box when minWidth is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Width/Height Setter Layout Tests > should not shrink box when width is set from undefined via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Width/Height Setter Layout Tests > should not shrink box when width is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea - Rendering Tests > Wrapping > should render with tab indicator correctly 1`] = ` +"Line 1→ Tabbed +Line 2→ → Double tab + + + + + + + + + + + + + + + + + + + + + + +" +`; diff --git a/packages/core/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.nodejs.snap b/packages/core/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.nodejs.snap new file mode 100644 index 000000000..c415dcb0e --- /dev/null +++ b/packages/core/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.nodejs.snap @@ -0,0 +1,481 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Absolute Positioning - Snapshot Tests > Basic absolute positioning > absolute positioned box at bottom-right using right/bottom > absolute positioned box at bottom-right 1`] = ` +" + + + + + + + + + + + + + + + ┌─────────────┐ + │Bottom Right │ + │ │ + │ │ + └─────────────┘ +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Basic absolute positioning > absolute positioned box at top-left > absolute positioned box at top-left 1`] = ` +"┌─────────────┐ +│Top Left │ +│ │ +│ │ +└─────────────┘ + + + + + + + + + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Basic absolute positioning > absolute positioned box centered with left/top > absolute positioned box centered 1`] = ` +" + + + + + ┌──────────────────┐ + │Centered │ + │ │ + │ │ + │ │ + │ │ + │ │ + └──────────────────┘ + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Complex hierarchies > multiple nested relative and absolute layers > relative -> absolute -> relative -> absolute hierarchy 1`] = ` +"┌────────────────────────────────────┐ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ │ │ +│ │ ┌──────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────┐ │ │ │ +│ │ │ │Deep │ │ │ │ +│ │ │ └────────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────┘ │ │ +│ │ │ │ +│ └──────────────────────────────┘ │ +│ │ +└────────────────────────────────────┘ + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Complex hierarchies > relative parent with absolute child containing absolute grandchild > relative -> absolute -> absolute hierarchy 1`] = ` +" + ┌─────────────────────────────────┐ + │ │ + │ ┌──────────────────────────┐ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ┌──────────┐ │ │ + │ │ │Grand │ │ │ + │ │ │ │ │ │ + │ │ └──────────┘ │ │ + │ │ │ │ + │ └──────────────────────────┘ │ + │ │ + └─────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute child fills parent completely > absolute child fills parent with inset 0 1`] = ` +" + + + ┌────────────────────────────┐ + │╔══════════════════════════╗│ + │║Full ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │╚══════════════════════════╝│ + └────────────────────────────┘ + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute child with conflicting insets (left and right without explicit width) > absolute child with left and right insets (no explicit width) 1`] = ` +" + + ┌────────────────────────────────┐ + │ │ + │ │ + │ ┌──────────────────────────┐ │ + │ │Stretch │ │ + │ │ │ │ + │ │ │ │ + │ └──────────────────────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute child with conflicting insets (top and bottom without explicit height) > absolute child with top and bottom insets (no explicit height) 1`] = ` +" + ┌────────────────────────────┐ + │ │ + │ ┌─────────────┐ │ + │ │VStretch │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ └─────────────┘ │ + │ │ + └────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute positioned box extending beyond viewport > absolute box extending beyond viewport 1`] = ` +" + + + + + + + + + + + + + + + ┌───────── + │Overflow + │ + │ + │ +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute positioned box with negative coordinates (partially off-screen) > absolute box with negative coordinates 1`] = ` +" │ + │ + │ + │ + │ +──────────────┘ + + + + + + + + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute positioned box with percentage height inside absolute parent > absolute child with percentage height 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ ┌─────────────┐ │ + │ │50% H │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ └─────────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────┘ + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Edge cases > absolute positioned box with percentage width inside absolute parent > absolute child with percentage width 1`] = ` +" + + ┌──────────────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌─────────────────┐ │ + │ │50% │ │ + │ │ │ │ + │ └─────────────────┘ │ + │ │ + └──────────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Mixed positioning > absolute child inside relative parent > absolute child inside relative parent 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌──────────┐ │ + │ │Absolute │ │ + │ │ │ │ + │ └──────────┘ │ + │ │ + └────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Mixed positioning > sibling absolute elements at same level > sibling absolute elements overlapping 1`] = ` +"┌─────────────┐ +│Box 1 │ +│ │ +│ │ +│ ┌─────────────┐ +└───────────│Box 2 │ + │ │ + │ │ + │ ┌─────────────┐ + └───────────│Box 3 │ + │ │ + │ │ + │ │ + └─────────────┘ + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Nested absolute positioning > absolute child at bottom:0 inside absolute parent (issue #406 fix) > nested absolute - child at bottom:0 of parent 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌─────────────┐ │ + │ │At Bottom │ │ + │ └─────────────┘ │ + └────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Nested absolute positioning > absolute child at bottom-right corner inside absolute parent > nested absolute - child at bottom-right corner 1`] = ` +" + ┌────────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌────────────┐ │ + │ │Corner │ │ + │ │ │ │ + │ └────────────┘ │ + │ │ + └────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Nested absolute positioning > absolute child at right:0 inside absolute parent > nested absolute - child at right:0 of parent 1`] = ` +" + + ┌─────────────────────────────────┐ + │ │ + │ ┌──────────┐│ + │ │At Right ││ + │ │ ││ + │ └──────────┘│ + │ │ + │ │ + │ │ + │ │ + │ │ + └─────────────────────────────────┘ + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Nested absolute positioning > absolute child inside absolute parent - basic > nested absolute - child inside parent at left/top 1`] = ` +" + + + ┌────────────────────────────┐ + │ │ + │ ┌──────────┐ │ + │ │Nested │ │ + │ │ │ │ + │ └──────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────┘ + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Nested absolute positioning > multiple absolute children inside absolute parent at different positions > nested absolute - four corners inside parent 1`] = ` +" + ┌──────────────────────────────────┐ + │ │ + │ ┌────────┐ ┌────────┐ │ + │ │TL │ │TR │ │ + │ └────────┘ └────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌────────┐ ┌────────┐ │ + │ │BL │ │BR │ │ + │ └────────┘ └────────┘ │ + │ │ + └──────────────────────────────────┘ + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests > Three-level nesting > deeply nested absolute positioning - grandchild at bottom > three-level nested absolute - grandchild at bottom 1`] = ` +" + ┌────────────────────────────────────┐ + │ │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ┌─────────────┐ │ │ + │ │ │Deep │ │ │ + │ │ └─────────────┘ │ │ + │ │ │ │ + │ └──────────────────────────────┘ │ + │ │ + │ │ + └────────────────────────────────────┘ + +" +`; diff --git a/packages/core/src/tests/__snapshots__/renderable.snapshot.test.ts.nodejs.snap b/packages/core/src/tests/__snapshots__/renderable.snapshot.test.ts.nodejs.snap new file mode 100644 index 000000000..711c8b5d5 --- /dev/null +++ b/packages/core/src/tests/__snapshots__/renderable.snapshot.test.ts.nodejs.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Renderable - insertBefore > reproduces insertBefore behavior with state change after timeout > insertBefore initial state 1`] = ` +"banana +apple +pear + + +" +`; + +exports[`Renderable - insertBefore > reproduces insertBefore behavior with state change after timeout > insertBefore reordered state 1`] = ` +"banana +pear +apple + + +" +`; diff --git a/packages/core/src/tests/__snapshots__/scrollbox.test.ts.nodejs.snap b/packages/core/src/tests/__snapshots__/scrollbox.test.ts.nodejs.snap new file mode 100644 index 000000000..1fd204604 --- /dev/null +++ b/packages/core/src/tests/__snapshots__/scrollbox.test.ts.nodejs.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ScrollBoxRenderable - Content Visibility > scrolls CodeRenderable with LineNumberRenderable using mouse wheel 1`] = ` +" 22 Line 22 + 23 Line 23 + 24 Line 24 + 25 Line 25 + 26 Line 26 + 27 Line 27 + 28 Line 28 + 29 Line 29 + 30 Line 30 + 31 ▄ + + + + + + + + + + + + + + +" +`; From 93f2d25b6afc66c14f1999a94d606e06c1df60ff Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:18:59 -0400 Subject: [PATCH 33/56] remove wordaround for bug in nodejs type:file shim --- packages/core/src/zig.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/src/zig.ts b/packages/core/src/zig.ts index a70725f3e..d30d49177 100644 --- a/packages/core/src/zig.ts +++ b/packages/core/src/zig.ts @@ -50,10 +50,6 @@ if (isBunfsPath(targetLibPath)) { targetLibPath = targetLibPath.replace("../", "") } -if (targetLibPath.startsWith("file://")) { - targetLibPath = targetLibPath.slice(7) -} - if (!existsSync(targetLibPath)) { throw new Error( `opentui is not supported on the current platform: ${process.platform}-${process.arch}: not found: ${targetLibPath}`, From b880fad531835df77af994988beb0bab1af38cde Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:23:59 -0400 Subject: [PATCH 34/56] clean up dep incongruity --- bun.lock | 188 +++++---------------------------- package.json | 2 +- packages/core/package.json | 2 - packages/core/scripts/build.ts | 2 +- 4 files changed, 30 insertions(+), 164 deletions(-) diff --git a/bun.lock b/bun.lock index ca274f77d..7eacf0e73 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "devDependencies": { "oxfmt": "0.41.0", "oxlint": "1.56.0", - "vitest": "^4.1.3", + "vitest": "4.1.3", }, }, "packages/core": { @@ -26,8 +26,6 @@ "@types/node": "^24.0.0", "@types/three": "0.177.0", "commander": "^13.1.0", - "esbuild-register": "^3.6.0", - "tsx": "^4.21.0", "typescript": "^5", "web-tree-sitter": "0.25.10", }, @@ -203,57 +201,57 @@ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], @@ -779,9 +777,7 @@ "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -839,8 +835,6 @@ "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], - "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], - "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -1209,8 +1203,6 @@ "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], @@ -1307,8 +1299,6 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1407,12 +1397,6 @@ "@astrojs/mdx/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - "@opentui/react/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - - "@opentui/solid/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - - "@opentui/web/@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1423,8 +1407,6 @@ "astro/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - "astro/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "astro/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], @@ -1453,140 +1435,26 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "unstorage/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@opentui/react/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - - "@opentui/solid/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], - - "@opentui/web/@types/bun/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], - "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "boxen/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "widest-line/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], diff --git a/package.json b/package.json index 79ff63f6e..cfae5a295 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "devDependencies": { "oxfmt": "0.41.0", "oxlint": "1.56.0", - "vitest": "^4.1.3" + "vitest": "4.1.3" }, "trustedDependencies": [ "koffi" diff --git a/packages/core/package.json b/packages/core/package.json index e9e02d5c3..1358760a5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,8 +33,6 @@ "@types/node": "^24.0.0", "@types/three": "0.177.0", "commander": "^13.1.0", - "esbuild-register": "^3.6.0", - "tsx": "^4.21.0", "typescript": "^5", "web-tree-sitter": "0.25.10" }, diff --git a/packages/core/scripts/build.ts b/packages/core/scripts/build.ts index 603c8d47e..bb9456465 100644 --- a/packages/core/scripts/build.ts +++ b/packages/core/scripts/build.ts @@ -1,5 +1,5 @@ +import { spawnSync, type SpawnSyncReturns } from "child_process" import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs" -import { spawnSync, type SpawnSyncReturns } from "node:child_process" import path, { dirname, join, resolve } from "path" import process from "process" import { fileURLToPath } from "url" From e954987ff4a2f84126d1081bcec5cf3d655a7513 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:26:27 -0400 Subject: [PATCH 35/56] remove rando env var --- scripts/run-node.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/run-node.sh b/scripts/run-node.sh index d61a5606d..0f99076df 100755 --- a/scripts/run-node.sh +++ b/scripts/run-node.sh @@ -3,7 +3,6 @@ # Usage: ./run-node.sh src/examples/simple-layout-example.ts set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -export OPENTUI_NODE_COMPAT=1 exec node \ --experimental-transform-types \ "--import=${SCRIPT_DIR}/../packages/core/src/nodejs/compat.ts" \ From 29d8a21303a6d566521acb2c261dc12ec8fdeffe Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 15:54:26 -0400 Subject: [PATCH 36/56] plan --- NODEJS_COMPAT.md | 509 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 NODEJS_COMPAT.md diff --git a/NODEJS_COMPAT.md b/NODEJS_COMPAT.md new file mode 100644 index 000000000..5e3fa68d0 --- /dev/null +++ b/NODEJS_COMPAT.md @@ -0,0 +1,509 @@ +# Node.js Compatibility Plan + +## Summary + +Goal: make the main OpenTUI packages usable from Node.js with no user-facing preload flags such as `-r` or `--import`, and without introducing import maps. + +The core approach is: + +1. Replace Bun-specific runtime imports in portable code with project-owned compat modules. +2. Keep Bun-only features isolated behind explicit Bun-only entrypoints. +3. Publish Node-consumable build artifacts instead of relying on loader hooks at runtime. +4. Add a real Node test lane for every package that is meant to work in Node. + +This plan intentionally moves compatibility out of process bootstrapping and into normal module code. + +## Principles + +- No import maps. +- No required `-r` / `--import` flags for consumers. +- No production dependence on `packages/core/src/nodejs/compat.ts`. +- Prefer standard platform APIs over Bun shims where possible. +- Use a stable compat import surface inside the repo. +- Keep the Bun-only API surface explicit instead of partially emulated. + +## Target End State + +Node users can install and import these entrypoints directly: + +- `@opentui/core` +- `@opentui/core/testing` +- `@opentui/react` +- `@opentui/react/test-utils` +- `@opentui/solid` + +These entrypoints remain Bun-only in the first pass: + +- `@opentui/core/runtime-plugin` +- `@opentui/core/runtime-plugin-support` +- `@opentui/core/3d` +- `@opentui/react/runtime-plugin-support` +- `@opentui/solid/runtime-plugin-support` +- `@opentui/solid/preload` +- `@opentui/solid/bun-plugin` + +If a Node user imports a Bun-only entrypoint, they should get a clean, deterministic error from a stub module, not a random Bun symbol failure. + +## Current Problems + +- Portable runtime code still imports `bun:ffi`. +- Portable runtime code still calls `Bun.*`. +- Tree-sitter assets use Bun import attributes such as `with { type: "file" }`. +- The current Node path relies on `packages/core/src/nodejs/compat.ts`, which must be preloaded before any Bun-specific import is evaluated. +- Published `dist` artifacts are still Bun-oriented, so a consumer import is not the same thing as a Vitest import. + +## Main Design + +### 1. Introduce a compat surface in `packages/core/src/compat` + +Planned modules: + +- `packages/core/src/compat/ffi.ts` +- `packages/core/src/compat/Worker.ts` +- `packages/core/src/compat/runtime.ts` +- `packages/core/src/compat/resolvers.ts` +- `packages/core/src/compat/test.ts` + +Portable code will only import from these modules instead of `bun:ffi`, `bun:test`, `Bun.*`, or Bun import attributes. + +### 2. Stable facade, flexible implementation + +The import surface should be stable even if implementation differs by runtime. + +That means call sites should always import: + +```ts +import { dlopen, ptr, toArrayBuffer } from "./compat/ffi.js" +import { Worker } from "./compat/Worker.js" +import { resolveFile, readTextFile } from "./compat/resolvers.js" +import { sleep, stringWidth, stripANSI, writeFile } from "./compat/runtime.js" +``` + +The implementation behind those modules can be either: + +- a single portable file when that is straightforward, or +- a thin facade with runtime-specific internals when a single file would force Bun-only syntax back into the source. + +Important constraint: the stable import surface is required, but the implementation does not need to be a single literal file if that becomes awkward. + +## Compat Module Plan + +### `compat/ffi.ts` + +Purpose: + +- Replace every `bun:ffi` import in portable runtime code. +- Ensure generated `.d.ts` files stop referencing `bun:ffi`. + +Exports should cover the exact subset the project uses today: + +- `dlopen` +- `JSCallback` +- `ptr` +- `toArrayBuffer` +- `Pointer` +- `FFIType` +- any other Bun FFI types currently exposed in public types + +Implementation plan: + +- Reuse the logic already developed in `packages/core/src/nodejs/bunModules/ffi.ts` for Node. +- In Bun, delegate to Bun FFI. +- Own the exported types under the compat module so declaration output references `./compat/ffi` instead of `bun:ffi`. + +Files to migrate first: + +- `packages/core/src/buffer.ts` +- `packages/core/src/edit-buffer.ts` +- `packages/core/src/editor-view.ts` +- `packages/core/src/NativeSpanFeed.ts` +- `packages/core/src/renderer.ts` +- `packages/core/src/syntax-style.ts` +- `packages/core/src/text-buffer.ts` +- `packages/core/src/text-buffer-view.ts` +- `packages/core/src/zig-structs.ts` +- `packages/core/src/zig.ts` +- `packages/core/src/lib/clipboard.ts` +- `packages/core/src/3d/canvas.ts` + +### `compat/Worker.ts` + +Purpose: + +- Replace implicit reliance on global `Worker`. +- Make worker creation explicit and runtime-neutral. + +Required surface: + +- `new Worker(string | URL)` +- `onmessage` +- `onerror` +- `postMessage` +- `terminate` + +Implementation plan: + +- Bun path: use the native worker implementation. +- Node path: wrap `node:worker_threads` and preserve the web-worker style API already used by `TreeSitterClient`. + +First consumer: + +- `packages/core/src/lib/tree-sitter/client.ts` + +### `compat/runtime.ts` + +Purpose: + +- Remove remaining `Bun.*` calls from portable runtime code. + +Initial surface: + +- `sleep(ms)` +- `stringWidth(text)` +- `stripANSI(text)` +- `writeFile(path, data, options?)` +- possibly `readTextFile(path)` if useful outside resolvers + +Rules: + +- Prefer direct standard-library replacements when possible. +- Keep the compat API narrow and project-owned. +- Do not preserve a fake global `Bun` object in the long-term design. + +Expected migrations: + +- `packages/core/src/lib/paste.ts` +- `packages/core/src/lib/extmarks.ts` +- `packages/core/src/renderables/LineNumberRenderable.ts` +- `packages/core/src/renderables/ScrollBar.ts` +- `packages/core/src/renderer.ts` +- `packages/core/src/zig.ts` + +### `compat/resolvers.ts` + +Purpose: + +- Replace Bun import attributes in portable code. + +Planned API: + +```ts +const javascriptHighlights = resolveFile(import.meta.url, "./assets/javascript/highlights.scm") +const shaderTemplate = readTextFile(import.meta.url, "./shaders/supersampling.wgsl") +``` + +Suggested helpers: + +- `resolveFile(fromImportMetaUrl, relativePath): string` +- `readTextFile(fromImportMetaUrl, relativePath): string` +- optionally `resolveUrl(fromImportMetaUrl, relativePath): URL` + +Usage plan: + +- Tree-sitter asset paths should use `resolveFile(...)`. +- Shader source should use `readTextFile(...)`. +- Generated native package stubs should use direct `new URL(..., import.meta.url)` plus `fileURLToPath(...)`; they do not need Bun import attributes at all. + +Files to migrate: + +- `packages/core/src/lib/tree-sitter/default-parsers.ts` +- `packages/core/src/3d/canvas.ts` +- `packages/core/scripts/build.ts` for native package index generation +- `packages/core/src/lib/tree-sitter/assets/update.ts` so future generated files use the resolver helpers + +### `compat/test.ts` + +Purpose: + +- Provide one shared test import surface for Bun and Vitest. + +Scope: + +- Repo tests only. +- Not part of the supported runtime API. + +Exports: + +- `describe` +- `it` +- `test` +- `expect` +- `beforeEach` +- `afterEach` +- `beforeAll` +- `afterAll` +- `mock` +- `spyOn` +- shared matcher setup such as `toInclude` + +This should replace direct `bun:test` imports in packages that need a Node test lane. + +## Source Migration Plan + +### Phase 1: Core portable runtime + +1. Add `packages/core/src/compat/{ffi,Worker,runtime,resolvers}.ts`. +2. Replace all `bun:ffi` imports in portable code with `./compat/ffi.js`. +3. Replace global `Worker` usage with `./compat/Worker.js`. +4. Replace `with { type: "file" }` and `with { type: "text" }` with resolver helpers. +5. Replace remaining `Bun.*` calls in portable runtime code. +6. Regenerate any generated files that currently emit Bun-specific asset imports. +7. Ensure the main `@opentui/core` runtime path no longer depends on `packages/core/src/nodejs/compat.ts`. + +Deliverable: + +- A Node import of the portable `core` source no longer requires preload hooks to resolve Bun-specific runtime APIs. + +### Phase 2: Explicit Bun-only isolation + +Keep these entrypoints Bun-only in the first pass: + +- `packages/core/src/runtime-plugin.ts` +- `packages/core/src/runtime-plugin-support.ts` +- `packages/react/scripts/runtime-plugin-support.ts` +- `packages/solid/scripts/runtime-plugin-support.ts` +- `packages/solid/scripts/solid-plugin.ts` +- `packages/solid/scripts/preload.ts` +- `packages/core/src/3d.ts` and `packages/core/src/3d/**` unless a separate 3D Node plan is approved + +Tasks: + +- Move any Bun-only imports out of otherwise portable modules. +- Add explicit Node stubs for Bun-only published subpaths. +- Keep Bun tests for these entrypoints in the Bun lane only. + +Deliverable: + +- The portable API surface is cleanly separated from the Bun-only API surface. + +### Phase 3: Packaging and publish output + +Portable source is not enough; published `dist` must also be Node-safe. + +Tasks for `@opentui/core`: + +1. Stop publishing Bun-targeted output as the only artifact. +2. Build a Node-safe ESM artifact with no `bun:ffi`, no `Bun.*`, and no Bun import attributes. +3. Keep a Bun build only where it provides real value. +4. Use package `exports` conditions to select Node vs default artifacts where needed. +5. For native optional packages such as `@opentui/core-darwin-arm64`, generate a portable `index.js` that resolves the adjacent library path with `fileURLToPath(new URL(...))`. +6. Ensure generated declarations reference compat modules instead of `bun:ffi`. + +Tasks for `@opentui/react` and `@opentui/solid`: + +1. Rebuild against the portable `@opentui/core` output. +2. Publish Node-safe main entrypoints. +3. Publish explicit Node stubs for Bun-only subpaths. +4. Stop copying raw `.ts` Bun-only entrypoints into published `dist` when a stub or transpiled JS file is more appropriate. + +Export map policy: + +- Use package `exports`. +- Do not introduce import maps. +- Prefer `"node"` and `"default"` conditions when runtimes need different files. +- Use a single file for both runtimes when the artifact is genuinely portable. + +Deliverable: + +- `node -e 'import("@opentui/core")'` works against published output with no preload flags. + +### Phase 4: Test migration + +#### Shared test policy + +- Bun remains the primary lane for Bun-only features. +- Node gets a first-class lane for every package intended to work in Node. +- Portable tests should not import `bun:test` directly. +- Node snapshots should be separate from Bun snapshots. + +#### `@opentui/core` + +Add and maintain: + +- `test:js` for Bun source tests +- `test:nodejs` for Node source tests +- `test:nodejs:dist` for plain Node imports against built output + +Node source lane: + +- Use Vitest while the migration is in progress. +- Prefer direct source imports once portable compat modules exist. +- Keep current hook-based Node test setup only as a temporary bridge. + +Node dist lane: + +- Use plain `node` to import built entrypoints. +- No `--import`. +- No custom loader hooks. +- This lane is the release gate for Node compatibility. + +Tests that remain Bun-only: + +- runtime plugin tests requiring `import { plugin } from "bun"` +- any tests for Bun-only entrypoints +- any tests that intentionally validate Bun plugin behavior + +#### `@opentui/react` + +Tasks: + +1. Add `packages/react/vitest.config.ts`. +2. Replace `bun:test` imports with a shared test compat import. +3. Add Node snapshots with a `.nodejs.snap` suffix. +4. Add `test:nodejs`. +5. Add `test:nodejs:dist` smoke coverage for the published package. + +Expected Bun-only exclusions: + +- `packages/react/tests/runtime-plugin-support.test.ts` + +#### `@opentui/solid` + +Tasks: + +1. Add `packages/solid/vitest.config.ts`. +2. Replace `bun:test` imports with a shared test compat import. +3. Add Node snapshots with a `.nodejs.snap` suffix. +4. Add `test:nodejs`. +5. Add `test:nodejs:dist` smoke coverage for the published package. + +Important requirement: + +- The Solid Node test lane must use a transform equivalent to the current Solid Bun plugin behavior. +- Reuse the current Babel-based logic where possible so Node and Bun produce the same JSX transform semantics. + +Expected Bun-only exclusions: + +- `packages/solid/tests/runtime-plugin-support*.test.ts` +- `packages/solid/tests/solid-plugin.test.ts` +- any tests whose fixtures import `plugin` from `bun` + +#### Root scripts + +Planned root-level scripts: + +- `test:bun` +- `test:nodejs` +- `test:nodejs:dist` +- `test` + +Recommended policy: + +- `test` should run the Bun lane plus the Node source lane. +- CI release jobs should also run `test:nodejs:dist`. + +### Phase 5: Cleanup + +Once the portable runtime no longer depends on loader hooks: + +1. Remove or retire `packages/core/src/nodejs/compat.ts`. +2. Remove or retire `packages/core/src/nodejs/bunModules/test.ts`. +3. Remove any Vitest config that exists only to inject the Node preload hook. +4. Remove build or test comments that describe the old preload-based path as the supported solution. + +## Package-by-Package Scope + +### `packages/core` + +Required in first pass: + +- portable main entrypoint +- portable `testing` entrypoint +- portable native library path resolution +- Node-safe declarations +- Node source tests +- Node dist smoke tests + +Deferred: + +- `runtime-plugin` +- `runtime-plugin-support` +- `3d` + +### `packages/react` + +Required in first pass: + +- portable main entrypoint +- portable `test-utils` +- Node tests +- Node dist smoke tests + +Deferred: + +- `runtime-plugin-support` + +### `packages/solid` + +Required in first pass: + +- portable main entrypoint +- Node tests +- Node dist smoke tests + +Deferred: + +- `runtime-plugin-support` +- `bun-plugin` +- `preload` + +### `packages/web` + +No dedicated runtime compatibility work is required for the initial port beyond consuming the portable `core` package correctly. + +## Build Strategy + +Recommended build policy: + +1. Make the runtime code portable first. +2. Then simplify builds where portability makes a separate Bun build unnecessary. +3. Only keep separate Bun and Node output trees where the implementation truly differs. + +Suggested artifact layout if split output is still needed: + +- `dist/node/**` +- `dist/bun/**` + +Suggested artifact layout if most code becomes portable: + +- one shared portable output tree +- separate stubs only for Bun-only subpaths + +The exact output layout is less important than this invariant: + +- published Node entrypoints must work when imported by plain `node` + +## Acceptance Criteria + +The Node port is complete when all of the following are true: + +1. `@opentui/core`, `@opentui/core/testing`, `@opentui/react`, and `@opentui/solid` import cleanly in plain Node with no preload flags. +2. Portable source files no longer import `bun:ffi`, `bun:test`, or use Bun import attributes. +3. Portable runtime code no longer depends on a global `Bun` object. +4. Published declaration files for portable entrypoints no longer reference `bun:ffi`. +5. Bun-only subpaths fail cleanly in Node with an explicit message. +6. Each supported package has a working Node test lane. +7. Each supported package has a Node dist smoke test. + +## Implementation Order + +Recommended order: + +1. Add compat modules in `packages/core/src/compat`. +2. Migrate `@opentui/core` portable runtime code. +3. Regenerate tree-sitter asset resolver output. +4. Fix native package stubs. +5. Make `@opentui/core` dist Node-safe. +6. Add `@opentui/core` Node dist smoke tests. +7. Add `@opentui/react` Node test lane. +8. Add `@opentui/solid` Node test lane. +9. Make `react` and `solid` dist Node-safe. +10. Add root Node test scripts and CI lanes. +11. Remove the old preload-hook path. + +## Notes + +- This plan does not require import maps. +- This plan does not require users to change docs to mention `-r` or `--import`. +- This plan assumes the stable compat import surface lives in `packages/core/src/compat`. +- `packages/core/src/compat/test.ts` is useful for repo tests, but it should not be treated as part of the supported runtime API unless that becomes an explicit product decision. From 4be74ec690976811ee1bacdde5e75d66a2bb6451 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 20:33:58 -0400 Subject: [PATCH 37/56] replace runtime bun shims w/ static shims --- packages/core/package.json | 2 +- packages/core/scripts/build.ts | 8 +- packages/core/src/3d/canvas.ts | 6 +- packages/core/src/NativeSpanFeed.ts | 2 +- .../native-span-feed-async-benchmark.ts | 2 +- .../benchmark/native-span-feed-benchmark.ts | 2 +- packages/core/src/buffer.ts | 2 +- packages/core/src/compat/FFIType.ts | 323 +++++ packages/core/src/compat/Worker.ts | 1 + .../src/compat/__snapshots__/test.ts.snap | 28 + .../src/compat/bun-ffi-structs/error.d.ts | 1 + .../src/compat/bun-ffi-structs/index.d.ts | 2 + .../core/src/compat/bun-ffi-structs/index.js | 644 +++++++++ .../compat/bun-ffi-structs/structs_ffi.d.ts | 31 + .../src/compat/bun-ffi-structs/types.d.ts | 122 ++ packages/core/src/compat/ffi.ts | 174 +++ packages/core/src/compat/nodejs/Worker.ts | 77 ++ .../bunModules => compat/nodejs}/ffi.ts | 377 +----- .../core/src/compat/nodejs/registerBun.ts | 26 + .../src/compat/nodejs/registerResolveJs.ts | 36 + packages/core/src/compat/nodejs/runtime.ts | 37 + .../bunModules => compat/nodejs}/test.ts | 0 .../src/compat/nodejs/trampoline.worker.ts | 29 + packages/core/src/compat/runtime.ts | 25 + packages/core/src/compat/test.ts | 1 + packages/core/src/compat/testHelpers.ts | 47 + packages/core/src/edit-buffer.test.ts | 67 +- packages/core/src/edit-buffer.ts | 2 +- packages/core/src/editor-view.ts | 2 +- packages/core/src/lib/clipboard.ts | 2 +- .../core/src/lib/extmarks-multiwidth.test.ts | 5 +- packages/core/src/lib/extmarks.ts | 5 +- packages/core/src/lib/paste.ts | 4 +- .../core/src/lib/tree-sitter/assets/update.ts | 25 +- .../core/src/lib/tree-sitter/cache.test.ts | 2 +- packages/core/src/lib/tree-sitter/client.ts | 3 +- .../src/lib/tree-sitter/default-parsers.ts | 47 +- .../core/src/lib/tree-sitter/parser.worker.ts | 21 +- packages/core/src/nodejs/NodeBun.ts | 169 --- packages/core/src/nodejs/bunModules/bun.ts | 1 - packages/core/src/nodejs/compat.ts | 136 -- packages/core/src/renderables/Code.test.ts | 9 +- .../src/renderables/LineNumberRenderable.ts | 7 +- packages/core/src/renderables/ScrollBar.ts | 3 +- .../__tests__/LineNumberRenderable.test.ts | 11 +- .../__tests__/Markdown.code-colors.test.ts | 13 +- .../renderables/__tests__/Markdown.test.ts | 13 +- .../__tests__/Textarea.buffer.test.ts | 7 +- .../__tests__/Textarea.selection.test.ts | 3 +- packages/core/src/renderer.ts | 5 +- packages/core/src/runtime-plugin.ts | 8 +- packages/core/src/syntax-style.ts | 2 +- .../core/src/testing/test-recorder.test.ts | 47 +- .../core/src/tests/destroy-on-exit.test.ts | 6 +- .../core/src/tests/renderer.clock.test.ts | 3 +- .../core/src/tests/renderer.control.test.ts | 5 +- .../core/src/tests/renderer.mouse.test.ts | 7 +- .../src/tests/runtime-plugin-support.test.ts | 3 +- .../core/src/tests/runtime-plugin.test.ts | 21 +- packages/core/src/text-buffer-view.ts | 2 +- packages/core/src/text-buffer.ts | 2 +- packages/core/src/zig-structs.ts | 4 +- packages/core/src/zig.ts | 7 +- packages/core/vitest.config.ts | 33 +- .../tests/__snapshots__/layout.test.tsx.snap | 1160 +++++++++++++++++ .../tests/runtime-plugin-support.fixture.ts | 4 +- scripts/run-node.sh | 5 +- 67 files changed, 3012 insertions(+), 874 deletions(-) create mode 100644 packages/core/src/compat/FFIType.ts create mode 100644 packages/core/src/compat/Worker.ts create mode 100644 packages/core/src/compat/__snapshots__/test.ts.snap create mode 100644 packages/core/src/compat/bun-ffi-structs/error.d.ts create mode 100644 packages/core/src/compat/bun-ffi-structs/index.d.ts create mode 100644 packages/core/src/compat/bun-ffi-structs/index.js create mode 100644 packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts create mode 100644 packages/core/src/compat/bun-ffi-structs/types.d.ts create mode 100644 packages/core/src/compat/ffi.ts create mode 100644 packages/core/src/compat/nodejs/Worker.ts rename packages/core/src/{nodejs/bunModules => compat/nodejs}/ffi.ts (66%) create mode 100644 packages/core/src/compat/nodejs/registerBun.ts create mode 100644 packages/core/src/compat/nodejs/registerResolveJs.ts create mode 100644 packages/core/src/compat/nodejs/runtime.ts rename packages/core/src/{nodejs/bunModules => compat/nodejs}/test.ts (100%) create mode 100644 packages/core/src/compat/nodejs/trampoline.worker.ts create mode 100644 packages/core/src/compat/runtime.ts create mode 100644 packages/core/src/compat/test.ts create mode 100644 packages/core/src/compat/testHelpers.ts delete mode 100644 packages/core/src/nodejs/NodeBun.ts delete mode 100644 packages/core/src/nodejs/bunModules/bun.ts delete mode 100644 packages/core/src/nodejs/compat.ts diff --git a/packages/core/package.json b/packages/core/package.json index 1358760a5..184e8757f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,7 +24,7 @@ "bench:ts": "bun src/benchmark/native-span-feed-benchmark.ts --suite=quick --json=src/benchmark/latest-quick-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=default --json=src/benchmark/latest-default-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=large --json=src/benchmark/latest-large-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=all --json=src/benchmark/latest-all-bench-run.json && bun src/benchmark/native-span-feed-async-benchmark.ts --json=src/benchmark/latest-async-bench-run.json", "publish": "bun scripts/publish.ts", "test:js": "bun test", - "test:nodejs": "npx vitest", + "test:nodejs": "npx vitest run", "test": "bun run test:native && bun run test:js && bun run test:nodejs" }, "license": "MIT", diff --git a/packages/core/scripts/build.ts b/packages/core/scripts/build.ts index bb9456465..5b0b37506 100644 --- a/packages/core/scripts/build.ts +++ b/packages/core/scripts/build.ts @@ -135,9 +135,11 @@ if (buildNative) { continue } - const indexJsContent = `const module = await import("./${libraryFileName}", { with: { type: "file" } }) -const path = module.default -export default path; + const indexJsContent = `import { fileURLToPath } from "node:url" + +const path = fileURLToPath(new URL("./${libraryFileName}", import.meta.url)) + +export default path ` const indexDtsContent = `declare const path: string export default path;` diff --git a/packages/core/src/3d/canvas.ts b/packages/core/src/3d/canvas.ts index be1507d99..ce862c510 100644 --- a/packages/core/src/3d/canvas.ts +++ b/packages/core/src/3d/canvas.ts @@ -1,12 +1,12 @@ +import { readFileSync } from "node:fs" import { GPUCanvasContextMock } from "bun-webgpu" import { RGBA } from "../lib/RGBA.js" import { SuperSampleType } from "./WGPURenderer.js" import type { OptimizedBuffer } from "../buffer.js" -import { toArrayBuffer } from "bun:ffi" +import { toArrayBuffer } from "../compat/ffi.js" import { Jimp } from "jimp" -// @ts-ignore -import shaderTemplate from "./shaders/supersampling.wgsl" with { type: "text" } +const shaderTemplate = readFileSync(new URL("./shaders/supersampling.wgsl", import.meta.url), "utf8") const WORKGROUP_SIZE = 4 const SUPERSAMPLING_COMPUTE_SHADER = shaderTemplate.replace(/\${WORKGROUP_SIZE}/g, WORKGROUP_SIZE.toString()) diff --git a/packages/core/src/NativeSpanFeed.ts b/packages/core/src/NativeSpanFeed.ts index 906083825..e1f3d8e50 100644 --- a/packages/core/src/NativeSpanFeed.ts +++ b/packages/core/src/NativeSpanFeed.ts @@ -1,4 +1,4 @@ -import { toArrayBuffer, type Pointer } from "bun:ffi" +import { toArrayBuffer, type Pointer } from "./compat/ffi.js" import { resolveRenderLib } from "./zig.js" import { SpanInfoStruct } from "./zig-structs.js" import type { GrowthPolicy, NativeSpanFeedOptions, NativeSpanFeedStats } from "./zig-structs.js" diff --git a/packages/core/src/benchmark/native-span-feed-async-benchmark.ts b/packages/core/src/benchmark/native-span-feed-async-benchmark.ts index 6d711cb8e..836b6a36b 100644 --- a/packages/core/src/benchmark/native-span-feed-async-benchmark.ts +++ b/packages/core/src/benchmark/native-span-feed-async-benchmark.ts @@ -1,4 +1,4 @@ -import { dlopen, FFIType, suffix } from "bun:ffi" +import { dlopen, FFIType, suffix } from "../compat/ffi.js" import { setRenderLibPath } from "../zig.js" if (!process.env.NATIVE_SPAN_FEED_LIB) { diff --git a/packages/core/src/benchmark/native-span-feed-benchmark.ts b/packages/core/src/benchmark/native-span-feed-benchmark.ts index 1004ddb59..b0acd425b 100644 --- a/packages/core/src/benchmark/native-span-feed-benchmark.ts +++ b/packages/core/src/benchmark/native-span-feed-benchmark.ts @@ -1,4 +1,4 @@ -import { dlopen, FFIType, suffix } from "bun:ffi" +import { dlopen, FFIType, suffix } from "../compat/ffi.js" import { setRenderLibPath } from "../zig.js" if (!process.env.NATIVE_SPAN_FEED_LIB) { diff --git a/packages/core/src/buffer.ts b/packages/core/src/buffer.ts index ea32b45dc..16c05a9c4 100644 --- a/packages/core/src/buffer.ts +++ b/packages/core/src/buffer.ts @@ -1,6 +1,6 @@ import { RGBA } from "./lib/index.js" import { resolveRenderLib, type RenderLib } from "./zig.js" -import { type Pointer, toArrayBuffer, ptr } from "bun:ffi" +import { type Pointer, ptr, toArrayBuffer } from "./compat/ffi.js" import { type BorderStyle, type BorderSides, BorderCharArrays, parseBorderStyle } from "./lib/index.js" import { TargetChannel, type WidthMethod, type CapturedSpan, type CapturedLine } from "./types.js" import type { TextBufferView } from "./text-buffer-view.js" diff --git a/packages/core/src/compat/FFIType.ts b/packages/core/src/compat/FFIType.ts new file mode 100644 index 000000000..24f4fb745 --- /dev/null +++ b/packages/core/src/compat/FFIType.ts @@ -0,0 +1,323 @@ +/** Copy of bun:ffi#FFIType */ +export enum FFIType { + char = 0, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int8_t = 1, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i8 = 1, + + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint8_t = 2, + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u8 = 2, + + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int16_t = 3, + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i16 = 3, + + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint16_t = 4, + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u16 = 4, + + /** + * 32-bit signed integer + */ + int32_t = 5, + + /** + * 32-bit signed integer + * + * Alias of {@link FFIType.int32_t} + */ + i32 = 5, + /** + * 32-bit signed integer + * + * The same as `int` in C + * + * ```c + * int + * ``` + */ + int = 5, + + /** + * 32-bit unsigned integer + * + * The same as `unsigned int` in C (on x64 & arm64) + * + * C: + * ```c + * unsigned int + * ``` + * JavaScript: + * ```js + * ptr(new Uint32Array(1)) + * ``` + */ + uint32_t = 6, + /** + * 32-bit unsigned integer + * + * Alias of {@link FFIType.uint32_t} + */ + u32 = 6, + + /** + * int64 is a 64-bit signed integer + */ + int64_t = 7, + /** + * i64 is a 64-bit signed integer + */ + i64 = 7, + + /** + * 64-bit unsigned integer + */ + uint64_t = 8, + /** + * 64-bit unsigned integer + */ + u64 = 8, + + /** + * IEEE-754 double precision float + */ + double = 9, + + /** + * Alias of {@link FFIType.double} + */ + f64 = 9, + + /** + * IEEE-754 single precision float + */ + float = 10, + + /** + * Alias of {@link FFIType.float} + */ + f32 = 10, + + /** + * Boolean value + * + * Must be `true` or `false`. `0` and `1` type coercion is not supported. + * + * In C, this corresponds to: + * ```c + * bool + * _Bool + * ``` + */ + bool = 11, + + /** + * Pointer value + * + * See {@link Bun.FFI.ptr} for more information + * + * In C: + * ```c + * void* + * ``` + * + * In JavaScript: + * ```js + * ptr(new Uint8Array(1)) + * ``` + */ + ptr = 12, + /** + * Pointer value + * + * alias of {@link FFIType.ptr} + */ + pointer = 12, + + /** + * void value + * + * void arguments are not supported + * + * void return type is the default return type + * + * In C: + * ```c + * void + * ``` + */ + void = 13, + + /** + * When used as a `returns`, this will automatically become a {@link CString}. + * + * When used in `args` it is equivalent to {@link FFIType.pointer} + */ + cstring = 14, + + /** + * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance + * but means you might get a `BigInt` or you might get a `number`. + * + * In C, this always becomes `int64_t` + * + * In JavaScript, this could be number or it could be BigInt, depending on what + * value is passed in. + */ + i64_fast = 15, + + /** + * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance + * but means you might get a `BigInt` or you might get a `number`. + * + * In C, this always becomes `uint64_t` + * + * In JavaScript, this could be number or it could be BigInt, depending on what + * value is passed in. + */ + u64_fast = 16, + function = 17, + + napi_env = 18, + napi_value = 19, + buffer = 20, +} diff --git a/packages/core/src/compat/Worker.ts b/packages/core/src/compat/Worker.ts new file mode 100644 index 000000000..3fc6228f9 --- /dev/null +++ b/packages/core/src/compat/Worker.ts @@ -0,0 +1 @@ +export const Worker = (globalThis.Worker ?? (await import("./nodejs/Worker.js")).Worker) as typeof globalThis.Worker diff --git a/packages/core/src/compat/__snapshots__/test.ts.snap b/packages/core/src/compat/__snapshots__/test.ts.snap new file mode 100644 index 000000000..091ddbfd3 --- /dev/null +++ b/packages/core/src/compat/__snapshots__/test.ts.snap @@ -0,0 +1,28 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`OptimizedBuffer snapshot tests with unicode encoding should render ASCII text correctly: ASCII text rendering 1`] = ` +"Hello + + + + +" +`; + +exports[`OptimizedBuffer snapshot tests with unicode encoding should render emoji text correctly: Emoji text rendering 1`] = ` +"Hi 👋 🌍 + + + + +" +`; + +exports[`OptimizedBuffer snapshot tests with unicode encoding should handle multiline text with unicode: Multiline unicode rendering 1`] = ` +"Hi 世界 +🌟 Star + + + +" +`; diff --git a/packages/core/src/compat/bun-ffi-structs/error.d.ts b/packages/core/src/compat/bun-ffi-structs/error.d.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs/error.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/core/src/compat/bun-ffi-structs/index.d.ts b/packages/core/src/compat/bun-ffi-structs/index.d.ts new file mode 100644 index 000000000..e0a154ba5 --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs/index.d.ts @@ -0,0 +1,2 @@ +export * from "./structs_ffi"; +export * from "./types"; diff --git a/packages/core/src/compat/bun-ffi-structs/index.js b/packages/core/src/compat/bun-ffi-structs/index.js new file mode 100644 index 000000000..bcd1cca9e --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs/index.js @@ -0,0 +1,644 @@ +// src/structs_ffi.ts +import { ptr, toArrayBuffer } from "../ffi.js"; +function fatalError(...args) { + const message = args.join(" "); + console.error("FATAL ERROR:", message); + throw new Error(message); +} +var pointerSize = process.arch === "x64" || process.arch === "arm64" ? 8 : 4; +var typeSizes = { + u8: 1, + bool_u8: 1, + bool_u32: 4, + u16: 2, + i16: 2, + u32: 4, + u64: 8, + f32: 4, + f64: 8, + pointer: pointerSize, + i32: 4 +}; +var primitiveKeys = Object.keys(typeSizes); +function isPrimitiveType(type) { + return typeof type === "string" && primitiveKeys.includes(type); +} +var typeAlignments = { ...typeSizes }; +var typeGetters = { + u8: (view, offset) => view.getUint8(offset), + bool_u8: (view, offset) => Boolean(view.getUint8(offset)), + bool_u32: (view, offset) => Boolean(view.getUint32(offset, true)), + u16: (view, offset) => view.getUint16(offset, true), + i16: (view, offset) => view.getInt16(offset, true), + u32: (view, offset) => view.getUint32(offset, true), + u64: (view, offset) => view.getBigUint64(offset, true), + f32: (view, offset) => view.getFloat32(offset, true), + f64: (view, offset) => view.getFloat64(offset, true), + i32: (view, offset) => view.getInt32(offset, true), + pointer: (view, offset) => pointerSize === 8 ? view.getBigUint64(offset, true) : BigInt(view.getUint32(offset, true)) +}; +function objectPtr() { + return { + __type: "objectPointer" + }; +} +function isObjectPointerDef(type) { + return typeof type === "object" && type !== null && type.__type === "objectPointer"; +} +function allocStruct(structDef, options) { + const buffer = new ArrayBuffer(structDef.size); + const view = new DataView(buffer); + const result = { buffer, view }; + const { pack: pointerPacker } = primitivePackers("pointer"); + if (options?.lengths) { + const subBuffers = {}; + for (const [arrayFieldName, length] of Object.entries(options.lengths)) { + const arrayMeta = structDef.arrayFields.get(arrayFieldName); + if (!arrayMeta) { + throw new Error(`Field '${arrayFieldName}' is not an array field with a lengthOf field`); + } + const subBuffer = new ArrayBuffer(length * arrayMeta.elementSize); + subBuffers[arrayFieldName] = subBuffer; + const pointer = length > 0 ? ptr(subBuffer) : null; + pointerPacker(view, arrayMeta.arrayOffset, pointer); + arrayMeta.lengthPack(view, arrayMeta.lengthOffset, length); + } + if (Object.keys(subBuffers).length > 0) { + result.subBuffers = subBuffers; + } + } + return result; +} +function alignOffset(offset, align) { + return offset + (align - 1) & ~(align - 1); +} +function enumTypeError(value) { + throw new TypeError(`Invalid enum value: ${value}`); +} +function defineEnum(mapping, base = "u32") { + const reverse = Object.fromEntries(Object.entries(mapping).map(([k, v]) => [v, k])); + return { + __type: "enum", + type: base, + to(value) { + return typeof value === "number" ? value : mapping[value] ?? enumTypeError(String(value)); + }, + from(value) { + return reverse[value] ?? enumTypeError(String(value)); + }, + enum: mapping + }; +} +function isEnum(type) { + return typeof type === "object" && type.__type === "enum"; +} +function isStruct(type) { + return typeof type === "object" && type.__type === "struct"; +} +function primitivePackers(type) { + let pack; + let unpack; + switch (type) { + case "u8": + pack = (view, off, val) => view.setUint8(off, val); + unpack = (view, off) => view.getUint8(off); + break; + case "bool_u8": + pack = (view, off, val) => view.setUint8(off, val ? 1 : 0); + unpack = (view, off) => Boolean(view.getUint8(off)); + break; + case "bool_u32": + pack = (view, off, val) => view.setUint32(off, val ? 1 : 0, true); + unpack = (view, off) => Boolean(view.getUint32(off, true)); + break; + case "u16": + pack = (view, off, val) => view.setUint16(off, val, true); + unpack = (view, off) => view.getUint16(off, true); + break; + case "i16": + pack = (view, off, val) => view.setInt16(off, val, true); + unpack = (view, off) => view.getInt16(off, true); + break; + case "u32": + pack = (view, off, val) => view.setUint32(off, val, true); + unpack = (view, off) => view.getUint32(off, true); + break; + case "i32": + pack = (view, off, val) => view.setInt32(off, val, true); + unpack = (view, off) => view.getInt32(off, true); + break; + case "u64": + pack = (view, off, val) => view.setBigUint64(off, BigInt(val), true); + unpack = (view, off) => view.getBigUint64(off, true); + break; + case "f32": + pack = (view, off, val) => view.setFloat32(off, val, true); + unpack = (view, off) => view.getFloat32(off, true); + break; + case "f64": + pack = (view, off, val) => view.setFloat64(off, val, true); + unpack = (view, off) => view.getFloat64(off, true); + break; + case "pointer": + pack = (view, off, val) => { + pointerSize === 8 ? view.setBigUint64(off, val ? BigInt(val) : 0n, true) : view.setUint32(off, val ? Number(val) : 0, true); + }; + unpack = (view, off) => { + const bint = pointerSize === 8 ? view.getBigUint64(off, true) : BigInt(view.getUint32(off, true)); + return Number(bint); + }; + break; + default: + fatalError(`Unsupported primitive type: ${type}`); + } + return { pack, unpack }; +} +var { pack: pointerPacker, unpack: pointerUnpacker } = primitivePackers("pointer"); +function packObjectArray(val) { + const buffer = new ArrayBuffer(val.length * pointerSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < val.length; i++) { + const instance = val[i]; + const ptrValue = instance?.ptr ?? null; + pointerPacker(bufferView, i * pointerSize, ptrValue); + } + return bufferView; +} +var encoder = new TextEncoder; +var decoder = new TextDecoder; +function defineStruct(fields, structDefOptions) { + let offset = 0; + let maxAlign = 1; + const layout = []; + const lengthOfFields = {}; + const lengthOfRequested = []; + const arrayFieldsMetadata = {}; + for (const [name, typeOrStruct, options = {}] of fields) { + if (options.condition && !options.condition()) { + continue; + } + let size = 0, align = 0; + let pack; + let unpack; + let needsLengthOf = false; + let lengthOfDef = null; + if (isPrimitiveType(typeOrStruct)) { + size = typeSizes[typeOrStruct]; + align = typeAlignments[typeOrStruct]; + ({ pack, unpack } = primitivePackers(typeOrStruct)); + } else if (typeof typeOrStruct === "string" && typeOrStruct === "cstring") { + size = pointerSize; + align = pointerSize; + pack = (view, off, val) => { + const bufPtr = val ? ptr(encoder.encode(val + "\x00")) : null; + pointerPacker(view, off, bufPtr); + }; + unpack = (view, off) => { + const ptrVal = pointerUnpacker(view, off); + return ptrVal; + }; + } else if (typeof typeOrStruct === "string" && typeOrStruct === "char*") { + size = pointerSize; + align = pointerSize; + pack = (view, off, val) => { + const bufPtr = val ? ptr(encoder.encode(val)) : null; + pointerPacker(view, off, bufPtr); + }; + unpack = (view, off) => { + const ptrVal = pointerUnpacker(view, off); + return ptrVal; + }; + needsLengthOf = true; + } else if (isEnum(typeOrStruct)) { + const base = typeOrStruct.type; + size = typeSizes[base]; + align = typeAlignments[base]; + const { pack: packEnum } = primitivePackers(base); + pack = (view, off, val) => { + const num = typeOrStruct.to(val); + packEnum(view, off, num); + }; + unpack = (view, off) => { + const raw = typeGetters[base](view, off); + return typeOrStruct.from(raw); + }; + } else if (isStruct(typeOrStruct)) { + if (options.asPointer === true) { + size = pointerSize; + align = pointerSize; + pack = (view, off, val, obj, options2) => { + if (!val) { + pointerPacker(view, off, null); + return; + } + const nestedBuf = typeOrStruct.pack(val, options2); + pointerPacker(view, off, ptr(nestedBuf)); + }; + unpack = (view, off) => { + throw new Error("Not implemented yet"); + }; + } else { + size = typeOrStruct.size; + align = typeOrStruct.align; + pack = (view, off, val, obj, options2) => { + const nestedBuf = typeOrStruct.pack(val, options2); + const nestedView = new Uint8Array(nestedBuf); + const dView = new Uint8Array(view.buffer); + dView.set(nestedView, off); + }; + unpack = (view, off) => { + const slice = view.buffer.slice(off, off + size); + return typeOrStruct.unpack(slice); + }; + } + } else if (isObjectPointerDef(typeOrStruct)) { + size = pointerSize; + align = pointerSize; + pack = (view, off, value) => { + const ptrValue = value?.ptr ?? null; + if (ptrValue === undefined) { + console.warn(`Field '${name}' expected object with '.ptr' property, but got undefined pointer value from:`, value); + pointerPacker(view, off, null); + } else { + pointerPacker(view, off, ptrValue); + } + }; + unpack = (view, off) => { + return pointerUnpacker(view, off); + }; + } else if (Array.isArray(typeOrStruct) && typeOrStruct.length === 1 && typeOrStruct[0] !== undefined) { + const [def] = typeOrStruct; + size = pointerSize; + align = pointerSize; + let arrayElementSize; + if (isEnum(def)) { + arrayElementSize = typeSizes[def.type]; + pack = (view, off, val, obj) => { + if (!val || val.length === 0) { + pointerPacker(view, off, null); + return; + } + const buffer = new ArrayBuffer(val.length * arrayElementSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < val.length; i++) { + const num = def.to(val[i]); + bufferView.setUint32(i * arrayElementSize, num, true); + } + pointerPacker(view, off, ptr(buffer)); + }; + unpack = null; + needsLengthOf = true; + lengthOfDef = def; + } else if (isStruct(def)) { + arrayElementSize = def.size; + pack = (view, off, val, obj, options2) => { + if (!val || val.length === 0) { + pointerPacker(view, off, null); + return; + } + const buffer = new ArrayBuffer(val.length * arrayElementSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < val.length; i++) { + def.packInto(val[i], bufferView, i * arrayElementSize, options2); + } + pointerPacker(view, off, ptr(buffer)); + }; + unpack = (view, off) => { + throw new Error("Not implemented yet"); + }; + } else if (isPrimitiveType(def)) { + arrayElementSize = typeSizes[def]; + const { pack: primitivePack } = primitivePackers(def); + pack = (view, off, val) => { + if (!val || val.length === 0) { + pointerPacker(view, off, null); + return; + } + const buffer = new ArrayBuffer(val.length * arrayElementSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < val.length; i++) { + primitivePack(bufferView, i * arrayElementSize, val[i]); + } + pointerPacker(view, off, ptr(buffer)); + }; + unpack = null; + needsLengthOf = true; + lengthOfDef = def; + } else if (isObjectPointerDef(def)) { + arrayElementSize = pointerSize; + pack = (view, off, val) => { + if (!val || val.length === 0) { + pointerPacker(view, off, null); + return; + } + const packedView = packObjectArray(val); + pointerPacker(view, off, ptr(packedView.buffer)); + }; + unpack = () => { + throw new Error("not implemented yet"); + }; + } else { + throw new Error(`Unsupported array element type for ${name}: ${JSON.stringify(def)}`); + } + const lengthOfField = Object.values(lengthOfFields).find((f) => f.lengthOf === name); + if (lengthOfField && isPrimitiveType(lengthOfField.type)) { + const { pack: lengthPack } = primitivePackers(lengthOfField.type); + arrayFieldsMetadata[name] = { + elementSize: arrayElementSize, + arrayOffset: offset, + lengthOffset: lengthOfField.offset, + lengthPack + }; + } + } else { + throw new Error(`Unsupported field type for ${name}: ${JSON.stringify(typeOrStruct)}`); + } + offset = alignOffset(offset, align); + if (options.unpackTransform) { + const originalUnpack = unpack; + unpack = (view, off) => options.unpackTransform(originalUnpack(view, off)); + } + if (options.packTransform) { + const originalPack = pack; + pack = (view, off, val, obj, packOptions) => originalPack(view, off, options.packTransform(val), obj, packOptions); + } + if (options.optional) { + const originalPack = pack; + if (isStruct(typeOrStruct) && !options.asPointer) { + pack = (view, off, val, obj, packOptions) => { + if (val || options.mapOptionalInline) { + originalPack(view, off, val, obj, packOptions); + } + }; + } else { + pack = (view, off, val, obj, packOptions) => originalPack(view, off, val ?? 0, obj, packOptions); + } + } + if (options.lengthOf) { + const originalPack = pack; + pack = (view, off, val, obj, packOptions) => { + const targetValue = obj[options.lengthOf]; + let length = 0; + if (targetValue) { + if (typeof targetValue === "string") { + length = Buffer.byteLength(targetValue); + } else { + length = targetValue.length; + } + } + return originalPack(view, off, length, obj, packOptions); + }; + } + let validateFunctions; + if (options.validate) { + validateFunctions = Array.isArray(options.validate) ? options.validate : [options.validate]; + } + const layoutField = { + name, + offset, + size, + align, + validate: validateFunctions, + optional: !!options.optional || !!options.lengthOf || options.default !== undefined, + default: options.default, + pack, + unpack, + type: typeOrStruct, + lengthOf: options.lengthOf + }; + layout.push(layoutField); + if (options.lengthOf) { + lengthOfFields[options.lengthOf] = layoutField; + } + if (needsLengthOf) { + const def = typeof typeOrStruct === "string" && typeOrStruct === "char*" ? "char*" : lengthOfDef; + if (!def) + fatalError(`Internal error: needsLengthOf=true but def is null for ${name}`); + lengthOfRequested.push({ requester: layoutField, def }); + } + offset += size; + maxAlign = Math.max(maxAlign, align); + } + for (const { requester, def } of lengthOfRequested) { + const lengthOfField = lengthOfFields[requester.name]; + if (!lengthOfField) { + if (def === "char*") { + continue; + } + throw new Error(`lengthOf field not found for array field ${requester.name}`); + } + if (def === "char*") { + requester.unpack = (view, off) => { + const ptrAddress = pointerUnpacker(view, off); + const length = lengthOfField.unpack(view, lengthOfField.offset); + if (ptrAddress === 0) { + return null; + } + const byteLength = typeof length === "bigint" ? Number(length) : length; + if (byteLength === 0) { + return ""; + } + const buffer = toArrayBuffer(ptrAddress, 0, byteLength); + return decoder.decode(buffer); + }; + } else if (isPrimitiveType(def)) { + const elemSize = typeSizes[def]; + const { unpack: primitiveUnpack } = primitivePackers(def); + requester.unpack = (view, off) => { + const result = []; + const length = lengthOfField.unpack(view, lengthOfField.offset); + const ptrAddress = pointerUnpacker(view, off); + if (ptrAddress === 0n && length > 0) { + throw new Error(`Array field ${requester.name} has null pointer but length ${length}.`); + } + if (ptrAddress === 0n || length === 0) { + return []; + } + const buffer = toArrayBuffer(ptrAddress, 0, length * elemSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < length; i++) { + result.push(primitiveUnpack(bufferView, i * elemSize)); + } + return result; + }; + } else { + const elemSize = def.type === "u32" ? 4 : 8; + requester.unpack = (view, off) => { + const result = []; + const length = lengthOfField.unpack(view, lengthOfField.offset); + const ptrAddress = pointerUnpacker(view, off); + if (ptrAddress === 0n && length > 0) { + throw new Error(`Array field ${requester.name} has null pointer but length ${length}.`); + } + if (ptrAddress === 0n || length === 0) { + return []; + } + const buffer = toArrayBuffer(ptrAddress, 0, length * elemSize); + const bufferView = new DataView(buffer); + for (let i = 0;i < length; i++) { + result.push(def.from(bufferView.getUint32(i * elemSize, true))); + } + return result; + }; + } + } + const totalSize = alignOffset(offset, maxAlign); + const description = layout.map((f) => ({ + name: f.name, + offset: f.offset, + size: f.size, + align: f.align, + optional: f.optional, + type: f.type, + lengthOf: f.lengthOf + })); + const layoutByName = new Map(description.map((f) => [f.name, f])); + const arrayFields = new Map(Object.entries(arrayFieldsMetadata)); + return { + __type: "struct", + size: totalSize, + align: maxAlign, + hasMapValue: !!structDefOptions?.mapValue, + layoutByName, + arrayFields, + pack(obj, options) { + const buf = new ArrayBuffer(totalSize); + const view = new DataView(buf); + let mappedObj = obj; + if (structDefOptions?.mapValue) { + mappedObj = structDefOptions.mapValue(obj); + } + for (const field of layout) { + const value = mappedObj[field.name] ?? field.default; + if (!field.optional && value === undefined) { + fatalError(`Packing non-optional field '${field.name}' but value is undefined (and no default provided)`); + } + if (field.validate) { + for (const validateFn of field.validate) { + validateFn(value, field.name, { + hints: options?.validationHints, + input: mappedObj + }); + } + } + field.pack(view, field.offset, value, mappedObj, options); + } + return view.buffer; + }, + packInto(obj, view, offset2, options) { + let mappedObj = obj; + if (structDefOptions?.mapValue) { + mappedObj = structDefOptions.mapValue(obj); + } + for (const field of layout) { + const value = mappedObj[field.name] ?? field.default; + if (!field.optional && value === undefined) { + console.warn(`packInto missing value for non-optional field '${field.name}' at offset ${offset2 + field.offset}. Writing default or zero.`); + } + if (field.validate) { + for (const validateFn of field.validate) { + validateFn(value, field.name, { + hints: options?.validationHints, + input: mappedObj + }); + } + } + field.pack(view, offset2 + field.offset, value, mappedObj, options); + } + }, + unpack(buf) { + if (buf.byteLength < totalSize) { + fatalError(`Buffer size (${buf.byteLength}) is smaller than struct size (${totalSize}) for unpacking.`); + } + const view = new DataView(buf); + const result = structDefOptions?.default ? { ...structDefOptions.default } : {}; + for (const field of layout) { + if (!field.unpack) { + continue; + } + try { + result[field.name] = field.unpack(view, field.offset); + } catch (e) { + console.error(`Error unpacking field '${field.name}' at offset ${field.offset}:`, e); + throw e; + } + } + if (structDefOptions?.reduceValue) { + return structDefOptions.reduceValue(result); + } + return result; + }, + packList(objects, options) { + if (objects.length === 0) { + return new ArrayBuffer(0); + } + const buffer = new ArrayBuffer(totalSize * objects.length); + const view = new DataView(buffer); + for (let i = 0;i < objects.length; i++) { + let mappedObj = objects[i]; + if (structDefOptions?.mapValue) { + mappedObj = structDefOptions.mapValue(objects[i]); + } + for (const field of layout) { + const value = mappedObj[field.name] ?? field.default; + if (!field.optional && value === undefined) { + fatalError(`Packing non-optional field '${field.name}' at index ${i} but value is undefined (and no default provided)`); + } + if (field.validate) { + for (const validateFn of field.validate) { + validateFn(value, field.name, { + hints: options?.validationHints, + input: mappedObj + }); + } + } + field.pack(view, i * totalSize + field.offset, value, mappedObj, options); + } + } + return buffer; + }, + unpackList(buf, count) { + if (count === 0) { + return []; + } + const expectedSize = totalSize * count; + if (buf.byteLength < expectedSize) { + fatalError(`Buffer size (${buf.byteLength}) is smaller than expected size (${expectedSize}) for unpacking ${count} structs.`); + } + const view = new DataView(buf); + const results = []; + for (let i = 0;i < count; i++) { + const offset2 = i * totalSize; + const result = structDefOptions?.default ? { ...structDefOptions.default } : {}; + for (const field of layout) { + if (!field.unpack) { + continue; + } + try { + result[field.name] = field.unpack(view, offset2 + field.offset); + } catch (e) { + console.error(`Error unpacking field '${field.name}' at index ${i}, offset ${offset2 + field.offset}:`, e); + throw e; + } + } + if (structDefOptions?.reduceValue) { + results.push(structDefOptions.reduceValue(result)); + } else { + results.push(result); + } + } + return results; + }, + describe() { + return description; + } + }; +} +export { + pointerSize, + packObjectArray, + objectPtr, + defineStruct, + defineEnum, + allocStruct +}; diff --git a/packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts b/packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts new file mode 100644 index 000000000..57ed752e1 --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts @@ -0,0 +1,31 @@ +import type { PrimitiveType, PointyObject, ObjectPointerDef, AllocStructOptions, AllocStructResult, EnumDef, StructDef, StructDefOptions, DefineStructReturnType } from "./types"; +export declare const pointerSize: number; +/** + * Type helper for creating object pointers for structs. + */ +export declare function objectPtr(): ObjectPointerDef; +export declare function allocStruct(structDef: StructDef, options?: AllocStructOptions): AllocStructResult; +export declare function defineEnum>(mapping: T, base?: Exclude): EnumDef; +type ValidationFunction = (value: any, fieldName: string, options: { + hints?: any; + input?: any; +}) => void | never; +interface StructFieldOptions { + optional?: boolean; + mapOptionalInline?: boolean; + unpackTransform?: (value: any) => any; + packTransform?: (value: any) => any; + lengthOf?: string; + asPointer?: boolean; + default?: any; + condition?: () => boolean; + validate?: ValidationFunction | ValidationFunction[]; +} +type StructField = readonly [string, PrimitiveType, StructFieldOptions?] | readonly [string, EnumDef, StructFieldOptions?] | readonly [string, StructDef, StructFieldOptions?] | readonly [string, "cstring" | "char*", StructFieldOptions?] | readonly [string, ObjectPointerDef, StructFieldOptions?] | readonly [ + string, + readonly [EnumDef | StructDef | PrimitiveType | ObjectPointerDef], + StructFieldOptions? +]; +export declare function packObjectArray(val: (PointyObject | null)[]): DataView; +export declare function defineStruct(fields: Fields & StructField[], structDefOptions?: Opts): DefineStructReturnType; +export {}; diff --git a/packages/core/src/compat/bun-ffi-structs/types.d.ts b/packages/core/src/compat/bun-ffi-structs/types.d.ts new file mode 100644 index 000000000..4de6c2b54 --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs/types.d.ts @@ -0,0 +1,122 @@ +import type { Pointer } from "../ffi.js"; +export type PrimitiveType = "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "pointer" | "i32" | "i16" | "bool_u8" | "bool_u32"; +export interface PointyObject { + ptr: Pointer | number | bigint | null; +} +export interface ObjectPointerDef { + __type: "objectPointer"; +} +type Prettify = { + [K in keyof T]: T[K]; +} & {}; +export type Simplify = T extends (...args: any[]) => any ? T : T extends object ? Prettify : T; +export type PrimitiveToTSType = T extends "u8" | "u16" | "u32" | "i16" | "i32" | "f32" | "f64" ? number : T extends "u64" ? bigint | number : T extends "bool_u8" | "bool_u32" ? boolean : T extends "pointer" ? number | bigint : never; +type FieldDefInputType = Options extends { + packTransform: (value: infer T) => any; +} ? T : Def extends PrimitiveType ? PrimitiveToTSType : Def extends "cstring" | "char*" ? string | null : Def extends EnumDef ? keyof E : Def extends StructDef ? InputType : Def extends ObjectPointerDef ? T | null : Def extends readonly [infer InnerDef] ? InnerDef extends PrimitiveType ? Iterable> : InnerDef extends EnumDef ? Iterable : InnerDef extends StructDef ? Iterable : InnerDef extends ObjectPointerDef ? (T | null)[] : never : never; +type HasLengthOfField = Fields extends readonly [ + infer First, + ...infer Rest extends readonly StructField[] +] ? First extends readonly [string, any, { + lengthOf: FieldName; +}] ? true : HasLengthOfField : false; +type FieldDefOutputType = Options extends { + unpackTransform: (value: any) => infer T; +} ? T : Def extends PrimitiveType ? PrimitiveToTSType : Def extends "cstring" ? string | null : Def extends "char*" ? HasLengthOfField extends true ? string | null : number : Def extends EnumDef ? keyof E : Def extends StructDef ? OutputType : Def extends ObjectPointerDef ? T | null : Def extends readonly [infer InnerDef] ? InnerDef extends PrimitiveType ? Iterable> : InnerDef extends EnumDef ? Iterable : InnerDef extends StructDef ? Iterable : InnerDef extends ObjectPointerDef ? (T | null)[] : never : never; +type IsOptional = Options extends { + optional: true; +} ? true : Options extends { + default: any; +} ? true : Options extends { + lengthOf: string; +} ? true : Options extends { + condition: () => boolean; +} ? true : false; +export type StructObjectInputType = { + [F in Fields[number] as IsOptional extends false ? F[0] : never]: FieldDefInputType; +} & { + [F in Fields[number] as IsOptional extends true ? F[0] : never]?: FieldDefInputType | null; +}; +export type StructObjectOutputType = { + [F in Fields[number] as IsOptional extends false ? F[0] : never]: FieldDefOutputType; +} & { + [F in Fields[number] as IsOptional extends true ? F[0] : never]?: FieldDefOutputType | null; +}; +export type DefineStructReturnType = StructDef infer R; +} ? R : StructObjectOutputType>, Simplify any; +} ? V : StructObjectInputType>>; +export interface AllocStructOptions { + lengths?: Record; +} +export interface AllocStructResult { + buffer: ArrayBuffer; + view: DataView; + subBuffers?: Record; +} +export interface EnumDef> { + __type: "enum"; + type: Exclude; + to(value: keyof T): number; + from(value: number | bigint): keyof T; + enum: T; +} +type ValidationFunction = (value: any, fieldName: string, options: { + hints?: any; + input?: any; +}) => void | never; +interface StructFieldOptions { + optional?: boolean; + mapOptionalInline?: boolean; + unpackTransform?: (value: any) => any; + packTransform?: (value: any) => any; + lengthOf?: string; + asPointer?: boolean; + default?: any; + condition?: () => boolean; + validate?: ValidationFunction | ValidationFunction[]; +} +type StructField = readonly [string, PrimitiveType, StructFieldOptions?] | readonly [string, EnumDef, StructFieldOptions?] | readonly [string, StructDef, StructFieldOptions?] | readonly [string, "cstring" | "char*", StructFieldOptions?] | readonly [string, ObjectPointerDef, StructFieldOptions?] | readonly [ + string, + readonly [EnumDef | StructDef | PrimitiveType | ObjectPointerDef], + StructFieldOptions? +]; +export interface StructFieldPackOptions { + validationHints?: any; +} +export interface StructFieldDescription { + name: string; + offset: number; + size: number; + align: number; + optional: boolean; + type: PrimitiveType | EnumDef | StructDef | "cstring" | "char*" | ObjectPointerDef | readonly [any]; + lengthOf?: string; +} +export interface ArrayFieldMetadata { + elementSize: number; + arrayOffset: number; + lengthOffset: number; + lengthPack: (view: DataView, offset: number, value: number) => void; +} +export interface StructDef { + __type: "struct"; + size: number; + align: number; + hasMapValue: boolean; + layoutByName: Map; + arrayFields: Map; + pack(obj: Simplify, options?: StructFieldPackOptions): ArrayBuffer; + packInto(obj: Simplify, view: DataView, offset: number, options?: StructFieldPackOptions): void; + packList(objects: Simplify[], options?: StructFieldPackOptions): ArrayBuffer; + unpack(buf: ArrayBuffer | SharedArrayBuffer): Simplify; + unpackList(buf: ArrayBuffer | SharedArrayBuffer, count: number): Simplify[]; + describe(): StructFieldDescription[]; +} +export interface StructDefOptions { + default?: Record; + mapValue?: (value: any) => any; + reduceValue?: (value: any) => any; +} +export {}; diff --git a/packages/core/src/compat/ffi.ts b/packages/core/src/compat/ffi.ts new file mode 100644 index 000000000..b342dcb93 --- /dev/null +++ b/packages/core/src/compat/ffi.ts @@ -0,0 +1,174 @@ +import { FFIType } from "./FFIType.js" + +export type Pointer = number & { __pointer__: null } + +interface FFITypeStringToType { + ["char"]: FFIType.char + ["int8_t"]: FFIType.int8_t + ["i8"]: FFIType.i8 + ["uint8_t"]: FFIType.uint8_t + ["u8"]: FFIType.u8 + ["int16_t"]: FFIType.int16_t + ["i16"]: FFIType.i16 + ["uint16_t"]: FFIType.uint16_t + ["u16"]: FFIType.u16 + ["int32_t"]: FFIType.int32_t + ["i32"]: FFIType.i32 + ["int"]: FFIType.int + ["uint32_t"]: FFIType.uint32_t + ["u32"]: FFIType.u32 + ["int64_t"]: FFIType.int64_t + ["i64"]: FFIType.i64 + ["uint64_t"]: FFIType.uint64_t + ["u64"]: FFIType.u64 + ["double"]: FFIType.double + ["f64"]: FFIType.f64 + ["float"]: FFIType.float + ["f32"]: FFIType.f32 + ["bool"]: FFIType.bool + ["ptr"]: FFIType.ptr + ["pointer"]: FFIType.pointer + ["void"]: FFIType.void + ["cstring"]: FFIType.cstring + ["function"]: FFIType.function + ["usize"]: FFIType.uint64_t + ["callback"]: FFIType.function + ["napi_env"]: FFIType.napi_env + ["napi_value"]: FFIType.napi_value + ["buffer"]: FFIType.buffer +} + +export type FFITypeOrString = FFIType | keyof FFITypeStringToType + +export interface FFIFunction { + readonly args?: readonly FFITypeOrString[] + readonly returns?: FFITypeOrString + readonly ptr?: Pointer | bigint + readonly threadsafe?: boolean +} + +type Symbols = Readonly> +type ToFFIType = T extends FFIType + ? T + : T extends keyof FFITypeStringToType + ? FFITypeStringToType[T] + : never +type NumericFFIType = + | FFIType.char + | FFIType.int8_t + | FFIType.i8 + | FFIType.uint8_t + | FFIType.u8 + | FFIType.int16_t + | FFIType.i16 + | FFIType.uint16_t + | FFIType.u16 + | FFIType.int32_t + | FFIType.i32 + | FFIType.int + | FFIType.uint32_t + | FFIType.u32 + | FFIType.double + | FFIType.f64 + | FFIType.float + | FFIType.f32 +type BigIntArgFFIType = + | FFIType.int64_t + | FFIType.i64 + | FFIType.uint64_t + | FFIType.u64 + | FFIType.i64_fast + | FFIType.u64_fast +type BigIntReturnFFIType = FFIType.int64_t | FFIType.i64 | FFIType.uint64_t | FFIType.u64 +type PointerLike = NodeJS.TypedArray | DataView | Pointer | null +type BufferLike = NodeJS.TypedArray | DataView +type FFIArgValue = T extends NumericFFIType + ? number + : T extends BigIntArgFFIType + ? number | bigint + : T extends FFIType.bool + ? boolean + : T extends FFIType.ptr | FFIType.pointer | FFIType.cstring + ? PointerLike + : T extends FFIType.void + ? undefined + : T extends FFIType.function + ? Pointer | JSCallback + : T extends FFIType.napi_env | FFIType.napi_value + ? unknown + : T extends FFIType.buffer + ? BufferLike + : never +type FFIReturnValue = T extends NumericFFIType + ? number + : T extends BigIntReturnFFIType + ? bigint + : T extends FFIType.i64_fast | FFIType.u64_fast + ? number | bigint + : T extends FFIType.bool + ? boolean + : T extends FFIType.ptr | FFIType.pointer | FFIType.cstring | FFIType.function + ? Pointer | null + : T extends FFIType.void + ? undefined + : T extends FFIType.napi_env | FFIType.napi_value + ? unknown + : T extends FFIType.buffer + ? BufferLike + : never + +declare const FFIFunctionCallableSymbol: unique symbol + +export type ConvertFns = { + [K in keyof Fns]: { + ( + ...args: Fns[K]["args"] extends infer A extends readonly FFITypeOrString[] + ? { [L in keyof A]: FFIArgValue> } + : [unknown] extends [Fns[K]["args"]] + ? [] + : never + ): [unknown] extends [Fns[K]["returns"]] ? undefined : FFIReturnValue>> + __ffi_function_callable: typeof FFIFunctionCallableSymbol + } +} + +export interface Library { + symbols: ConvertFns + close(): void +} + +export interface JSCallback { + readonly ptr: Pointer | null + readonly threadsafe: boolean + close(): void +} + +export interface JSCallbackConstructor { + new (callback: (...args: any[]) => any, definition: FFIFunction): JSCallback +} + +export type DlopenFunction = >(name: string | URL, symbols: Fns) => Library +export type PtrFunction = (value: ArrayBufferLike | ArrayBufferView) => Pointer +export type ToArrayBufferFunction = ( + pointer: Pointer | bigint | object | null, + offset?: number, + length?: number, +) => ArrayBuffer + +type FfiModule = { + JSCallback: JSCallbackConstructor + dlopen: DlopenFunction + ptr: PtrFunction + suffix: string + toArrayBuffer: ToArrayBufferFunction +} + +const ffiModule: FfiModule = ( + process.versions.bun ? await import("bun:ffi") : await import("./nodejs/ffi.js") +) as FfiModule + +export const JSCallback = ffiModule.JSCallback +export const dlopen = ffiModule.dlopen +export const ptr = ffiModule.ptr +export const suffix = ffiModule.suffix +export const toArrayBuffer = ffiModule.toArrayBuffer diff --git a/packages/core/src/compat/nodejs/Worker.ts b/packages/core/src/compat/nodejs/Worker.ts new file mode 100644 index 000000000..a94a4bb99 --- /dev/null +++ b/packages/core/src/compat/nodejs/Worker.ts @@ -0,0 +1,77 @@ +import { existsSync } from "node:fs" +import { extname, isAbsolute, resolve } from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" +import { Worker as NodeWorker } from "node:worker_threads" + +type MessageEventLike = { data: T } +type ErrorEventLike = { message: string } + +const ownExtension = extname(import.meta.url) + +function resolveWorkerTarget(url: string | URL): string { + if (url instanceof URL) { + return url.href + } + + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { + return url + } + + const absolutePath = isAbsolute(url) ? url : resolve(url) + return pathToFileURL(absolutePath).href +} + +function normalizeExtension(specifier: string): string +function normalizeExtension(specifier: URL): URL +function normalizeExtension(specifier: string | URL): string | URL { + if (existsSync(specifier)) { + return specifier + } + + const stringSpecifier = String(specifier) + const extension = extname(stringSpecifier) + if (extension === ownExtension) { + return specifier + } + + const newSpecifier = stringSpecifier.slice(0, -extension.length) + ownExtension + if (specifier instanceof URL) { + return new URL(newSpecifier) + } + return newSpecifier +} + +let trampoline: URL | undefined +let registerJs: string | URL | undefined + +export class Worker extends NodeWorker { + onmessage: ((event: MessageEventLike) => void) | null = null + onerror: ((event: ErrorEventLike) => void) | null = null + + constructor(url: string | URL) { + let execArgv = process.execArgv + if (import.meta.url.endsWith(".ts")) { + registerJs ??= normalizeExtension(new URL("./registerResolveJs.js", import.meta.url)) + const registerJsArg = `--import=${fileURLToPath(registerJs)}` + if (!execArgv.includes(registerJsArg)) { + execArgv = [...execArgv, registerJsArg] + } + } + + trampoline ??= normalizeExtension(new URL("./trampoline.worker.js", import.meta.url)) + super(trampoline, { + workerData: { + targetUrl: resolveWorkerTarget(url), + }, + execArgv, + }) + + this.on("message", (data: unknown) => { + this.onmessage?.({ data }) + }) + + this.on("error", (error: Error) => { + this.onerror?.(error) + }) + } +} diff --git a/packages/core/src/nodejs/bunModules/ffi.ts b/packages/core/src/compat/nodejs/ffi.ts similarity index 66% rename from packages/core/src/nodejs/bunModules/ffi.ts rename to packages/core/src/compat/nodejs/ffi.ts index 31615f1db..e73f433ae 100644 --- a/packages/core/src/nodejs/bunModules/ffi.ts +++ b/packages/core/src/compat/nodejs/ffi.ts @@ -1,339 +1,19 @@ +import koffi from "koffi" +import { fileURLToPath } from "node:url" +import { isArrayBufferView } from "node:util/types" import type { - dlopen as bunDlopen, - JSCallback as BunJSCallback, - ptr as bunPtr, - toArrayBuffer as bunToArrayBuffer, ConvertFns, + DlopenFunction, FFIFunction, FFITypeOrString, + JSCallback as IJSCallback, Pointer, -} from "bun:ffi" -import koffi from "koffi" -import { isArrayBufferView } from "node:util/types" + PtrFunction, + ToArrayBufferFunction, +} from "../ffi.js" +import { FFIType } from "../FFIType.js" -/** Copy of Bun's FFIType enum. */ -export enum FFIType { - char = 0, - /** - * 8-bit signed integer - * - * Must be a value between -127 and 127 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * signed char - * char // on x64 & aarch64 macOS - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - int8_t = 1, - /** - * 8-bit signed integer - * - * Must be a value between -127 and 127 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * signed char - * char // on x64 & aarch64 macOS - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - i8 = 1, - - /** - * 8-bit unsigned integer - * - * Must be a value between 0 and 255 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * unsigned char - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - uint8_t = 2, - /** - * 8-bit unsigned integer - * - * Must be a value between 0 and 255 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * unsigned char - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - u8 = 2, - - /** - * 16-bit signed integer - * - * Must be a value between -32768 and 32767 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * in16_t - * short // on arm64 & x64 - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - int16_t = 3, - /** - * 16-bit signed integer - * - * Must be a value between -32768 and 32767 - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * in16_t - * short // on arm64 & x64 - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - i16 = 3, - - /** - * 16-bit unsigned integer - * - * Must be a value between 0 and 65535, inclusive. - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * uint16_t - * unsigned short // on arm64 & x64 - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - uint16_t = 4, - /** - * 16-bit unsigned integer - * - * Must be a value between 0 and 65535, inclusive. - * - * When passing to a FFI function (C ABI), type coercion is not performed. - * - * In C: - * ```c - * uint16_t - * unsigned short // on arm64 & x64 - * ``` - * - * In JavaScript: - * ```js - * var num = 0; - * ``` - */ - u16 = 4, - - /** - * 32-bit signed integer - */ - int32_t = 5, - - /** - * 32-bit signed integer - * - * Alias of {@link FFIType.int32_t} - */ - i32 = 5, - /** - * 32-bit signed integer - * - * The same as `int` in C - * - * ```c - * int - * ``` - */ - int = 5, - - /** - * 32-bit unsigned integer - * - * The same as `unsigned int` in C (on x64 & arm64) - * - * C: - * ```c - * unsigned int - * ``` - * JavaScript: - * ```js - * ptr(new Uint32Array(1)) - * ``` - */ - uint32_t = 6, - /** - * 32-bit unsigned integer - * - * Alias of {@link FFIType.uint32_t} - */ - u32 = 6, - - /** - * int64 is a 64-bit signed integer - */ - int64_t = 7, - /** - * i64 is a 64-bit signed integer - */ - i64 = 7, - - /** - * 64-bit unsigned integer - */ - uint64_t = 8, - /** - * 64-bit unsigned integer - */ - u64 = 8, - - /** - * IEEE-754 double precision float - */ - double = 9, - - /** - * Alias of {@link FFIType.double} - */ - f64 = 9, - - /** - * IEEE-754 single precision float - */ - float = 10, - - /** - * Alias of {@link FFIType.float} - */ - f32 = 10, - - /** - * Boolean value - * - * Must be `true` or `false`. `0` and `1` type coercion is not supported. - * - * In C, this corresponds to: - * ```c - * bool - * _Bool - * ``` - */ - bool = 11, - - /** - * Pointer value - * - * See {@link Bun.FFI.ptr} for more information - * - * In C: - * ```c - * void* - * ``` - * - * In JavaScript: - * ```js - * ptr(new Uint8Array(1)) - * ``` - */ - ptr = 12, - /** - * Pointer value - * - * alias of {@link FFIType.ptr} - */ - pointer = 12, - - /** - * void value - * - * void arguments are not supported - * - * void return type is the default return type - * - * In C: - * ```c - * void - * ``` - */ - void = 13, - - /** - * When used as a `returns`, this will automatically become a {@link CString}. - * - * When used in `args` it is equivalent to {@link FFIType.pointer} - */ - cstring = 14, - - /** - * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance - * but means you might get a `BigInt` or you might get a `number`. - * - * In C, this always becomes `int64_t` - * - * In JavaScript, this could be number or it could be BigInt, depending on what - * value is passed in. - */ - i64_fast = 15, - - /** - * Attempt to coerce `BigInt` into a `Number` if it fits. This improves performance - * but means you might get a `BigInt` or you might get a `number`. - * - * In C, this always becomes `uint64_t` - * - * In JavaScript, this could be number or it could be BigInt, depending on what - * value is passed in. - */ - u64_fast = 16, - function = 17, - - napi_env = 18, - napi_value = 19, - buffer = 20, -} +export { FFIType } const FFITypeStringToType = { ["char"]: FFIType.char, @@ -415,7 +95,7 @@ function ffiTypeToKoffiType(type: FFITypeOrString): koffi.TypeSpec { return ffiTypeToKoffiTypeMap[numberType] } -export class JSCallback implements BunJSCallback { +export class JSCallback implements IJSCallback { #threadsafe: boolean #registeredCallback: koffi.IKoffiRegisteredCallback | null @@ -590,10 +270,10 @@ function nativeAlloc(value: object, bytes: number) { * passed as an FFI function argument, the wrapper can pass the original * TypedArray to koffi instead (enabling write-back for output parameters). */ -export const ptr: typeof bunPtr = (value) => { +export const ptr: PtrFunction = (value) => { const view = isArrayBufferView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) - : new Uint8Array(value) + : new Uint8Array(value as ArrayBuffer) // Allocate koffi memory and copy current data — gives a real native address // that can be safely embedded in struct binary data. @@ -621,9 +301,13 @@ function getMemcpy() { return _memcpy } -export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) => { +export const toArrayBuffer: ToArrayBufferFunction = (pointer, offset, length) => { + if (pointer === null) { + throw new TypeError("ptr must be a number") + } + if (length === undefined) { - throw new Error(`bun:ffi.toArrayBuffer requires a length argument`) + throw new Error(`nodejs ffi.toArrayBuffer requires a length argument`) } // If pointer is a koffi External, we can use koffi.view directly @@ -658,7 +342,7 @@ export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) // Fallback: memcpy for addresses we don't have an External for let ptrBigint = typeof pointer === "bigint" ? pointer : BigInt(pointer) - if (offset) { + if (offset && ptrBigint !== null) { ptrBigint += BigInt(offset) } const dest = new Uint8Array(length) @@ -666,27 +350,14 @@ export const toArrayBuffer: typeof bunToArrayBuffer = (pointer, offset, length) return dest.buffer } -function guessSuffix() { - switch (process.platform) { - case "darwin": - return "dylib" - case "linux": - return "so" - case "win32": - return "dll" - default: - return "so" - } -} +export const suffix: string = koffi.extension.slice(1) -export const suffix: string = guessSuffix() - -export const dlopen: typeof bunDlopen = (name, symbols) => { +export const dlopen: DlopenFunction = (name, symbols) => { let loadPath: string if (typeof name === "string") { loadPath = name } else if (name instanceof URL) { - loadPath = name.pathname + loadPath = fileURLToPath(name) } else { throw new Error(`Unsupported FFI library name: ${name}`) } @@ -701,3 +372,5 @@ export const dlopen: typeof bunDlopen = (name, symbols) => { close: () => lib.unload(), } } + +export const __url = import.meta.url diff --git a/packages/core/src/compat/nodejs/registerBun.ts b/packages/core/src/compat/nodejs/registerBun.ts new file mode 100644 index 000000000..ce1d76d07 --- /dev/null +++ b/packages/core/src/compat/nodejs/registerBun.ts @@ -0,0 +1,26 @@ +import * as mod from "node:module" +import * as NodeBun from "../runtime.js" +import { __url as ffiUrl } from "./ffi.js" + +if (typeof globalThis.Bun === "undefined") { + Object.defineProperty(globalThis, "Bun", { + value: NodeBun, + writable: false, + enumerable: true, + configurable: true, + }) +} + +mod.registerHooks({ + resolve: (specifier, context, next) => { + if (specifier === "bun:ffi") { + return next(ffiUrl, context) + } + + if (specifier.startsWith("bun:")) { + throw new Error(`Untransformed Bun specifier: '${specifier}' from '${context.parentURL}'`) + } + + return next(specifier, context) + }, +}) diff --git a/packages/core/src/compat/nodejs/registerResolveJs.ts b/packages/core/src/compat/nodejs/registerResolveJs.ts new file mode 100644 index 000000000..a8f28cab6 --- /dev/null +++ b/packages/core/src/compat/nodejs/registerResolveJs.ts @@ -0,0 +1,36 @@ +import * as mod from "node:module" +import path from "node:path" + +// allow import(foo.js) to resolve to import(foo.ts) +// required for workers under vitest +const extensionMap: Record = { + ".js": ".ts", + ".jsx": ".tsx", + ".cjs": ".cts", + ".mjs": ".mts", +} +mod.registerHooks({ + resolve: (specifier, context, next) => { + try { + return next(specifier, context) + } catch (error) { + if (!error || typeof error !== "object" || !("code" in error)) { + throw error + } + + if (error.code === "ERR_MODULE_NOT_FOUND") { + const extension = path.extname(specifier) + const newExtension = extension in extensionMap ? extensionMap[extension] : undefined + if (newExtension) { + return next(specifier.slice(0, -extension.length) + newExtension, context) + } + } + + if (error.code === "ERR_UNSUPPORTED_ESM_URL_SCHEME" && "message" in error) { + error.message += `\nSpecifier: '${specifier}'\nContext: '${JSON.stringify(context)}'` + } + + throw error + } + }, +}) diff --git a/packages/core/src/compat/nodejs/runtime.ts b/packages/core/src/compat/nodejs/runtime.ts new file mode 100644 index 000000000..633365106 --- /dev/null +++ b/packages/core/src/compat/nodejs/runtime.ts @@ -0,0 +1,37 @@ +import { mkdir, writeFile as writeFileNode } from "node:fs/promises" +import { dirname } from "node:path" +import { fileURLToPath } from "node:url" + +import stringWidthLib from "string-width" +import stripAnsiLib from "strip-ansi" + +import type { WriteFileOptions } from "../runtime.js" + +export function sleep(msOrDate: number | Date): Promise { + const ms = msOrDate instanceof Date ? msOrDate.getTime() - Date.now() : msOrDate + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export const stringWidth = stringWidthLib +export const stripANSI = stripAnsiLib + +export async function writeFile( + destination: string | URL, + data: string | ArrayBufferView, + options?: WriteFileOptions, +): Promise { + const destinationPath = destination instanceof URL ? fileURLToPath(destination) : destination + + if (options?.createPath) { + await mkdir(dirname(destinationPath), { recursive: true }) + } + + if (typeof data === "string") { + await writeFileNode(destination, data, { mode: options?.mode, encoding: "utf8" }) + return new TextEncoder().encode(data).length + } + + const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + await writeFileNode(destination, bytes, { mode: options?.mode }) + return bytes.length +} diff --git a/packages/core/src/nodejs/bunModules/test.ts b/packages/core/src/compat/nodejs/test.ts similarity index 100% rename from packages/core/src/nodejs/bunModules/test.ts rename to packages/core/src/compat/nodejs/test.ts diff --git a/packages/core/src/compat/nodejs/trampoline.worker.ts b/packages/core/src/compat/nodejs/trampoline.worker.ts new file mode 100644 index 000000000..c284409b5 --- /dev/null +++ b/packages/core/src/compat/nodejs/trampoline.worker.ts @@ -0,0 +1,29 @@ +import { parentPort as maybeParentPort, workerData } from "node:worker_threads" + +function setup() { + if (!maybeParentPort) { + throw new Error("Expected parentPort in worker thread") + } + const parentPort = maybeParentPort + globalThis.postMessage = (message) => parentPort.postMessage(message) + + let onmessage: ((event: MessageEvent) => void) | null = null + Object.defineProperty(globalThis, "onmessage", { + configurable: true, + enumerable: true, + set(handler) { + if (onmessage) { + parentPort.removeListener("message", onmessage) + onmessage = null + } + + if (handler) { + onmessage = (data) => handler({ data }) + parentPort.on("message", onmessage) + } + }, + }) +} + +setup() +await import(workerData.targetUrl) diff --git a/packages/core/src/compat/runtime.ts b/packages/core/src/compat/runtime.ts new file mode 100644 index 000000000..c524c7a23 --- /dev/null +++ b/packages/core/src/compat/runtime.ts @@ -0,0 +1,25 @@ +export interface WriteFileOptions { + createPath?: boolean + mode?: number +} + +type RuntimeModule = { + sleep: (msOrDate: number | Date) => Promise + stringWidth: (text: string) => number + stripANSI: (text: string) => string + writeFile: (destination: string | URL, data: string | ArrayBufferView, options?: WriteFileOptions) => Promise +} + +const runtime: RuntimeModule = process.versions.bun + ? { + sleep: Bun.sleep, + stringWidth: Bun.stringWidth, + stripANSI: Bun.stripANSI, + writeFile: Bun.write as RuntimeModule["writeFile"], + } + : await import("./nodejs/runtime.js") + +export const sleep = runtime.sleep +export const stringWidth = runtime.stringWidth +export const stripANSI = runtime.stripANSI +export const writeFile = runtime.writeFile diff --git a/packages/core/src/compat/test.ts b/packages/core/src/compat/test.ts new file mode 100644 index 000000000..54cec7971 --- /dev/null +++ b/packages/core/src/compat/test.ts @@ -0,0 +1 @@ +export * from "./nodejs/test.js" diff --git a/packages/core/src/compat/testHelpers.ts b/packages/core/src/compat/testHelpers.ts new file mode 100644 index 000000000..4694327bb --- /dev/null +++ b/packages/core/src/compat/testHelpers.ts @@ -0,0 +1,47 @@ +import * as cp from "node:child_process" + +export interface SpawnSyncOptions { + cwd?: string + env?: NodeJS.ProcessEnv + stderr?: "ignore" | "pipe" + stdin?: "ignore" | "pipe" + stdout?: "ignore" | "pipe" + timeout?: number + maxBuffer?: number +} + +export interface SpawnSyncResult { + stdout: Uint8Array + stderr: Uint8Array + exitCode: number + success: boolean + pid: number + signalCode?: NodeJS.Signals | number +} + +export function spawnSync(cmd: string[], options: SpawnSyncOptions = {}): SpawnSyncResult { + const [file, ...rawArgs] = cmd + const shouldAddNodeTypeFlags = !process.versions.bun && (file === process.execPath || file === "node") + const args = shouldAddNodeTypeFlags ? ["--experimental-transform-types", ...rawArgs] : rawArgs + + const result = cp.spawnSync(file, args, { + cwd: options.cwd, + env: options.env, + stdio: [ + options.stdin === "pipe" ? "pipe" : "ignore", + options.stdout === "pipe" ? "pipe" : "ignore", + options.stderr === "pipe" ? "pipe" : "ignore", + ], + timeout: options.timeout, + maxBuffer: options.maxBuffer, + }) + + return { + stdout: result.stdout ?? Buffer.alloc(0), + stderr: result.stderr ?? Buffer.alloc(0), + exitCode: result.status ?? 1, + success: result.status === 0, + pid: result.pid ?? 0, + signalCode: result.signal ?? undefined, + } +} diff --git a/packages/core/src/edit-buffer.test.ts b/packages/core/src/edit-buffer.test.ts index ca1010dce..0a10354d3 100644 --- a/packages/core/src/edit-buffer.test.ts +++ b/packages/core/src/edit-buffer.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { sleep } from "./compat/runtime.js" import { EditBuffer } from "./edit-buffer.js" describe("EditBuffer", () => { @@ -910,19 +911,19 @@ describe("EditBuffer Events", () => { }) testBuffer1.setText("Buffer 1") - await Bun.sleep(10) + await sleep(10) const count1AfterSetText = count1 testBuffer1.moveCursorRight() - await Bun.sleep(10) + await sleep(10) expect(count1).toBeGreaterThan(count1AfterSetText) expect(count2).toBe(0) testBuffer2.setText("Buffer 2") - await Bun.sleep(10) + await sleep(10) const count2AfterSetText = count2 testBuffer2.moveCursorRight() - await Bun.sleep(10) + await sleep(10) expect(count1).toBe(count1AfterSetText + 1) expect(count2).toBeGreaterThan(count2AfterSetText) @@ -963,7 +964,7 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello World") - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(0) testBuffer.destroy() @@ -978,11 +979,11 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount testBuffer.insertText(" World") - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) testBuffer.destroy() @@ -997,12 +998,12 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello World") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount testBuffer.setCursorToLineCol(0, 5) testBuffer.deleteChar() - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) testBuffer.destroy() @@ -1017,12 +1018,12 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount testBuffer.setCursorToLineCol(0, 5) testBuffer.deleteCharBackward() - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) testBuffer.destroy() @@ -1037,12 +1038,12 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Line 1\nLine 2\nLine 3") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount testBuffer.gotoLine(1) testBuffer.deleteLine() - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) testBuffer.destroy() @@ -1057,12 +1058,12 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount testBuffer.setCursorToLineCol(0, 5) testBuffer.newLine() - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) testBuffer.destroy() @@ -1082,7 +1083,7 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) expect(count1).toBeGreaterThan(0) expect(count2).toBeGreaterThan(0) @@ -1094,7 +1095,7 @@ describe("EditBuffer Events", () => { it("should support removing content-changed listeners", async () => { const testBuffer = EditBuffer.create("wcwidth") testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) let eventCount = 0 const listener = () => { @@ -1103,13 +1104,13 @@ describe("EditBuffer Events", () => { testBuffer.on("content-changed", listener) testBuffer.insertText(" World") - await Bun.sleep(10) + await sleep(10) const firstCount = eventCount testBuffer.off("content-changed", listener) testBuffer.insertText("!") - await Bun.sleep(10) + await sleep(10) // Count should not have increased after removing listener expect(eventCount).toBe(firstCount) @@ -1132,21 +1133,21 @@ describe("EditBuffer Events", () => { }) testBuffer1.setText("Buffer 1") - await Bun.sleep(10) + await sleep(10) const count1AfterSetText = count1 testBuffer1.insertText(" updated") - await Bun.sleep(10) + await sleep(10) expect(count1).toBeGreaterThan(count1AfterSetText) expect(count2).toBe(0) testBuffer2.setText("Buffer 2") - await Bun.sleep(10) + await sleep(10) const count2AfterSetText = count2 testBuffer2.insertText(" updated") - await Bun.sleep(10) + await sleep(10) expect(count1).toBe(count1AfterSetText + 1) expect(count2).toBeGreaterThan(count2AfterSetText) @@ -1164,7 +1165,7 @@ describe("EditBuffer Events", () => { }) testBuffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) const countBeforeDestroy = eventCount @@ -1404,7 +1405,7 @@ describe("EditBuffer History Management", () => { }) buffer.setText("Hello") - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(0) }) @@ -1416,7 +1417,7 @@ describe("EditBuffer History Management", () => { }) buffer.replaceText("Hello") - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(0) }) @@ -1428,7 +1429,7 @@ describe("EditBuffer History Management", () => { }) buffer.setTextOwned("Hello") - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(0) }) @@ -1556,11 +1557,11 @@ describe("EditBuffer Clear Method", () => { }) buffer.setText("Hello World") - await Bun.sleep(10) + await sleep(10) const countAfterSetText = eventCount buffer.clear() - await Bun.sleep(10) + await sleep(10) expect(eventCount).toBeGreaterThan(countAfterSetText) }) @@ -1573,11 +1574,11 @@ describe("EditBuffer Clear Method", () => { buffer.setText("Hello World") buffer.setCursorToLineCol(0, 5) - await Bun.sleep(10) + await sleep(10) const countBeforeClear = eventCount buffer.clear() - await Bun.sleep(10) + await sleep(10) // Should emit cursor-changed when resetting cursor to 0,0 expect(eventCount).toBeGreaterThan(countBeforeClear) @@ -1596,13 +1597,13 @@ describe("EditBuffer Clear Method", () => { buffer.setText("Hello World") buffer.setCursorToLineCol(0, 5) - await Bun.sleep(10) + await sleep(10) const contentCountBefore = contentChangedCount const cursorCountBefore = cursorChangedCount buffer.clear() - await Bun.sleep(10) + await sleep(10) expect(contentChangedCount).toBeGreaterThan(contentCountBefore) expect(cursorChangedCount).toBeGreaterThan(cursorCountBefore) diff --git a/packages/core/src/edit-buffer.ts b/packages/core/src/edit-buffer.ts index 5be36e0a3..f07e945fc 100644 --- a/packages/core/src/edit-buffer.ts +++ b/packages/core/src/edit-buffer.ts @@ -1,5 +1,5 @@ import { resolveRenderLib, type LogicalCursor, type RenderLib } from "./zig.js" -import { type Pointer } from "bun:ffi" +import { type Pointer } from "./compat/ffi.js" import { type WidthMethod, type Highlight } from "./types.js" import { RGBA } from "./lib/RGBA.js" import { EventEmitter } from "events" diff --git a/packages/core/src/editor-view.ts b/packages/core/src/editor-view.ts index 4ee917d0e..36c50d352 100644 --- a/packages/core/src/editor-view.ts +++ b/packages/core/src/editor-view.ts @@ -1,6 +1,6 @@ import { RGBA } from "./lib/RGBA.js" import { resolveRenderLib, type RenderLib, type VisualCursor, type LineInfo } from "./zig.js" -import { type Pointer } from "bun:ffi" +import { type Pointer } from "./compat/ffi.js" import type { EditBuffer } from "./edit-buffer.js" import { createExtmarksController } from "./lib/index.js" diff --git a/packages/core/src/lib/clipboard.ts b/packages/core/src/lib/clipboard.ts index 3a04d9ac3..f4b0c4f45 100644 --- a/packages/core/src/lib/clipboard.ts +++ b/packages/core/src/lib/clipboard.ts @@ -1,7 +1,7 @@ // OSC 52 clipboard support for terminal applications. // Delegates to native Zig implementation for ANSI sequence generation. -import type { Pointer } from "bun:ffi" +import type { Pointer } from "../compat/ffi.js" import type { RenderLib } from "../zig.js" export enum ClipboardTarget { diff --git a/packages/core/src/lib/extmarks-multiwidth.test.ts b/packages/core/src/lib/extmarks-multiwidth.test.ts index f00e52606..069465fec 100644 --- a/packages/core/src/lib/extmarks-multiwidth.test.ts +++ b/packages/core/src/lib/extmarks-multiwidth.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, afterEach } from "bun:test" +import { stringWidth } from "../compat/runtime.js" import { TextareaRenderable } from "../renderables/Textarea.js" import { createTestRenderer, type TestRenderer, type MockInput } from "../testing/test-renderer.js" import { type ExtmarksController } from "./extmarks.js" @@ -64,12 +65,12 @@ describe("ExtmarksController - Multi-width Graphemes", () => { if (text[i] === "\n") { displayOffset += 1 } else { - displayOffset += Bun.stringWidth(text[i]) + displayOffset += stringWidth(text[i]) } } const mentionText = "@git-committer" - const mentionDisplayWidth = Bun.stringWidth(mentionText) + const mentionDisplayWidth = stringWidth(mentionText) const mentionStart = displayOffset // Should be 11 const mentionEnd = displayOffset + mentionDisplayWidth // Should be 25 diff --git a/packages/core/src/lib/extmarks.ts b/packages/core/src/lib/extmarks.ts index fc77247bf..a76705738 100644 --- a/packages/core/src/lib/extmarks.ts +++ b/packages/core/src/lib/extmarks.ts @@ -1,5 +1,6 @@ import type { EditBuffer } from "../edit-buffer.js" import type { EditorView } from "../editor-view.js" +import { stringWidth } from "../compat/runtime.js" import { ExtmarksHistory, type ExtmarksSnapshot } from "./extmarks-history.js" export interface Extmark { @@ -614,7 +615,7 @@ export class ExtmarksController { j++ } const chunk = text.substring(i, j) - const chunkWidth = Bun.stringWidth(chunk) + const chunkWidth = stringWidth(chunk) if (displayWidthSoFar + chunkWidth < offset) { // Entire chunk fits before offset @@ -624,7 +625,7 @@ export class ExtmarksController { // Offset is within this chunk - need to find exact position // Walk character by character for (let k = i; k < j && displayWidthSoFar < offset; k++) { - const charWidth = Bun.stringWidth(text[k]) + const charWidth = stringWidth(text[k]) displayWidthSoFar += charWidth } break diff --git a/packages/core/src/lib/paste.ts b/packages/core/src/lib/paste.ts index 93e704801..e4c1da706 100644 --- a/packages/core/src/lib/paste.ts +++ b/packages/core/src/lib/paste.ts @@ -1,3 +1,5 @@ +import { stripANSI } from "../compat/runtime.js" + export type PasteKind = "text" | "binary" | "unknown" export interface PasteMetadata { @@ -12,5 +14,5 @@ export function decodePasteBytes(bytes: Uint8Array): string { } export function stripAnsiSequences(text: string): string { - return Bun.stripANSI(text) + return stripANSI(text) } diff --git a/packages/core/src/lib/tree-sitter/assets/update.ts b/packages/core/src/lib/tree-sitter/assets/update.ts index d877e0780..da9295bcf 100644 --- a/packages/core/src/lib/tree-sitter/assets/update.ts +++ b/packages/core/src/lib/tree-sitter/assets/update.ts @@ -146,15 +146,15 @@ async function downloadAndCombineQueries( } async function generateDefaultParsersFile(parsers: GeneratedParser[], outputPath: string): Promise { - const imports = parsers + const constants = parsers .map((parser) => { const safeFiletype = parser.filetype.replace(/[^a-zA-Z0-9]/g, "_") const lines = [ - `import ${safeFiletype}_highlights from "${parser.highlightsPath}" with { type: "file" }`, - `import ${safeFiletype}_language from "${parser.languagePath}" with { type: "file" }`, + `const ${safeFiletype}_highlights = fileURLToPath(new URL("${parser.highlightsPath}", import.meta.url))`, + `const ${safeFiletype}_language = fileURLToPath(new URL("${parser.languagePath}", import.meta.url))`, ] if (parser.injectionsPath) { - lines.push(`import ${safeFiletype}_injections from "${parser.injectionsPath}" with { type: "file" }`) + lines.push(`const ${safeFiletype}_injections = fileURLToPath(new URL("${parser.injectionsPath}", import.meta.url))`) } return lines.join("\n") }) @@ -163,13 +163,9 @@ async function generateDefaultParsersFile(parsers: GeneratedParser[], outputPath const parserDefinitions = parsers .map((parser) => { const safeFiletype = parser.filetype.replace(/[^a-zA-Z0-9]/g, "_") - const queriesLines = [ - ` highlights: [resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_highlights)],`, - ] + const queriesLines = [` highlights: [${safeFiletype}_highlights],`] if (parser.injectionsPath) { - queriesLines.push( - ` injections: [resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_injections)],`, - ) + queriesLines.push(` injections: [${safeFiletype}_injections],`) } const injectionMappingLine = parser.injectionMapping @@ -182,7 +178,7 @@ async function generateDefaultParsersFile(parsers: GeneratedParser[], outputPath ${aliasesLine ? aliasesLine + "\n" : ""} queries: { ${queriesLines.join("\n")} }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_language),${injectionMappingLine ? "\n" + injectionMappingLine : ""} + wasm: ${safeFiletype}_language,${injectionMappingLine ? "\n" + injectionMappingLine : ""} }` }) .join(",\n") @@ -191,11 +187,10 @@ ${queriesLines.join("\n")} // Run 'bun assets/update.ts' to regenerate this file // Last generated: ${new Date().toISOString()} -import type { FiletypeParserOptions } from "./types" -import { resolve, dirname } from "path" -import { fileURLToPath } from "url" +import { fileURLToPath } from "node:url" +import type { FiletypeParserOptions } from "./types.js" -${imports} +${constants} // Cached parsers to avoid re-resolving paths on every call let _cachedParsers: FiletypeParserOptions[] | undefined diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index 0e7d88701..d70fb1caa 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test" -import { createServer, type Server } from "node:http" import { readFileSync } from "node:fs" +import { createServer, type Server } from "node:http" import { mkdir, readdir, stat, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join, resolve } from "node:path" diff --git a/packages/core/src/lib/tree-sitter/client.ts b/packages/core/src/lib/tree-sitter/client.ts index c7dcd5cc4..b8c7f38ea 100644 --- a/packages/core/src/lib/tree-sitter/client.ts +++ b/packages/core/src/lib/tree-sitter/client.ts @@ -16,6 +16,7 @@ import { resolve, isAbsolute, parse } from "path" import { existsSync } from "fs" import { registerEnvVar, env } from "../env.js" import { isBunfsPath, normalizeBunfsPath } from "../bunfs.js" +import { Worker } from "../../compat/Worker.js" registerEnvVar({ name: "OTUI_TREE_SITTER_WORKER_PATH", @@ -52,7 +53,7 @@ const isUrl = (path: string) => path.startsWith("http://") || path.startsWith("h // TODO: TreeSitterClient should have a setOptions method, passing it on to the worker etc. export class TreeSitterClient extends EventEmitter { private initialized = false - private worker: Worker | undefined + private worker: InstanceType | undefined private buffers: Map = new Map() private initializePromise: Promise | undefined private initializeResolvers: diff --git a/packages/core/src/lib/tree-sitter/default-parsers.ts b/packages/core/src/lib/tree-sitter/default-parsers.ts index 5c4018cc0..310ec65ae 100644 --- a/packages/core/src/lib/tree-sitter/default-parsers.ts +++ b/packages/core/src/lib/tree-sitter/default-parsers.ts @@ -2,21 +2,20 @@ // Run 'bun assets/update.ts' to regenerate this file // Last generated: 2026-03-20T21:07:24.696Z +import { fileURLToPath } from "node:url" import type { FiletypeParserOptions } from "./types.js" -import { resolve, dirname } from "path" -import { fileURLToPath } from "url" -import javascript_highlights from "./assets/javascript/highlights.scm" with { type: "file" } -import javascript_language from "./assets/javascript/tree-sitter-javascript.wasm" with { type: "file" } -import typescript_highlights from "./assets/typescript/highlights.scm" with { type: "file" } -import typescript_language from "./assets/typescript/tree-sitter-typescript.wasm" with { type: "file" } -import markdown_highlights from "./assets/markdown/highlights.scm" with { type: "file" } -import markdown_language from "./assets/markdown/tree-sitter-markdown.wasm" with { type: "file" } -import markdown_injections from "./assets/markdown/injections.scm" with { type: "file" } -import markdown_inline_highlights from "./assets/markdown_inline/highlights.scm" with { type: "file" } -import markdown_inline_language from "./assets/markdown_inline/tree-sitter-markdown_inline.wasm" with { type: "file" } -import zig_highlights from "./assets/zig/highlights.scm" with { type: "file" } -import zig_language from "./assets/zig/tree-sitter-zig.wasm" with { type: "file" } +const javascript_highlights = fileURLToPath(new URL("./assets/javascript/highlights.scm", import.meta.url)) +const javascript_language = fileURLToPath(new URL("./assets/javascript/tree-sitter-javascript.wasm", import.meta.url)) +const typescript_highlights = fileURLToPath(new URL("./assets/typescript/highlights.scm", import.meta.url)) +const typescript_language = fileURLToPath(new URL("./assets/typescript/tree-sitter-typescript.wasm", import.meta.url)) +const markdown_highlights = fileURLToPath(new URL("./assets/markdown/highlights.scm", import.meta.url)) +const markdown_language = fileURLToPath(new URL("./assets/markdown/tree-sitter-markdown.wasm", import.meta.url)) +const markdown_injections = fileURLToPath(new URL("./assets/markdown/injections.scm", import.meta.url)) +const markdown_inline_highlights = fileURLToPath(new URL("./assets/markdown_inline/highlights.scm", import.meta.url)) +const markdown_inline_language = fileURLToPath(new URL("./assets/markdown_inline/tree-sitter-markdown_inline.wasm", import.meta.url)) +const zig_highlights = fileURLToPath(new URL("./assets/zig/highlights.scm", import.meta.url)) +const zig_language = fileURLToPath(new URL("./assets/zig/tree-sitter-zig.wasm", import.meta.url)) // Cached parsers to avoid re-resolving paths on every call let _cachedParsers: FiletypeParserOptions[] | undefined @@ -28,25 +27,25 @@ export function getParsers(): FiletypeParserOptions[] { filetype: "javascript", aliases: ["javascriptreact"], queries: { - highlights: [resolve(dirname(fileURLToPath(import.meta.url)), javascript_highlights)], + highlights: [javascript_highlights], }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), javascript_language), + wasm: javascript_language, }, { filetype: "typescript", aliases: ["typescriptreact"], queries: { - highlights: [resolve(dirname(fileURLToPath(import.meta.url)), typescript_highlights)], + highlights: [typescript_highlights], }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), typescript_language), + wasm: typescript_language, }, { filetype: "markdown", queries: { - highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_highlights)], - injections: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_injections)], + highlights: [markdown_highlights], + injections: [markdown_injections], }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_language), + wasm: markdown_language, injectionMapping: { "nodeTypes": { "inline": "markdown_inline", @@ -69,16 +68,16 @@ export function getParsers(): FiletypeParserOptions[] { { filetype: "markdown_inline", queries: { - highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_highlights)], + highlights: [markdown_inline_highlights], }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_language), + wasm: markdown_inline_language, }, { filetype: "zig", queries: { - highlights: [resolve(dirname(fileURLToPath(import.meta.url)), zig_highlights)], + highlights: [zig_highlights], }, - wasm: resolve(dirname(fileURLToPath(import.meta.url)), zig_language), + wasm: zig_language, }, ] } diff --git a/packages/core/src/lib/tree-sitter/parser.worker.ts b/packages/core/src/lib/tree-sitter/parser.worker.ts index f49fdb73a..f31544124 100644 --- a/packages/core/src/lib/tree-sitter/parser.worker.ts +++ b/packages/core/src/lib/tree-sitter/parser.worker.ts @@ -1,18 +1,19 @@ -import { Parser, Query, Tree, Language } from "web-tree-sitter" -import type { Edit, QueryCapture, Range } from "web-tree-sitter" import { mkdir } from "fs/promises" import * as path from "path" +import { fileURLToPath } from "url" +import type { Edit, QueryCapture, Range } from "web-tree-sitter" +import { Language, Parser, Query, Tree } from "web-tree-sitter" +import { isMainThread } from "worker_threads" +import { isBunfsPath, normalizeBunfsPath } from "../bunfs.js" +import { DownloadUtils } from "./download-utils.js" import type { + FiletypeParserOptions, HighlightRange, HighlightResponse, - SimpleHighlight, - FiletypeParserOptions, - PerformanceStats, InjectionMapping, + PerformanceStats, + SimpleHighlight, } from "./types.js" -import { DownloadUtils } from "./download-utils.js" -import { isMainThread } from "worker_threads" -import { isBunfsPath, normalizeBunfsPath } from "../bunfs.js" const self = globalThis @@ -88,9 +89,7 @@ class ParserWorker { await mkdir(path.join(this.tsDataPath, "languages"), { recursive: true }) await mkdir(path.join(this.tsDataPath, "queries"), { recursive: true }) - let { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { - with: { type: "wasm" }, - }) + let treeWasm = fileURLToPath(new URL(import.meta.resolve("web-tree-sitter/tree-sitter.wasm"))) if (isBunfsPath(treeWasm)) { treeWasm = normalizeBunfsPath(path.parse(treeWasm).base) diff --git a/packages/core/src/nodejs/NodeBun.ts b/packages/core/src/nodejs/NodeBun.ts deleted file mode 100644 index 58b02d2bf..000000000 --- a/packages/core/src/nodejs/NodeBun.ts +++ /dev/null @@ -1,169 +0,0 @@ -import * as cp from "node:child_process" -import type { WriteFileOptions } from "node:fs" -import * as fs from "node:fs/promises" -import * as path from "node:path" -import { isArrayBufferView } from "node:util/types" -import stringWidth from "string-width" -import stripANSI from "strip-ansi" - -/** - * ```bash - * rg 'Bun\.(\w+)' -r ' | "$1"' -o -N -I | sort | uniq | pbcopy - * ``` - */ -type UsedBunApis = - | "argv" - | "build" - | "file" - | "Glob" - | "serve" - | "sleep" - | "spawn" - | "spawnSync" - | "stringWidth" - | "stripANSI" - | "write" - -type NodeBunInterface = Pick - -type BunFile = Bun.BunFile -type BunWrite = typeof Bun.write -type BunFileLike = { name: string | undefined } -type BunPathLike = string | NodeJS.TypedArray | ArrayBufferLike | URL - -type SpawnSyncOptions = Bun.SpawnOptions.SpawnSyncOptions< - Bun.SpawnOptions.Writable, - Bun.SpawnOptions.Readable, - Bun.SpawnOptions.Readable -> - -class NodeBunError extends Error { - constructor(message: string) { - super(message) - this.name = "NodeBunError" - } -} - -class NodeBun implements NodeBunInterface { - get argv(): string[] { - return process.argv - } - - sleep(msOrDate: number | Date): Promise { - let ms: number - if (msOrDate instanceof Date) { - ms = msOrDate.getTime() - Date.now() - } else { - ms = msOrDate - } - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - stringWidth = stringWidth - stripANSI = stripANSI - - write: typeof Bun.write = (destination, data, options): Promise => { - let dest: string | URL - if (typeof destination === "string") { - dest = destination - } else if (destination instanceof URL) { - dest = destination - } else if ("name" in destination && destination.name !== undefined) { - dest = destination.name - } else { - // ArrayBuffer, NodeJS.TypedArray, etc. - throw new NodeBunError("Bun.write: Unsupported destination type") - } - - let buffer: Uint8Array - if (typeof data === "string") { - buffer = new TextEncoder().encode(data) - } else if (isArrayBufferView(data)) { - buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength) - } else { - throw new NodeBunError("Bun.write: Unsupported data type") - } - - const nodeOptions: WriteFileOptions = {} - if (typeof buffer === "string") { - nodeOptions.encoding = "utf-8" - } - if (options && "mode" in options && options?.mode !== undefined) { - nodeOptions.mode = options.mode - } - if (options && "createPath" in options && options?.createPath) { - const destPath = typeof dest === "string" ? dest : dest.pathname - fs.mkdir(path.dirname(destPath), { recursive: true }) - } - - return fs.writeFile(dest, buffer, nodeOptions).then(() => buffer.length) - } - - // Unsupported - get Glob(): typeof Bun.Glob { - throw new NodeBunError("Bun.Glob is not supported in Node.js") - } - - get spawn(): typeof Bun.spawn { - throw new NodeBunError("Bun.spawn is not supported in Node.js") - } - - spawnSync: typeof Bun.spawnSync = (( - cmdsOrOptions: string[] | (SpawnSyncOptions & { cmd: string[] }), - options?: SpawnSyncOptions, - ): Bun.SyncSubprocess => { - let cmd: string[] - let opts: SpawnSyncOptions - if (Array.isArray(cmdsOrOptions)) { - cmd = cmdsOrOptions - opts = options ?? {} - } else { - cmd = cmdsOrOptions.cmd - opts = cmdsOrOptions - } - - const [file, ...rawArgs] = cmd - // When spawning node, inject compat shims so the child process can load .ts - // files and use Bun APIs (bun:ffi, bun:test, etc.) - let args = rawArgs - if (file === process.execPath || file === "node") { - const compatPath = new URL("./compat.ts", import.meta.url).pathname - args = ["--experimental-transform-types", `--import=${compatPath}`, ...rawArgs] - } - const result = cp.spawnSync(file, args, { - cwd: opts.cwd, - env: opts.env as NodeJS.ProcessEnv | undefined, - stdio: [ - opts.stdin === "pipe" ? "pipe" : "ignore", - opts.stdout === "pipe" ? "pipe" : "ignore", - opts.stderr === "pipe" ? "pipe" : "ignore", - ], - timeout: opts.timeout, - maxBuffer: opts.maxBuffer, - }) - - return { - stdout: result.stdout ?? Buffer.alloc(0), - stderr: result.stderr ?? Buffer.alloc(0), - exitCode: result.status ?? 1, - success: result.status === 0, - pid: result.pid ?? 0, - signalCode: result.signal ?? undefined, - resourceUsage: undefined!, - } as Bun.SyncSubprocess - }) as typeof Bun.spawnSync - - get build(): typeof Bun.build { - throw new NodeBunError("Bun.build is not supported in Node.js") - } - - get file(): typeof Bun.file { - throw new NodeBunError("Bun.file is not supported in Node.js") - } - - get serve(): typeof Bun.serve { - throw new NodeBunError("Bun.serve is not supported in Node.js") - } -} - -export default new NodeBun() diff --git a/packages/core/src/nodejs/bunModules/bun.ts b/packages/core/src/nodejs/bunModules/bun.ts deleted file mode 100644 index 7d29a20d9..000000000 --- a/packages/core/src/nodejs/bunModules/bun.ts +++ /dev/null @@ -1 +0,0 @@ -export default Bun diff --git a/packages/core/src/nodejs/compat.ts b/packages/core/src/nodejs/compat.ts deleted file mode 100644 index 00070ac68..000000000 --- a/packages/core/src/nodejs/compat.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as mod from "node:module" -import { fileURLToPath } from "node:url" -import { Worker as NodeWorker, isMainThread, parentPort } from "node:worker_threads" - -const require = mod.createRequire(import.meta.url) - -/** - * Wraps node:worker_threads Worker to match the Web Worker API surface - * used by this project (constructor with URL string, .onmessage, .onerror, - * .postMessage, .terminate). - */ -class WebWorkerShim extends NodeWorker { - onmessage: ((event: { data: unknown }) => void) | null = null - onerror: ((event: { message: string }) => void) | null = null - - constructor(url: string | URL) { - const resolved = typeof url === "string" && url.startsWith("file://") ? new URL(url) : url - let constructorError: Error | null = null - try { - super(resolved, { execArgv: [...process.execArgv, `--import=${import.meta.url}`] }) - } catch (e) { - // Bun defers worker errors instead of throwing synchronously. - // Allocate a dummy worker and fire the error async. - super(new URL(import.meta.url), { execArgv: [] }) - constructorError = e as Error - this.terminate() - } - this.on("message", (data: unknown) => { - this.onmessage?.({ data }) - }) - this.on("error", (error: Error) => { - this.onerror?.({ message: error.message }) - }) - if (constructorError) { - const err = constructorError - queueMicrotask(() => this.emit("error", err)) - } - } -} - -/** - * Sets up Bun shims in a Node.js process. - */ -export function setup() { - Object.defineProperty(globalThis, "Bun", { - configurable: true, - enumerable: true, - get: () => require("./NodeBun.js").default, - }) - - if (globalThis.Worker === undefined) { - ;(globalThis as any).Worker = WebWorkerShim - } - - // Inside a worker thread, bridge Web Worker messaging API to parentPort - if (!isMainThread && parentPort) { - ;(globalThis as any).postMessage = (msg: unknown) => parentPort!.postMessage(msg) - Object.defineProperty(globalThis, "onmessage", { - configurable: true, - set(handler: ((event: { data: unknown }) => void) | null) { - parentPort!.removeAllListeners("message") - if (handler) { - parentPort!.on("message", (data: unknown) => handler({ data })) - } - }, - }) - } - - mod.registerHooks({ resolve: resolveBun, load: loadBun }) - mod.registerHooks({ resolve: resolveJsToTs }) -} - -const BUN_PREFIX = "bun:" - -const resolveBun: mod.ResolveHookSync = (request, context, next) => { - if (request.startsWith(BUN_PREFIX) || request === "bun") { - const name = request === "bun" ? "bun" : request.slice(BUN_PREFIX.length) - const extname = import.meta.url.split(".").pop() - const result = next(`./bunModules/${name}.${extname}`, { - parentURL: import.meta.url, - importAttributes: { - type: "commonjs", - }, - }) - return result - } - - return next(request, context) -} - -const loadBun: mod.LoadHookSync = (url, context, next) => { - if (context.importAttributes?.type === "file" || context.importAttributes?.type === "wasm") { - // Bun's `import ... with { type: "file" }` returns the absolute file path. - // Convert file:// URL to a path to match. - const filePath = url.startsWith("file://") ? fileURLToPath(url) : url - return { - shortCircuit: true, - format: "json", - source: JSON.stringify(filePath), - } - } - - const result = next(url, context) - if (result.source === undefined) { - return { ...result, shortCircuit: true } - } - return result -} - -const JS_EXTENSIONS = [".js", ".jsx", ".mjs", ".cjs"] -const TS_REPLACEMENTS: Record = { - ".js": ".ts", - ".jsx": ".tsx", - ".mjs": ".mts", - ".cjs": ".cts", -} - -const resolveJsToTs: mod.ResolveHookSync = (request, context, next) => { - // Only rewrite relative imports - if (!request.startsWith(".")) return next(request, context) - - const ext = JS_EXTENSIONS.find((e) => request.endsWith(e)) - if (!ext) return next(request, context) - - // Try the original .js first - try { - return next(request, context) - } catch { - // Fall through to .ts attempt - } - - const tsRequest = request.slice(0, -ext.length) + TS_REPLACEMENTS[ext] - return next(tsRequest, context) -} - -setup() diff --git a/packages/core/src/renderables/Code.test.ts b/packages/core/src/renderables/Code.test.ts index f0081fa90..cd6b3863b 100644 --- a/packages/core/src/renderables/Code.test.ts +++ b/packages/core/src/renderables/Code.test.ts @@ -1,4 +1,5 @@ import { test, expect, beforeEach, afterEach } from "bun:test" +import { sleep } from "../compat/runtime.js" import { CodeRenderable } from "./Code.js" import { SyntaxStyle } from "../syntax-style.js" import { RGBA } from "../lib/RGBA.js" @@ -1109,14 +1110,14 @@ test("CodeRenderable - streaming mode with drawUnstyledText=false waits for new currentRenderer.root.add(codeRenderable) currentRenderer.start() - await Bun.sleep(30) + await sleep(30) expect(codeRenderable.plainText).toBe("const initial = 'hello';") codeRenderable.content = "const updated = 'world';" expect(codeRenderable.plainText).toBe("const initial = 'hello';") - await Bun.sleep(30) + await sleep(30) expect(codeRenderable.plainText).toBe("const updated = 'world';") @@ -2046,7 +2047,7 @@ test("CodeRenderable - streaming with drawUnstyledText=false falls back to unsty currentRenderer.root.add(codeRenderable) currentRenderer.start() - await Bun.sleep(30) + await sleep(30) mockClient.highlightOnce = async () => { throw new Error("Highlighting failed") @@ -2054,7 +2055,7 @@ test("CodeRenderable - streaming with drawUnstyledText=false falls back to unsty codeRenderable.content = "const updated = 'world';" - await Bun.sleep(30) + await sleep(30) expect(codeRenderable.plainText).toBe("const updated = 'world';") diff --git a/packages/core/src/renderables/LineNumberRenderable.ts b/packages/core/src/renderables/LineNumberRenderable.ts index 0b479ea0c..36dbf33ed 100644 --- a/packages/core/src/renderables/LineNumberRenderable.ts +++ b/packages/core/src/renderables/LineNumberRenderable.ts @@ -1,5 +1,6 @@ import { Renderable, type RenderableOptions } from "../Renderable.js" import { OptimizedBuffer } from "../buffer.js" +import { stringWidth } from "../compat/runtime.js" import type { RenderContext, LineInfoProvider } from "../types.js" import { RGBA, parseColor } from "../lib/RGBA.js" import { MeasureMode } from "yoga-layout" @@ -158,11 +159,11 @@ class GutterRenderable extends Renderable { for (const sign of this._lineSigns.values()) { if (sign.before) { - const width = Bun.stringWidth(sign.before) + const width = stringWidth(sign.before) this._maxBeforeWidth = Math.max(this._maxBeforeWidth, width) } if (sign.after) { - const width = Bun.stringWidth(sign.after) + const width = stringWidth(sign.after) this._maxAfterWidth = Math.max(this._maxAfterWidth, width) } } @@ -302,7 +303,7 @@ class GutterRenderable extends Renderable { // Draw 'before' sign if present const sign = this._lineSigns.get(logicalLine) if (sign?.before) { - const beforeWidth = Bun.stringWidth(sign.before) + const beforeWidth = stringWidth(sign.before) // Pad to max before width for alignment const padding = this._maxBeforeWidth - beforeWidth currentX += padding diff --git a/packages/core/src/renderables/ScrollBar.ts b/packages/core/src/renderables/ScrollBar.ts index 4a54efd7c..b70d912e2 100644 --- a/packages/core/src/renderables/ScrollBar.ts +++ b/packages/core/src/renderables/ScrollBar.ts @@ -1,4 +1,5 @@ import type { OptimizedBuffer } from "../buffer.js" +import { stringWidth } from "../compat/runtime.js" import { parseColor, RGBA, type ColorInput } from "../lib/index.js" import type { KeyEvent } from "../lib/KeyHandler.js" import { Renderable, type RenderableOptions } from "../Renderable.js" @@ -344,7 +345,7 @@ export class ArrowRenderable extends Renderable { } if (!options.width) { - this.width = Bun.stringWidth(this.getArrowChar()) + this.width = stringWidth(this.getArrowChar()) } } diff --git a/packages/core/src/renderables/__tests__/LineNumberRenderable.test.ts b/packages/core/src/renderables/__tests__/LineNumberRenderable.test.ts index 079e1b8b9..57cd54c63 100644 --- a/packages/core/src/renderables/__tests__/LineNumberRenderable.test.ts +++ b/packages/core/src/renderables/__tests__/LineNumberRenderable.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect } from "bun:test" +import { sleep } from "../../compat/runtime.js" import { createTestRenderer } from "../../testing/test-renderer.js" import { TextBufferRenderable } from "../TextBufferRenderable.js" import { LineNumberRenderable } from "../LineNumberRenderable.js" @@ -1185,9 +1186,9 @@ describe("LineNumberRenderable", () => { // Wait for render and highlighting await renderOnce() // Give highlighting time to complete (increased for CI) - await Bun.sleep(1000) + await sleep(1000) await renderOnce() - await Bun.sleep(100) + await sleep(100) await renderOnce() frame = captureCharFrame() @@ -1238,7 +1239,7 @@ describe("LineNumberRenderable", () => { // First render await renderOnce() - await Bun.sleep(50) + await sleep(50) await renderOnce() let frame = captureCharFrame() @@ -1252,7 +1253,7 @@ describe("LineNumberRenderable", () => { codeRenderable.content = "line 1\nline 2\nline 3\nline 4\nline 5" await renderOnce() - await Bun.sleep(50) + await sleep(50) await renderOnce() frame = captureCharFrame() @@ -1312,7 +1313,7 @@ describe("LineNumberRenderable", () => { codeRenderable.filetype = "typescript" await renderOnce() - await Bun.sleep(100) + await sleep(100) await renderOnce() frame = captureCharFrame() diff --git a/packages/core/src/renderables/__tests__/Markdown.code-colors.test.ts b/packages/core/src/renderables/__tests__/Markdown.code-colors.test.ts index efc148381..ca4cb8769 100644 --- a/packages/core/src/renderables/__tests__/Markdown.code-colors.test.ts +++ b/packages/core/src/renderables/__tests__/Markdown.code-colors.test.ts @@ -1,4 +1,5 @@ import { test, expect, beforeEach, afterEach } from "bun:test" +import { sleep } from "../../compat/runtime.js" import { MarkdownRenderable, type MarkdownOptions } from "../Markdown.js" import { CodeRenderable } from "../Code.js" import { SyntaxStyle } from "../../syntax-style.js" @@ -100,7 +101,7 @@ test("unsupported fenced code blocks keep inherited markdown fg/bg after highlig expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const codeBlock = md._blockStates[0]?.renderable as CodeRenderable @@ -130,7 +131,7 @@ test("fenced tsx code blocks normalize the language before highlighting", async expect(mockTreeSitterClient.highlightCalls[0]?.filetype).toBe("typescriptreact") mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() }) @@ -152,7 +153,7 @@ test("updating fenced code blocks reapplies normalized filetypes", async () => { expect(codeBlock.filetype).toBe("javascriptreact") mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() md.content = "```tsx\nconst view =
Hello
\n```" @@ -163,7 +164,7 @@ test("updating fenced code blocks reapplies normalized filetypes", async () => { expect(mockTreeSitterClient.highlightCalls.at(-1)?.filetype).toBe("typescriptreact") mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() }) @@ -220,7 +221,7 @@ test("updating markdown fg/bg rerenders markdown fallback renderables", async () renderer.root.add(md) await renderer.idle() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const paragraphBlock = md._blockStates[0]?.renderable as CodeRenderable @@ -232,7 +233,7 @@ test("updating markdown fg/bg rerenders markdown fallback renderables", async () md.bg = nextBg renderer.requestRender() await renderer.idle() - await Bun.sleep(10) + await sleep(10) await renderer.idle() expect(md._blockStates[0]?.renderable).toBe(paragraphBlock) diff --git a/packages/core/src/renderables/__tests__/Markdown.test.ts b/packages/core/src/renderables/__tests__/Markdown.test.ts index c3dd32c12..9237ab8d0 100644 --- a/packages/core/src/renderables/__tests__/Markdown.test.ts +++ b/packages/core/src/renderables/__tests__/Markdown.test.ts @@ -1,4 +1,5 @@ import { test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test" +import { sleep } from "../../compat/runtime.js" import { MarkdownRenderable, type MarkdownOptions } from "../Markdown.js" import { CodeRenderable } from "../Code.js" import { TextRenderable } from "../Text.js" @@ -74,7 +75,7 @@ async function renderMarkdownRenderable(md: MarkdownRenderable, timeoutMs: numbe await renderOnce() while (hasPendingMarkdownParagraphHighlights() && Date.now() - startedAt < timeoutMs) { - await Bun.sleep(10) + await sleep(10) await renderOnce() } @@ -780,7 +781,7 @@ test("code block concealment is disabled by default", async () => { expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const frame = captureFrame() @@ -807,7 +808,7 @@ test("code block concealment can be enabled with concealCode", async () => { expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const frame = captureFrame() @@ -835,7 +836,7 @@ test("toggling concealCode updates existing code block renderables", async () => expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const frameBefore = captureFrame() @@ -847,7 +848,7 @@ test("toggling concealCode updates existing code block renderables", async () => expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() const frameAfter = captureFrame() @@ -1455,7 +1456,7 @@ test("streaming code blocks with concealCode=true do not flash unconcealed markd expect(mockTreeSitterClient.isHighlighting()).toBe(true) mockTreeSitterClient.resolveAllHighlightOnce() - await Bun.sleep(10) + await sleep(10) await renderer.idle() recorder.stop() diff --git a/packages/core/src/renderables/__tests__/Textarea.buffer.test.ts b/packages/core/src/renderables/__tests__/Textarea.buffer.test.ts index 0807446ee..bbd94fce7 100644 --- a/packages/core/src/renderables/__tests__/Textarea.buffer.test.ts +++ b/packages/core/src/renderables/__tests__/Textarea.buffer.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, beforeEach, afterEach } from "bun:test" +import { stringWidth } from "../../compat/runtime.js" import { createTestRenderer, type TestRenderer, type MockInput } from "../../testing/test-renderer.js" import { createTextareaRenderable } from "./renderable-test-utils.js" @@ -464,7 +465,7 @@ describe("Textarea - Buffer Tests", () => { expect(visualCursor!.logicalCol).toBe(3) }) - it("should set cursor to end of content using cursorOffset setter and Bun.stringWidth", async () => { + it("should set cursor to end of content using cursorOffset setter and stringWidth", async () => { const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, { initialValue: "", width: 40, @@ -475,11 +476,11 @@ describe("Textarea - Buffer Tests", () => { const content = "Hello World" editor.setText(content) - editor.cursorOffset = Bun.stringWidth(content) + editor.cursorOffset = stringWidth(content) const visualCursor = editor.visualCursor expect(visualCursor).not.toBe(null) - expect(visualCursor!.offset).toBe(Bun.stringWidth(content)) + expect(visualCursor!.offset).toBe(stringWidth(content)) expect(visualCursor!.logicalRow).toBe(0) expect(visualCursor!.logicalCol).toBe(content.length) expect(visualCursor!.visualCol).toBe(content.length) diff --git a/packages/core/src/renderables/__tests__/Textarea.selection.test.ts b/packages/core/src/renderables/__tests__/Textarea.selection.test.ts index 04073ef3c..4abc1b58b 100644 --- a/packages/core/src/renderables/__tests__/Textarea.selection.test.ts +++ b/packages/core/src/renderables/__tests__/Textarea.selection.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, beforeEach, afterEach } from "bun:test" +import { sleep } from "../../compat/runtime.js" import { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from "../../testing/test-renderer.js" import { createTextareaRenderable } from "./renderable-test-utils.js" import { RGBA } from "../../lib/RGBA.js" @@ -1371,7 +1372,7 @@ describe("Textarea - Selection Tests", () => { // Scroll up with mouse wheel await currentMouse.scroll(editor.x, editor.y + 1, "up") - await Bun.sleep(100) + await sleep(100) const selectionAfter = editor.getSelection() const selectedTextAfter = editor.getSelectedText() diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 8e5c9946a..cf211f8ee 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -10,7 +10,8 @@ import { type WidthMethod, } from "./types.js" import { RGBA, parseColor, type ColorInput } from "./lib/RGBA.js" -import type { Pointer } from "bun:ffi" +import type { Pointer } from "./compat/ffi.js" +import { sleep } from "./compat/runtime.js" import { OptimizedBuffer } from "./buffer.js" import { resolveRenderLib, type RenderLib } from "./zig.js" import { TerminalConsole, type ConsoleOptions, capture } from "./console.js" @@ -659,7 +660,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { private exitHandler: () => void = (() => { this.destroy() if (env.OTUI_DUMP_CAPTURES) { - Bun.sleep(100).then(() => { + sleep(100).then(() => { this.dumpOutputCache("=== CAPTURED OUTPUT ===\n") }) } diff --git a/packages/core/src/runtime-plugin.ts b/packages/core/src/runtime-plugin.ts index 9f5f62c35..5f8fe3ac1 100644 --- a/packages/core/src/runtime-plugin.ts +++ b/packages/core/src/runtime-plugin.ts @@ -37,7 +37,6 @@ import { existsSync, readFileSync, realpathSync } from "node:fs" import { basename, dirname, isAbsolute, join } from "node:path" import { fileURLToPath } from "node:url" -import { type BunPlugin } from "bun" import * as coreRuntime from "./index.js" export type RuntimeModuleExports = Record @@ -60,6 +59,11 @@ export interface CreateRuntimePluginOptions { rewrite?: RuntimePluginRewriteOptions } +export interface BunPlugin { + name: string + setup(build: any): void | Promise +} + const CORE_RUNTIME_SPECIFIER = "@opentui/core" const CORE_TESTING_RUNTIME_SPECIFIER = "@opentui/core/testing" const RUNTIME_MODULE_PREFIX = "opentui:runtime-module:" @@ -466,7 +470,7 @@ export function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): Bun throw new Error(`Unable to determine runtime loader for path: ${args.path}`) } - const contents = await Bun.file(loadedPath).text() + const contents = readFileSync(loadedPath, "utf8") const runtimeRewrittenContents = shouldRewriteRuntimeSpecifiers ? rewriteRuntimeSpecifiers(contents, runtimeModuleIdsBySpecifier) : contents diff --git a/packages/core/src/syntax-style.ts b/packages/core/src/syntax-style.ts index 3382aba76..611c60b91 100644 --- a/packages/core/src/syntax-style.ts +++ b/packages/core/src/syntax-style.ts @@ -1,6 +1,6 @@ import { RGBA, parseColor, type ColorInput } from "./lib/RGBA.js" import { resolveRenderLib, type RenderLib } from "./zig.js" -import { type Pointer } from "bun:ffi" +import { type Pointer } from "./compat/ffi.js" import { createTextAttributes } from "./utils.js" export interface StyleDefinition { diff --git a/packages/core/src/testing/test-recorder.test.ts b/packages/core/src/testing/test-recorder.test.ts index 7ffa3383d..c534ba785 100644 --- a/packages/core/src/testing/test-recorder.test.ts +++ b/packages/core/src/testing/test-recorder.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test" +import { sleep } from "../compat/runtime.js" import { createTestRenderer, type TestRenderer } from "./test-renderer.js" import { TestRecorder } from "./test-recorder.js" import { TextRenderable } from "../renderables/Text.js" @@ -42,7 +43,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Hello World" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) expect(recorder.recordedFrames.length).toBe(1) @@ -57,7 +58,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Test Content" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorder.recordedFrames expect(frames.length).toBe(1) @@ -71,7 +72,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Frame Metadata" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorder.recordedFrames expect(frames.length).toBe(1) @@ -86,7 +87,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Multiple Frames" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) await renderOnce() await renderOnce() @@ -105,13 +106,13 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Initial" }) renderer.root.add(text) - await Bun.sleep(10) + await sleep(10) text.content = "Changed" - await Bun.sleep(10) + await sleep(10) recorder.stop() - // NOTE: Should this fail, make sure the Bun.sleeps are in sync with maxFps of the renderer + // NOTE: Should this fail, make sure the sleeps are in sync with maxFps of the renderer const frame1 = recorder.recordedFrames[0].frame const frame2 = recorder.recordedFrames[1].frame @@ -123,7 +124,7 @@ describe("TestRecorder", () => { test("should not record when not started", async () => { const text = new TextRenderable(renderer, { content: "Not Recording" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) expect(recorder.recordedFrames.length).toBe(0) }) @@ -133,7 +134,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Stopped" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) expect(recorder.recordedFrames.length).toBe(1) @@ -147,7 +148,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Clear Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) await renderOnce() @@ -163,7 +164,7 @@ describe("TestRecorder", () => { recorder.rec() renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) recorder.stop() expect(recorder.recordedFrames.length).toBe(1) @@ -181,7 +182,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Duplicate Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) recorder.stop() @@ -193,7 +194,7 @@ describe("TestRecorder", () => { recorder.rec() renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) recorder.stop() recorder.clear() @@ -228,7 +229,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Copy Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames1 = recorder.recordedFrames const frames2 = recorder.recordedFrames @@ -256,7 +257,7 @@ describe("TestRecorder", () => { const text2 = new TextRenderable(renderer, { content: "Line 2" }) renderer.root.add(text1) renderer.root.add(text2) - await Bun.sleep(1) + await sleep(1) const frame = recorder.recordedFrames[0].frame expect(frame).toContain("Line 1") @@ -270,7 +271,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Rapid Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) for (let i = 0; i < 4; i++) { await renderOnce() @@ -287,7 +288,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Buffer Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorderWithFg.recordedFrames expect(frames.length).toBe(1) @@ -305,7 +306,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Buffer Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorderWithBg.recordedFrames expect(frames.length).toBe(1) @@ -323,7 +324,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Buffer Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorderWithAttrs.recordedFrames expect(frames.length).toBe(1) @@ -343,7 +344,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Buffer Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorderWithAll.recordedFrames expect(frames.length).toBe(1) @@ -360,7 +361,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "No Buffer Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorder.recordedFrames expect(frames.length).toBe(1) @@ -375,7 +376,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Copy Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) await renderOnce() @@ -400,7 +401,7 @@ describe("TestRecorder", () => { const text = new TextRenderable(renderer, { content: "Size Test" }) renderer.root.add(text) - await Bun.sleep(1) + await sleep(1) const frames = recorderWithAll.recordedFrames expect(frames.length).toBe(1) diff --git a/packages/core/src/tests/destroy-on-exit.test.ts b/packages/core/src/tests/destroy-on-exit.test.ts index 0eac7f73d..4ca5bd0f8 100644 --- a/packages/core/src/tests/destroy-on-exit.test.ts +++ b/packages/core/src/tests/destroy-on-exit.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it } from "bun:test" +import { spawnSync } from "../compat/testHelpers.js" import { join } from "node:path" const fixturePath = join(import.meta.dirname, "destroy-on-exit.fixture.ts") +const supportedDescribe = process.versions.bun ? describe : describe.skip const runFixture = (code: number, mode: "idle" | "during-render" = "idle") => { - const result = Bun.spawnSync([process.execPath, fixturePath, code.toString(), mode], { + const result = spawnSync([process.execPath, fixturePath, code.toString(), mode], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", @@ -16,7 +18,7 @@ const runFixture = (code: number, mode: "idle" | "during-render" = "idle") => { return { result, stdout } } -describe("destroy on process exit", () => { +supportedDescribe("destroy on process exit", () => { it("it should let applications restore terminal state in an exit handler", () => { const { result, stdout } = runFixture(0) diff --git a/packages/core/src/tests/renderer.clock.test.ts b/packages/core/src/tests/renderer.clock.test.ts index 4e184cb26..fcf27b937 100644 --- a/packages/core/src/tests/renderer.clock.test.ts +++ b/packages/core/src/tests/renderer.clock.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test" +import { sleep } from "../compat/runtime.js" import { SystemClock } from "../lib/clock.js" import { createTestRenderer, type TestRenderer } from "../testing/test-renderer.js" import { ManualClock } from "../testing/manual-clock.js" @@ -76,7 +77,7 @@ test("requestRender() uses SystemClock by default when no clock is injected", as } defaultRenderer.requestRender() - await Bun.sleep(20) + await sleep(20) expect(renderCalled).toBe(true) } finally { diff --git a/packages/core/src/tests/renderer.control.test.ts b/packages/core/src/tests/renderer.control.test.ts index 7d71f4fde..92d882dc5 100644 --- a/packages/core/src/tests/renderer.control.test.ts +++ b/packages/core/src/tests/renderer.control.test.ts @@ -1,4 +1,5 @@ import { test, expect, beforeEach, afterEach } from "bun:test" +import { sleep } from "../compat/runtime.js" import { createTestRenderer, type TestRenderer, type MockInput, type MockMouse } from "../testing/test-renderer.js" import { RendererControlState } from "../renderer.js" import { Renderable } from "../Renderable.js" @@ -149,7 +150,7 @@ test("requestRender() does not trigger when renderer is suspended", async () => test("requestRender() does trigger when renderer is paused", async () => { renderer.start() - await Bun.sleep(20) + await sleep(20) renderer.pause() let renderCalled = false @@ -162,7 +163,7 @@ test("requestRender() does trigger when renderer is paused", async () => { } renderer.requestRender() - await Bun.sleep(20) + await sleep(20) expect(renderCalled).toBe(true) diff --git a/packages/core/src/tests/renderer.mouse.test.ts b/packages/core/src/tests/renderer.mouse.test.ts index 79637df3d..64c935249 100644 --- a/packages/core/src/tests/renderer.mouse.test.ts +++ b/packages/core/src/tests/renderer.mouse.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, test } from "bun:test" +import { sleep } from "../compat/runtime.js" import { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from "../testing.js" import { Renderable, type RenderableOptions } from "../Renderable.js" import type { MouseEvent } from "../renderer.js" @@ -55,7 +56,7 @@ describe("renderer handleMouseData", () => { } renderer.stdin.emit("data", Buffer.from("x")) - await Bun.sleep(10) + await sleep(10) expect(sequences).toContain("x") expect(mouseDown).toBe(false) @@ -73,7 +74,7 @@ describe("renderer handleMouseData", () => { }) renderer.stdin.emit("data", Buffer.from("x")) - await Bun.sleep(10) + await sleep(10) expect(sequences).toContain("x") } finally { @@ -1264,7 +1265,7 @@ describe("renderer handleMouseData split height", () => { const renderOffset = baseHeight - splitHeight const beforeSequences = sequences.length await mockMouse.click(1, Math.max(0, renderOffset - 1)) - await Bun.sleep(10) + await sleep(10) expect(sequences.length).toBeGreaterThan(beforeSequences) } finally { diff --git a/packages/core/src/tests/runtime-plugin-support.test.ts b/packages/core/src/tests/runtime-plugin-support.test.ts index b30127632..db9f9f0b8 100644 --- a/packages/core/src/tests/runtime-plugin-support.test.ts +++ b/packages/core/src/tests/runtime-plugin-support.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test" +import { spawnSync } from "../compat/testHelpers.js" import { join } from "node:path" // Fixtures require `import { plugin } from "bun"` — no Node.js equivalent. @@ -7,7 +8,7 @@ const _describe = process.versions.bun ? describe : describe.skip _describe("runtime plugin support", () => { it("installs exactly once via drop-in module", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/core/src/tests/runtime-plugin.test.ts b/packages/core/src/tests/runtime-plugin.test.ts index 6b12c487d..384fe2ebb 100644 --- a/packages/core/src/tests/runtime-plugin.test.ts +++ b/packages/core/src/tests/runtime-plugin.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test" +import { spawnSync } from "../compat/testHelpers.js" import { join } from "node:path" import * as coreRuntime from "../index.js" import { createRuntimePlugin, runtimeModuleIdForSpecifier } from "../runtime-plugin.js" @@ -199,7 +200,7 @@ describe("runtime plugin", () => { bunIt("resolves runtime modules end-to-end in a subprocess", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -214,7 +215,7 @@ describe("runtime plugin", () => { bunIt("resolves bare imports from external runtime roots", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-resolve-roots.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -229,7 +230,7 @@ describe("runtime plugin", () => { bunIt("rewrites runtime specifiers in node_modules modules by default", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-runtime-specifier.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -244,7 +245,7 @@ describe("runtime plugin", () => { bunIt("rewrites runtime specifiers in node_modules .mjs modules", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-mjs.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -259,7 +260,7 @@ describe("runtime plugin", () => { bunIt("rewrites runtime specifiers across node_modules ESM cycles", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-cycle.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -276,7 +277,7 @@ describe("runtime plugin", () => { bunIt("does not keep stale node_modules package type across plugin instances", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-package-type-cache.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -291,7 +292,7 @@ describe("runtime plugin", () => { bunIt("rewrites bare imports for scoped node_modules package siblings when enabled", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -308,7 +309,7 @@ describe("runtime plugin", () => { bunIt("does not rewrite non-runtime bare imports in node_modules modules by default", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-node-modules-no-bare-rewrite.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -323,7 +324,7 @@ describe("runtime plugin", () => { bunIt("rewrites runtime specifiers when Bun canonicalizes a symlinked import path", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-path-alias.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", @@ -343,7 +344,7 @@ describe("runtime plugin", () => { } const fixturePath = join(import.meta.dirname, "runtime-plugin-windows-file-url.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, "..", ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/core/src/text-buffer-view.ts b/packages/core/src/text-buffer-view.ts index f671f02c4..528bae757 100644 --- a/packages/core/src/text-buffer-view.ts +++ b/packages/core/src/text-buffer-view.ts @@ -1,6 +1,6 @@ import { RGBA } from "./lib/RGBA.js" import { resolveRenderLib, type LineInfo, type RenderLib } from "./zig.js" -import { type Pointer } from "bun:ffi" +import { type Pointer } from "./compat/ffi.js" import type { TextBuffer } from "./text-buffer.js" export class TextBufferView { diff --git a/packages/core/src/text-buffer.ts b/packages/core/src/text-buffer.ts index a75fdd634..c6d44de05 100644 --- a/packages/core/src/text-buffer.ts +++ b/packages/core/src/text-buffer.ts @@ -1,7 +1,7 @@ import type { StyledText } from "./lib/styled-text.js" import { RGBA } from "./lib/RGBA.js" import { resolveRenderLib, type LineInfo, type RenderLib } from "./zig.js" -import { type Pointer } from "bun:ffi" +import { type Pointer } from "./compat/ffi.js" import { type WidthMethod, type Highlight } from "./types.js" import type { SyntaxStyle } from "./syntax-style.js" diff --git a/packages/core/src/zig-structs.ts b/packages/core/src/zig-structs.ts index 36bce84a4..79f99b9b7 100644 --- a/packages/core/src/zig-structs.ts +++ b/packages/core/src/zig-structs.ts @@ -1,5 +1,5 @@ -import { defineStruct, defineEnum } from "bun-ffi-structs" -import { ptr, toArrayBuffer, type Pointer } from "bun:ffi" +import { defineStruct, defineEnum } from "./compat/bun-ffi-structs/index.js" +import { ptr, toArrayBuffer, type Pointer } from "./compat/ffi.js" import { RGBA } from "./lib/RGBA.js" const rgbaPackTransform = (rgba?: RGBA) => (rgba ? ptr(rgba.buffer) : null) diff --git a/packages/core/src/zig.ts b/packages/core/src/zig.ts index d30d49177..f33e82242 100644 --- a/packages/core/src/zig.ts +++ b/packages/core/src/zig.ts @@ -1,4 +1,4 @@ -import { dlopen, JSCallback, ptr, toArrayBuffer, type Pointer } from "bun:ffi" +import { JSCallback, dlopen, ptr, toArrayBuffer, type Pointer } from "./compat/ffi.js" import { EventEmitter } from "events" import { existsSync, writeFileSync } from "fs" import { @@ -16,6 +16,7 @@ import { OptimizedBuffer } from "./buffer.js" import { isBunfsPath } from "./lib/bunfs.js" import { env, registerEnvVar } from "./lib/env.js" import { RGBA } from "./lib/RGBA.js" +import { writeFile } from "./compat/runtime.js" import { TextBuffer } from "./text-buffer.js" import type { AllocatorStats, @@ -1329,7 +1330,9 @@ function convertToDebugSymbols>(symbols: T): T { const now = new Date() const timestamp = now.toISOString().replace(/[:.]/g, "-").replace(/T/, "_").split("Z")[0] const traceFilePath = `ffi_otui_trace_${timestamp}.log` - Bun.write(traceFilePath, output) + void writeFile(traceFilePath, output).catch((error) => { + console.error("Failed to write FFI trace file:", error) + }) } catch (e) { console.error("Failed to write FFI trace file:", e) } diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index bbb19a520..be66851b4 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -1,39 +1,20 @@ /** * Vitest is used to run tests under Node.js. - * bun:test imports are replaced with Vitest in nodejs/compat.ts. + * Tests import `bun:test`, which is aliased to the Vitest adapter here. */ import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" import { defineConfig } from "vitest/config" export default defineConfig({ + resolve: { + alias: { + "bun:test": fileURLToPath(new URL("./src/compat/test.ts", import.meta.url)), + }, + }, test: { - // globalSetup: "./src/nodejs/compat.ts", environment: "node", - isolate: false, - // alias: { - // "bun:ffi": "./src/nodejs/bunModules/ffi.ts", - // "bun:test": "./src/nodejs/bunModules/test.ts", - // }, - execArgv: [ - // "--experimental-transform-types", - // "--import=tsx", - // "--import=@swc-node/register/esm", - "--no-experimental-strip-types", - "--experimental-transform-types", - // "--import=esbuild-register/loader", - "--import=./src/nodejs/compat.ts", - ], - experimental: { - // Disable Vite bundling entirely so we exercise the nodejs/compat.ts shim. - viteModuleRunner: false, - // Disable vitest's native Node loader hooks — they call - // module.stripTypeScriptTypes() (strip-only mode) which doesn't support - // const enum or parameter properties. We rely on Node.js's - // --experimental-transform-types instead. - nodeLoader: false, - }, - // Create independent snapshots from bun:test resolveSnapshotPath: (testPath, ext) => join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs${ext}`), }, diff --git a/packages/react/tests/__snapshots__/layout.test.tsx.snap b/packages/react/tests/__snapshots__/layout.test.tsx.snap index a36b616f8..61512bff7 100644 --- a/packages/react/tests/__snapshots__/layout.test.tsx.snap +++ b/packages/react/tests/__snapshots__/layout.test.tsx.snap @@ -191,5 +191,1165 @@ exports[`React Renderer | Layout Tests Empty and Edge Cases should handle very s "Hi +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render basic text content correctly 1`] = ` +" + + + Hello World + +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render multiline text content correctly 1`] = ` +" + Line 1: Hello + Line 2: World + Line 3: Testing + Line 4: Multiline +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render text with graphemes/emojis correctly 1`] = ` +" + +Hello 🌍 World 👋 + Test 🚀 Emoji + +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render TextNode text composition correctly 1`] = ` +"First Second Third + + + + +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render text positioning correctly 1`] = ` +"Top + + Mid + + Bot +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render empty buffer correctly 1`] = ` +" + + + + +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render text with character wrapping correctly 1`] = ` +"This is a very +long text that +should wrap to +multiple lines +when wrap is en +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render wrapped text with different content 1`] = ` +" + ABCDEFGHIJ + KLMNOPQRST + UVWXYZ abc + defghijklm +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render wrapped text with emojis and graphemes 1`] = ` +" Hello 🌍 Wor + ld 👋 This i + s a test wit + h emojis 🚀 + that should +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render wrapped multiline text correctly 1`] = ` +" +First li +ne with +long con +tent +" +`; + +exports[`TextRenderable Selection Text Content Snapshots should render text with tab indicator correctly 1`] = ` +"Line 1→ Tabbed +Line 2→ → Double tab + + + +" +`; + +exports[`TextRenderable Selection Text Node Dimension Updates should update dimensions and reposition subsequent elements when text nodes expand 1`] = ` +"Short +Second text + + + +" +`; + +exports[`TextRenderable Selection Text Node Dimension Updates should update dimensions and reposition subsequent elements when text nodes expand 2`] = ` +"Short text that will + definitely wrap +Second text + + +" +`; + +exports[`TextRenderable Selection Text Node Dimension Updates should handle multiple text node updates with complex layout changes 1`] = ` +"First part +Middle text +Bottom text + + + + + + + +" +`; + +exports[`TextRenderable Selection Text Node Dimension Updates should handle multiple text node updates with complex layout changes 2`] = ` +"First of +a +sentence +partthat +will wrap +Middle text +Bottom text + + + +" +`; + +exports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when width is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when height is set via setter in column layout with text 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when minWidth is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when minHeight is set via setter in column layout with text 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when width is set from undefined via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + + + + + + +" +`; + +exports[`TextRenderable Selection Absolute Positioned Box with Text should render text in absolute positioned box with padding and borders correctly 1`] = ` +" + + │ │ + │ │ + │ Important Notification │ + │ │ + │ │ + │ This is a longer message that should wrap properly within + │ │ + │ │ + + + + + + + + + + +" +`; + +exports[`TextRenderable Selection Absolute Positioned Box with Text should render text fully visible in absolute positioned box at various positions 1`] = ` +" + ┌──────────────────────────────────────┐ + │ Error: File not found in the │ + │ specified directory path │ + └──────────────────────────────────────┘ + + + + + + + + + + + + + + + + ─────────────────────────────────── + Success: Operation completed + successfully! + ─────────────────────────────────── + +" +`; + +exports[`TextRenderable Selection Absolute Positioned Box with Text should handle width:100% text in absolute positioned box with constrained maxWidth 1`] = ` +" + + + + + + + This is an extremely long piece of text + that needs to wrap multiple times within + the constrained width of the absolutely + positioned container box with significant + padding on all sides. + + + +" +`; + +exports[`TextRenderable Selection Absolute Positioned Box with Text should render multiple text elements in absolute positioned box with proper spacing 1`] = ` +" + + + ┌───────────────────────────────────────────┐ + │ │ + │ System Update │ + │ │ + │ A new version is available with bug │ + │ fixes and performance improvements. │ + │ │ + │ Click to install │ + │ │ + └───────────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`TextRenderable Selection Word Wrapping should wrap at word boundaries when using word mode 1`] = ` +"The quick +brown fox +jumps over the +lazy dog + +" +`; + +exports[`TextRenderable Selection Word Wrapping should wrap at character boundaries when using char mode 1`] = ` +"The quick brown + fox jumps over + the lazy dog + + +" +`; + +exports[`TextRenderable Selection Word Wrapping should handle word wrapping with punctuation 1`] = ` +"Hello, +World. +Test- +Example/ +Path +" +`; + +exports[`TextRenderable Selection Word Wrapping should handle word wrapping with hyphens and dashes 1`] = ` +"self- +contained +multi-line +text- +wrapping +" +`; + +exports[`TextRenderable Selection Word Wrapping should dynamically change wrap mode 1`] = ` +"The quick +brown fox +jumps + + +" +`; + +exports[`TextRenderable Selection Word Wrapping should handle long words that exceed wrap width in word mode 1`] = ` +"ABCDEFGHIJ +KLMNOPQRST +UVWXYZ + + +" +`; + +exports[`TextRenderable Selection Word Wrapping should preserve empty lines with word wrapping 1`] = ` +"First +line + +Third +line +" +`; + +exports[`TextRenderable Selection Word Wrapping should handle word wrapping with single character words 1`] = ` +"a b c d +e f g h +i j k l +m n o p + +" +`; + +exports[`TextRenderable Selection Word Wrapping should compare char vs word wrapping with same content 1`] = ` +"Hello +wonderful +world of +text +wrapping +" +`; + +exports[`TextRenderable Selection Word Wrapping should correctly wrap text when updating content via text.content 1`] = ` +"Short text + + + + +" +`; + +exports[`TextRenderable Selection Word Wrapping should correctly wrap text when updating content via text.content 2`] = ` +"This is a much +longer text that +should definitely +wrap to multiple +lines +" +`; + +exports[`TextTableRenderable renders a basic table with styled cell chunks: basic table 1`] = ` +" + ┌─────┬──────┬───────────────────┐ + │Name │Status│Notes │ + ├─────┼──────┼───────────────────┤ + │Alpha│OK │All systems nominal│ + ├─────┼──────┼───────────────────┤ + │Bravo│WARN │Pending checks │ + └─────┴──────┴───────────────────┘ + + + + + + + + +" +`; + +exports[`TextTableRenderable wraps content and fits columns when width is constrained: wrapped constrained width 1`] = ` +"┌──┬─────────────────────────────┐ +│ID│Description │ +├──┼─────────────────────────────┤ +│1 │This is a long sentence that │ +│ │should wrap across multiple │ +│ │visual lines │ +├──┼─────────────────────────────┤ +│2 │Short │ +└──┴─────────────────────────────┘ + + + + + + + +" +`; + +exports[`TextTableRenderable balanced fitter keeps constrained columns visually closer: fitter proportional constrained 1`] = ` +"┌────┬─────────────┬─────────┬─────────┬───────┬─────────┐ +│Prov│Compute │Storage │Pricing │Regions│Use Cases│ +│ider│Services │Solutions│Model │ │ │ +├────┼─────────────┼─────────┼─────────┼───────┼─────────┤ +│Amaz│EC2 │S3 tiers,│Pay as │Global │Enterpris│ +│on W│instances │ EBS, │you go, │regions│e migrati│ +│eb S│with │EFS, and │reserved │ and ma│on, analy│ +│ervi│extensive │archive │terms, │ny edge│tics, ML,│ +│ces │options for │classes │and │ locati│ and back│ +│ │general, │for long │discounte│ons │end servi│ +│ │memory, and │retention│d spot ca│ │ces │ +│ │accelerated │ │pacity │ │ │ +│ │workloads │ │ │ │ │ +└────┴─────────────┴─────────┴─────────┴───────┴─────────┘ + + +" +`; + +exports[`TextTableRenderable balanced fitter keeps constrained columns visually closer: fitter balanced constrained 1`] = ` +"┌────────┬────────┬─────────┬─────────┬────────┬─────────┐ +│Provider│Compute │Storage │Pricing │Regions │Use Cases│ +│ │Services│Solutions│Model │ │ │ +├────────┼────────┼─────────┼─────────┼────────┼─────────┤ +│Amazon │EC2 │S3 tiers,│Pay as │Global │Enterpris│ +│Web │instance│ EBS, │you go, │regions │e migrati│ +│Services│s with e│EFS, and │reserved │and │on, analy│ +│ │xtensive│archive │terms, │many │tics, ML,│ +│ │ options│classes │and │edge │ and back│ +│ │ for gen│for long │discounte│location│end servi│ +│ │eral, me│retention│d spot ca│s │ces │ +│ │mory, an│ │pacity │ │ │ +│ │d accele│ │ │ │ │ +│ │rated wo│ │ │ │ │ +│ │rkloads │ │ │ │ │ +└────────┴────────┴─────────┴─────────┴────────┴─────────┘ +" +`; + +exports[`TextTableRenderable rebuilds table when content setter is used: content setter update 1`] = ` +"┌─────┬───────┐ +│Col 1│Col 2 │ +├─────┼───────┤ +│row-1│updated│ +├─────┼───────┤ +│row-2│active │ +└─────┴───────┘ + + + + + + + + + +" +`; + +exports[`TextTableRenderable keeps borders aligned with CJK and emoji content: unicode border alignment 1`] = ` +"┌──────┬──────────────┐ +│Locale│Sample │ +├──────┼──────────────┤ +│ja-JP │東京で寿司 🍣 │ +├──────┼──────────────┤ +│zh-CN │你好世界 🚀 │ +├──────┼──────────────┤ +│ko-KR │한글 테스트 😄│ +└──────┴──────────────┘ + + + + + + + +" +`; + +exports[`TextTableRenderable wraps CJK and emoji without grapheme duplication: unicode wrapping 1`] = ` +"┌───┬────────────────────────┐ +│Ite│Details │ +│m │ │ +├───┼────────────────────────┤ +│mix│東京界 🌍 emoji │ +│ed │wrapping continues │ +│ │across lines for width │ +│ │checks │ +├───┼────────────────────────┤ +│emo│Faces 😀😃😄 should │ +│ji │remain stable │ +└───┴────────────────────────┘ + + + + +" +`; + +exports[`TextTableRenderable keeps full wrapped table layouts after a wide-to-narrow demo-style resize: demo resize expected primary table 1`] = ` +"┌─────────────────────────┬─────────────┬────────────────────────────┐ +│Task │Owner │ETA │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Wrap regression in │core │done after validating none, │ +│operational status │platform and │word, and char wrap modes │ +│dashboard with dynamic │runtime │across narrow, medium, wide,│ +│row heights and │reliability │ and ultra-wide terminal │ +│constrained layout │squad │widths │ +│validation │ │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Unicode layout │render │in review with follow-up │ +│stabilization for mixed │pipeline │checks for border style │ +│Latin, punctuation, │maintainers │transitions, cell padding │ +│symbols, and long │with │variants, and selection │ +│identifiers in adjacent │fallback │range consistency │ +│columns │shaping │ │ +│ │support │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Snapshot pass for table │qa │today pending final │ +│rendering in content │automation │baseline updates for │ +│mode and full mode with │and visual │oversized fixtures that │ +│heavy and double border │diff triage │intentionally stress │ +│combinations │group │wrapping behavior on high- │ +│ │ │resolution terminals │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Document edge cases │developer │planned for this sprint │ +│where long tokens │experience │once final reproducible │ +│without spaces force │and docs │examples are captured and │ +│char wrapping and reveal │tooling │linked to regression │ +│per-cell clipping │ │tracking tickets │ +│regressions │ │ │ +├─────────────────────────┼─────────────┼────────────────────────────┤ +│Performance sweep of │runtime │scheduled after review, │ +│wrapping algorithm under │performance │with benchmark runs on │ +│large datasets to │task force │laptop and desktop │ +│confirm stable frame │ │terminals at 200-plus │ +│times during rapid key │ │column widths │ +│toggling │ │ │ +└─────────────────────────┴─────────────┴────────────────────────────┘ +" +`; + +exports[`TextTableRenderable keeps full wrapped table layouts after a wide-to-narrow demo-style resize: demo resize expected unicode table 1`] = ` +"┌─────┬──────────────────────────────────────────────────────────────┐ +│Colum│Wrapped Text │ +│n │ │ +├─────┼──────────────────────────────────────────────────────────────┤ +│mixed│CJK and emoji wrapping stress case: こんにちは世界 and │ +│-lang│안녕하세요 세계 and 你好,世界 followed by long English prose │ +│uages│that keeps flowing to test whether each cell wraps naturally │ +│ │even when the terminal is extremely wide and the row still │ +│ │needs multiple visual lines for readability 🌍🚀 │ +├─────┼──────────────────────────────────────────────────────────────┤ +│emoji│Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version │ +│-and-│tags like release-candidate-build-2026-02-very-long-token- │ +│symbo│without-breaks to ensure char wrapping remains stable and no │ +│ls │glyph alignment issues appear at column boundaries │ +├─────┼──────────────────────────────────────────────────────────────┤ +│long-│長文の日本語テキストと中文段落和한국어문장을連続して配置し、 │ +│cjk- │その後に additional English context describing renderer │ +│phras│behavior, border intersection handling, and selection │ +│e │extraction so that this single cell remains a reliable │ +│ │wrapping torture test. │ +├─────┼──────────────────────────────────────────────────────────────┤ +│mixed│Wrap behavior with punctuation-heavy content: [alpha]{beta}( │ +│-punc│gamma)|epsilon| then repeated fragments, commas, │ +│tuati│semicolons, and slashes to verify token boundaries do not │ +│on │break border drawing logic or spacing consistency in │ +│ │neighboring columns. │ +└─────┴──────────────────────────────────────────────────────────────┘ +" +`; + +exports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box at top-left: absolute positioned box at top-left 1`] = ` +"┌─────────────┐ +│Top Left │ +│ │ +│ │ +└─────────────┘ + + + + + + + + + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box at bottom-right using right/bottom: absolute positioned box at bottom-right 1`] = ` +" + + + + + + + + + + + + + + + ┌─────────────┐ + │Bottom Right │ + │ │ + │ │ + └─────────────┘ +" +`; + +exports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box centered with left/top: absolute positioned box centered 1`] = ` +" + + + + + ┌──────────────────┐ + │Centered │ + │ │ + │ │ + │ │ + │ │ + │ │ + └──────────────────┘ + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child inside absolute parent - basic: nested absolute - child inside parent at left/top 1`] = ` +" + + + ┌────────────────────────────┐ + │ │ + │ ┌──────────┐ │ + │ │Nested │ │ + │ │ │ │ + │ └──────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────┘ + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at bottom:0 inside absolute parent (issue #406 fix): nested absolute - child at bottom:0 of parent 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌─────────────┐ │ + │ │At Bottom │ │ + │ └─────────────┘ │ + └────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at right:0 inside absolute parent: nested absolute - child at right:0 of parent 1`] = ` +" + + ┌─────────────────────────────────┐ + │ │ + │ ┌──────────┐│ + │ │At Right ││ + │ │ ││ + │ └──────────┘│ + │ │ + │ │ + │ │ + │ │ + │ │ + └─────────────────────────────────┘ + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at bottom-right corner inside absolute parent: nested absolute - child at bottom-right corner 1`] = ` +" + ┌────────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌────────────┐ │ + │ │Corner │ │ + │ │ │ │ + │ └────────────┘ │ + │ │ + └────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Nested absolute positioning multiple absolute children inside absolute parent at different positions: nested absolute - four corners inside parent 1`] = ` +" + ┌──────────────────────────────────┐ + │ │ + │ ┌────────┐ ┌────────┐ │ + │ │TL │ │TR │ │ + │ └────────┘ └────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌────────┐ ┌────────┐ │ + │ │BL │ │BR │ │ + │ └────────┘ └────────┘ │ + │ │ + └──────────────────────────────────┘ + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Three-level nesting deeply nested absolute positioning - grandchild at bottom: three-level nested absolute - grandchild at bottom 1`] = ` +" + ┌────────────────────────────────────┐ + │ │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ┌─────────────┐ │ │ + │ │ │Deep │ │ │ + │ │ └─────────────┘ │ │ + │ │ │ │ + │ └──────────────────────────────┘ │ + │ │ + │ │ + └────────────────────────────────────┘ + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Mixed positioning absolute child inside relative parent: absolute child inside relative parent 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌──────────┐ │ + │ │Absolute │ │ + │ │ │ │ + │ └──────────┘ │ + │ │ + └────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Mixed positioning sibling absolute elements at same level: sibling absolute elements overlapping 1`] = ` +"┌─────────────┐ +│Box 1 │ +│ │ +│ │ +│ ┌─────────────┐ +└───────────│Box 2 │ + │ │ + │ │ + │ ┌─────────────┐ + └───────────│Box 3 │ + │ │ + │ │ + │ │ + └─────────────┘ + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with negative coordinates (partially off-screen): absolute box with negative coordinates 1`] = ` +" │ + │ + │ + │ + │ +──────────────┘ + + + + + + + + + + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box extending beyond viewport: absolute box extending beyond viewport 1`] = ` +" + + + + + + + + + + + + + + + ┌───────── + │Overflow + │ + │ + │ +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute child fills parent completely: absolute child fills parent with inset 0 1`] = ` +" + + + ┌────────────────────────────┐ + │╔══════════════════════════╗│ + │║Full ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │║ ║│ + │╚══════════════════════════╝│ + └────────────────────────────┘ + + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with percentage width inside absolute parent: absolute child with percentage width 1`] = ` +" + + ┌──────────────────────────────────────┐ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌─────────────────┐ │ + │ │50% │ │ + │ │ │ │ + │ └─────────────────┘ │ + │ │ + └──────────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with percentage height inside absolute parent: absolute child with percentage height 1`] = ` +" + + ┌────────────────────────────┐ + │ │ + │ ┌─────────────┐ │ + │ │50% H │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ └─────────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────┘ + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute child with conflicting insets (left and right without explicit width): absolute child with left and right insets (no explicit width) 1`] = ` +" + + ┌────────────────────────────────┐ + │ │ + │ │ + │ ┌──────────────────────────┐ │ + │ │Stretch │ │ + │ │ │ │ + │ │ │ │ + │ └──────────────────────────┘ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └────────────────────────────────┘ + + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Edge cases absolute child with conflicting insets (top and bottom without explicit height): absolute child with top and bottom insets (no explicit height) 1`] = ` +" + ┌────────────────────────────┐ + │ │ + │ ┌─────────────┐ │ + │ │VStretch │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ └─────────────┘ │ + │ │ + └────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Complex hierarchies relative parent with absolute child containing absolute grandchild: relative -> absolute -> absolute hierarchy 1`] = ` +" + ┌─────────────────────────────────┐ + │ │ + │ ┌──────────────────────────┐ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ┌──────────┐ │ │ + │ │ │Grand │ │ │ + │ │ │ │ │ │ + │ │ └──────────┘ │ │ + │ │ │ │ + │ └──────────────────────────┘ │ + │ │ + └─────────────────────────────────┘ + + + +" +`; + +exports[`Absolute Positioning - Snapshot Tests Complex hierarchies multiple nested relative and absolute layers: relative -> absolute -> relative -> absolute hierarchy 1`] = ` +"┌────────────────────────────────────┐ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ │ │ +│ │ ┌──────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────┐ │ │ │ +│ │ │ │Deep │ │ │ │ +│ │ │ └────────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────┘ │ │ +│ │ │ │ +│ └──────────────────────────────┘ │ +│ │ +└────────────────────────────────────┘ + + +" +`; + +exports[`ScrollBoxRenderable - Content Visibility scrolls CodeRenderable with LineNumberRenderable using mouse wheel 1`] = ` +" 22 Line 22 + 23 Line 23 + 24 Line 24 + 25 Line 25 + 26 Line 26 + 27 Line 27 + 28 Line 28 + 29 Line 29 + 30 Line 30 + 31 ▄ + + + + + + + + + + + + + + +" +`; + +exports[`Renderable - insertBefore reproduces insertBefore behavior with state change after timeout: insertBefore initial state 1`] = ` +"banana +apple +pear + + +" +`; + +exports[`Renderable - insertBefore reproduces insertBefore behavior with state change after timeout: insertBefore reordered state 1`] = ` +"banana +pear +apple + + " `; diff --git a/packages/react/tests/runtime-plugin-support.fixture.ts b/packages/react/tests/runtime-plugin-support.fixture.ts index bafb342b2..656b82be1 100644 --- a/packages/react/tests/runtime-plugin-support.fixture.ts +++ b/packages/react/tests/runtime-plugin-support.fixture.ts @@ -22,7 +22,7 @@ type FixtureState = typeof globalThis & { } const tempRoot = mkdtempSync(join(tmpdir(), "react-runtime-plugin-support-fixture-")) -const entryPath = join(tempRoot, "entry.ts") +const entryPath = join(tempRoot, "entry.js") const source = [ 'import * as core from "@opentui/core"', @@ -33,7 +33,7 @@ const source = [ 'import * as react from "react"', 'import * as reactJsx from "react/jsx-runtime"', 'import * as reactJsxDev from "react/jsx-dev-runtime"', - "const state = globalThis as { __reactRuntimeHost__?: { core: Record; coreTesting: Record; opentuiReact: Record; opentuiReactJsx: Record; opentuiReactJsxDev: Record; react: Record; reactJsx: Record; reactJsxDev: Record } }", + "const state = globalThis", "const host = state.__reactRuntimeHost__", "const checks = [", " `core=${core.engine === host?.core.engine}`,", diff --git a/scripts/run-node.sh b/scripts/run-node.sh index 0f99076df..4f3d0345d 100755 --- a/scripts/run-node.sh +++ b/scripts/run-node.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash -# Run a TypeScript file with Node.js using the opentui compat shim. +# Run a TypeScript file with Node.js. # Usage: ./run-node.sh src/examples/simple-layout-example.ts set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" exec node \ --experimental-transform-types \ - "--import=${SCRIPT_DIR}/../packages/core/src/nodejs/compat.ts" \ + --import="${SCRIPT_DIR}/../packages/core/src/compat/nodejs/registerResolveJs.ts" \ + --import="${SCRIPT_DIR}/../packages/core/src/compat/nodejs/registerBun.ts" \ "$@" From 9d5c9f25df0eafda6c31502ef5a554e572b358bc Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 20:44:30 -0400 Subject: [PATCH 38/56] move bun-ffi-structs fork into nodejs/ --- packages/core/src/compat/bun-ffi-structs.ts | 10 ++++++++++ .../src/compat/{ => nodejs}/bun-ffi-structs/error.d.ts | 0 .../src/compat/{ => nodejs}/bun-ffi-structs/index.d.ts | 0 .../src/compat/{ => nodejs}/bun-ffi-structs/index.js | 0 .../{ => nodejs}/bun-ffi-structs/structs_ffi.d.ts | 0 .../src/compat/{ => nodejs}/bun-ffi-structs/types.d.ts | 0 packages/core/src/zig-structs.ts | 2 +- 7 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/compat/bun-ffi-structs.ts rename packages/core/src/compat/{ => nodejs}/bun-ffi-structs/error.d.ts (100%) rename packages/core/src/compat/{ => nodejs}/bun-ffi-structs/index.d.ts (100%) rename packages/core/src/compat/{ => nodejs}/bun-ffi-structs/index.js (100%) rename packages/core/src/compat/{ => nodejs}/bun-ffi-structs/structs_ffi.d.ts (100%) rename packages/core/src/compat/{ => nodejs}/bun-ffi-structs/types.d.ts (100%) diff --git a/packages/core/src/compat/bun-ffi-structs.ts b/packages/core/src/compat/bun-ffi-structs.ts new file mode 100644 index 000000000..f41264921 --- /dev/null +++ b/packages/core/src/compat/bun-ffi-structs.ts @@ -0,0 +1,10 @@ +let mod: typeof import("./nodejs/bun-ffi-structs/index.js") + +if (process.versions.bun) { + mod = (await import("bun-ffi-structs")) as any +} else { + mod = await import("./nodejs/bun-ffi-structs/index.js") +} + +export const defineStruct = mod.defineStruct +export const defineEnum = mod.defineEnum diff --git a/packages/core/src/compat/bun-ffi-structs/error.d.ts b/packages/core/src/compat/nodejs/bun-ffi-structs/error.d.ts similarity index 100% rename from packages/core/src/compat/bun-ffi-structs/error.d.ts rename to packages/core/src/compat/nodejs/bun-ffi-structs/error.d.ts diff --git a/packages/core/src/compat/bun-ffi-structs/index.d.ts b/packages/core/src/compat/nodejs/bun-ffi-structs/index.d.ts similarity index 100% rename from packages/core/src/compat/bun-ffi-structs/index.d.ts rename to packages/core/src/compat/nodejs/bun-ffi-structs/index.d.ts diff --git a/packages/core/src/compat/bun-ffi-structs/index.js b/packages/core/src/compat/nodejs/bun-ffi-structs/index.js similarity index 100% rename from packages/core/src/compat/bun-ffi-structs/index.js rename to packages/core/src/compat/nodejs/bun-ffi-structs/index.js diff --git a/packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts b/packages/core/src/compat/nodejs/bun-ffi-structs/structs_ffi.d.ts similarity index 100% rename from packages/core/src/compat/bun-ffi-structs/structs_ffi.d.ts rename to packages/core/src/compat/nodejs/bun-ffi-structs/structs_ffi.d.ts diff --git a/packages/core/src/compat/bun-ffi-structs/types.d.ts b/packages/core/src/compat/nodejs/bun-ffi-structs/types.d.ts similarity index 100% rename from packages/core/src/compat/bun-ffi-structs/types.d.ts rename to packages/core/src/compat/nodejs/bun-ffi-structs/types.d.ts diff --git a/packages/core/src/zig-structs.ts b/packages/core/src/zig-structs.ts index 79f99b9b7..71dc68baf 100644 --- a/packages/core/src/zig-structs.ts +++ b/packages/core/src/zig-structs.ts @@ -1,4 +1,4 @@ -import { defineStruct, defineEnum } from "./compat/bun-ffi-structs/index.js" +import { defineEnum, defineStruct } from "./compat/bun-ffi-structs.js" import { ptr, toArrayBuffer, type Pointer } from "./compat/ffi.js" import { RGBA } from "./lib/RGBA.js" From 8337e31c8ccea2cbde070786fd75ee92baad49c1 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 20:47:22 -0400 Subject: [PATCH 39/56] remove type variance --- packages/core/src/compat/ffi.ts | 4 ++++ packages/core/src/compat/nodejs/registerBun.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/compat/ffi.ts b/packages/core/src/compat/ffi.ts index b342dcb93..e83bd8280 100644 --- a/packages/core/src/compat/ffi.ts +++ b/packages/core/src/compat/ffi.ts @@ -1,5 +1,7 @@ import { FFIType } from "./FFIType.js" +export { FFIType } + export type Pointer = number & { __pointer__: null } interface FFITypeStringToType { @@ -172,3 +174,5 @@ export const dlopen = ffiModule.dlopen export const ptr = ffiModule.ptr export const suffix = ffiModule.suffix export const toArrayBuffer = ffiModule.toArrayBuffer + +export const __url = import.meta.url diff --git a/packages/core/src/compat/nodejs/registerBun.ts b/packages/core/src/compat/nodejs/registerBun.ts index ce1d76d07..0621057e0 100644 --- a/packages/core/src/compat/nodejs/registerBun.ts +++ b/packages/core/src/compat/nodejs/registerBun.ts @@ -1,6 +1,6 @@ import * as mod from "node:module" +import { __url as ffiUrl } from "../ffi.js" import * as NodeBun from "../runtime.js" -import { __url as ffiUrl } from "./ffi.js" if (typeof globalThis.Bun === "undefined") { Object.defineProperty(globalThis, "Bun", { From 047a00dc95ed87a95522870a3a1d9c4d6050d550 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 21:27:23 -0400 Subject: [PATCH 40/56] improve chances an example works w/ registerBun --- .../core/src/compat/nodejs/registerBun.ts | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/core/src/compat/nodejs/registerBun.ts b/packages/core/src/compat/nodejs/registerBun.ts index 0621057e0..042e2f05d 100644 --- a/packages/core/src/compat/nodejs/registerBun.ts +++ b/packages/core/src/compat/nodejs/registerBun.ts @@ -1,6 +1,8 @@ import * as mod from "node:module" +import { extname } from "node:path" import { __url as ffiUrl } from "../ffi.js" import * as NodeBun from "../runtime.js" +import { fileURLToPath } from "node:url"; if (typeof globalThis.Bun === "undefined") { Object.defineProperty(globalThis, "Bun", { @@ -11,16 +13,74 @@ if (typeof globalThis.Bun === "undefined") { }) } +const recentOddSpecifiers = new Map() +function popRecentSpecifier(specifier: string) { + const context = recentOddSpecifiers.get(specifier) + if (context) { + recentOddSpecifiers.delete(specifier) + } else if (recentOddSpecifiers.size > 10) { + const key = recentOddSpecifiers.keys().next().value + if (key) { + recentOddSpecifiers.delete(key) + } + } + return context +} + +function extendError(error: unknown, specifier: string, context: mod.ResolveHookContext | undefined) { + if (error && typeof error === "object" && "message" in error) { + error.message += `\nSpecifier: '${specifier}'\nFrom: ${JSON.stringify(context, null, 2)}` + } + return error +} + +const NORMAL_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"]) + mod.registerHooks({ resolve: (specifier, context, next) => { - if (specifier === "bun:ffi") { - return next(ffiUrl, context) - } + try { + if (specifier === "bun:ffi") { + return next(ffiUrl, context) + } + + if (specifier.startsWith("bun:")) { + throw new Error(`Untransformed Bun specifier: '${specifier}' from '${context.parentURL}'`) + } + + const result = next(specifier, context) - if (specifier.startsWith("bun:")) { - throw new Error(`Untransformed Bun specifier: '${specifier}' from '${context.parentURL}'`) + if ( + (!result.url.startsWith("node:") && !NORMAL_EXTENSIONS.has(extname(result.url))) || + (context.importAttributes.type && + context.importAttributes.type !== "module" && + context.importAttributes.type !== "commonjs") + ) { + recentOddSpecifiers.set(result.url, context) + } + + return result + } catch (error) { + throw extendError(error, specifier, context) } + }, + load: (specifier, context, next) => { + // Exists only for error reporting / debugging. + const resolveContext = popRecentSpecifier(specifier) + try { + return next(specifier, context) + } catch (error) { + if (context.importAttributes.type === "file" || context.importAttributes.type?.includes( + '/' + )) { + const absolutePath = fileURLToPath(specifier) + return { + format: 'module', + source: `export default ${JSON.stringify(absolutePath)}`, + shortCircuit: true, + } + } - return next(specifier, context) + throw extendError(error, specifier, resolveContext) + } }, }) From b1b4323d1d1691b2a016dd02419bc6b92fde1ac8 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 22:37:57 -0400 Subject: [PATCH 41/56] react, solid tests --- package.json | 3 +- packages/core/package.json | 8 + packages/react/package.json | 3 +- .../__snapshots__/layout.test.tsx.nodejs.snap | 195 ++++++ packages/react/tests/destroy-crash.test.tsx | 5 +- .../tests/runtime-plugin-support.test.ts | 7 +- packages/react/vitest.config.ts | 16 + packages/solid/package.json | 3 +- .../control-flow.test.tsx.nodejs.snap | 15 + .../dynamic-collections.test.tsx.nodejs.snap | 35 + .../dynamic-portal.test.tsx.nodejs.snap | 8 + .../__snapshots__/layout.test.tsx.nodejs.snap | 223 +++++++ ...line-number-scrollbox.test.tsx.nodejs.snap | 371 +++++++++++ .../textarea.test.tsx.nodejs.snap | 613 ++++++++++++++++++ packages/solid/tests/cursor-behavior.test.tsx | 13 +- packages/solid/tests/destroy-crash.test.tsx | 7 +- .../solid/tests/destroy-race-repro.test.ts | 16 +- packages/solid/tests/diff.test.tsx | 7 +- ...untime-plugin-support-node-modules.test.ts | 7 +- .../runtime-plugin-support-preload.test.ts | 7 +- .../tests/runtime-plugin-support.test.ts | 7 +- .../solid/tests/scrollbox-content.test.tsx | 5 +- .../solid/tests/slot-repro-no-preload.test.ts | 5 +- packages/solid/tests/slot.test.tsx | 3 +- packages/solid/tests/solid-plugin.test.ts | 16 +- packages/solid/vitest.config.ts | 76 +++ 26 files changed, 1634 insertions(+), 40 deletions(-) create mode 100644 packages/react/tests/__snapshots__/layout.test.tsx.nodejs.snap create mode 100644 packages/react/vitest.config.ts create mode 100644 packages/solid/tests/__snapshots__/control-flow.test.tsx.nodejs.snap create mode 100644 packages/solid/tests/__snapshots__/dynamic-collections.test.tsx.nodejs.snap create mode 100644 packages/solid/tests/__snapshots__/dynamic-portal.test.tsx.nodejs.snap create mode 100644 packages/solid/tests/__snapshots__/layout.test.tsx.nodejs.snap create mode 100644 packages/solid/tests/__snapshots__/line-number-scrollbox.test.tsx.nodejs.snap create mode 100644 packages/solid/tests/__snapshots__/textarea.test.tsx.nodejs.snap create mode 100644 packages/solid/vitest.config.ts diff --git a/package.json b/package.json index cfae5a295..ca9148982 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "publish:react": "cd packages/react && bun run publish", "publish:solid": "cd packages/solid && bun run publish", "prepare-release": "bun scripts/prepare-release.ts", - "test": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test" + "test": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test", + "test:nodejs": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test:nodejs" }, "devDependencies": { "oxfmt": "0.41.0", diff --git a/packages/core/package.json b/packages/core/package.json index 184e8757f..2989a6f2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,6 +74,14 @@ "types": "./src/testing.ts", "import": "./src/testing.ts" }, + "./compat/runtime": { + "types": "./src/compat/runtime.ts", + "import": "./src/compat/runtime.ts" + }, + "./compat/testHelpers": { + "types": "./src/compat/testHelpers.ts", + "import": "./src/compat/testHelpers.ts" + }, "./runtime-plugin": { "types": "./src/runtime-plugin.ts", "import": "./src/runtime-plugin.ts" diff --git a/packages/react/package.json b/packages/react/package.json index b11ce2736..5ac3a698a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,7 +39,8 @@ "build:examples": "bun examples/build.ts", "build:dev": "bun run scripts/build.ts --dev", "publish": "bun run scripts/publish.ts", - "test": "bun test" + "test": "bun test", + "test:nodejs": "npx vitest run" }, "devDependencies": { "@types/bun": "latest", diff --git a/packages/react/tests/__snapshots__/layout.test.tsx.nodejs.snap b/packages/react/tests/__snapshots__/layout.test.tsx.nodejs.snap new file mode 100644 index 000000000..b4169a402 --- /dev/null +++ b/packages/react/tests/__snapshots__/layout.test.tsx.nodejs.snap @@ -0,0 +1,195 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`React Renderer | Layout Tests > Basic Text Rendering > should render multiline text correctly 1`] = ` +"Line 1 +Line 2 +Line 3 + + +" +`; + +exports[`React Renderer | Layout Tests > Basic Text Rendering > should render simple text correctly 1`] = ` +"Hello World + + + + +" +`; + +exports[`React Renderer | Layout Tests > Basic Text Rendering > should render text with dynamic content 1`] = ` +"Counter: 42 + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should auto-enable border when borderColor is set 1`] = ` +"┌──────────────────┐ +│Colored Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should auto-enable border when borderStyle is set 1`] = ` +"┌──────────────────┐ +│With Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should auto-enable border when focusedBorderColor is set 1`] = ` +"┌──────────────────┐ +│Focused Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should render absolute positioned boxes 1`] = ` +"┌────────┐ +│Box 1 │ +└────────┘ ┌────────┐ + │Box 2 │ + └────────┘ + + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should render basic box layout correctly 1`] = ` +"┌──────────────────┐ +│Inside Box │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`React Renderer | Layout Tests > Box Layout Rendering > should render nested boxes correctly 1`] = ` +"┌─Parent Box─────────────────┐ +│ │ +│ │ +│ ┌────────┐ │ +│ │Nested │ │ +│ └────────┘ │ +│ Sibling │ +│ │ +│ │ +└────────────────────────────┘ + + +" +`; + +exports[`React Renderer | Layout Tests > Complex Layouts > should render complex nested layout correctly 1`] = ` +"┌─Complex Layout───────────────────────┐ +│ ┌─────────────┐ │ +│ │Header Sectio│ │ +│ │Menu Item 1 │ │ +│ │Menu Item 2 │ │ +│ └─────────────┘ │ +│ ┌────────────────┐ │ +│ │Content Area │ │ +│ │Some content her│ │ +│ │More content │ │ +│ │Footer text │ │ +│ │ │ │ +│ │ │ │ +│ └────────────────┘ │ +│ Status: Ready │ +└──────────────────────────────────────┘ + + +" +`; + +exports[`React Renderer | Layout Tests > Complex Layouts > should render scrollbox with sticky scroll and spacer 1`] = ` +"┌─scroll area────────────────┐ +│ │ +│┌─hi───────────────────────┐│ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +│└──────────────────────────┘│ +│ │ +│ │ +└────────────────────────────┘ +┌─spacer─────────────────────┐ +│spacer │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────┘ +" +`; + +exports[`React Renderer | Layout Tests > Complex Layouts > should render text with mixed styling and layout 1`] = ` +"┌─────────────────────────────────┐ +│ERROR: Something went wrong │ +│WARNING: Check your settings │ +│SUCCESS: All systems operational │ +│ │ +│ │ +│ │ +└─────────────────────────────────┘ + + +" +`; + +exports[`React Renderer | Layout Tests > Empty and Edge Cases > should handle component with no children 1`] = ` +" + + + + + + + +" +`; + +exports[`React Renderer | Layout Tests > Empty and Edge Cases > should handle empty component 1`] = ` +" + + + + +" +`; + +exports[`React Renderer | Layout Tests > Empty and Edge Cases > should handle very small dimensions 1`] = ` +"Hi + + +" +`; diff --git a/packages/react/tests/destroy-crash.test.tsx b/packages/react/tests/destroy-crash.test.tsx index 2efa57961..0d454413d 100644 --- a/packages/react/tests/destroy-crash.test.tsx +++ b/packages/react/tests/destroy-crash.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test" import React, { useEffect, useState } from "react" import { createTestRenderer } from "@opentui/core/testing" import { createRoot } from "../src/reconciler/renderer.js" +import { sleep } from "@opentui/core/compat/runtime" /** * Regression test for: Native Yoga crash when renderer.destroy() is called @@ -73,7 +74,7 @@ describe("Renderer Destroy Crash with Pending React Updates", () => { // Let the component mount and interval start await testSetup.renderOnce() - await Bun.sleep(30) + await sleep(30) await testSetup.renderOnce() // Destroy WITHOUT unmounting React - this is the bug! @@ -83,7 +84,7 @@ describe("Renderer Destroy Crash with Pending React Updates", () => { // Wait for interval to fire more updates after destroy // This is when the crash occurs if the bug is present - await Bun.sleep(100) + await sleep(100) // If we reach here without crashing, the bug is fixed expect(true).toBe(true) diff --git a/packages/react/tests/runtime-plugin-support.test.ts b/packages/react/tests/runtime-plugin-support.test.ts index 82a747d6d..f1128d378 100644 --- a/packages/react/tests/runtime-plugin-support.test.ts +++ b/packages/react/tests/runtime-plugin-support.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" +import { spawnSync } from "@opentui/core/compat/testHelpers" + +const bunIt = process.versions.bun ? it : it.skip describe("react runtime plugin support", () => { - it("loads external modules against host runtime exports", () => { + bunIt("loads external modules against host runtime exports", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/react/vitest.config.ts b/packages/react/vitest.config.ts new file mode 100644 index 000000000..a1d70df50 --- /dev/null +++ b/packages/react/vitest.config.ts @@ -0,0 +1,16 @@ +import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { + alias: { + "bun:test": fileURLToPath(new URL("../core/src/compat/test.ts", import.meta.url)), + }, + }, + test: { + environment: "node", + resolveSnapshotPath: (testPath, ext) => + join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs${ext}`), + }, +}) diff --git a/packages/solid/package.json b/packages/solid/package.json index 111a72a58..d746a06b9 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -16,7 +16,8 @@ "build": "bun scripts/build.ts", "build:examples": "bun examples/build.ts", "publish": "bun scripts/publish.ts", - "test": "bun test" + "test": "bun test", + "test:nodejs": "npx vitest run" }, "exports": { ".": { diff --git a/packages/solid/tests/__snapshots__/control-flow.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/control-flow.test.tsx.nodejs.snap new file mode 100644 index 000000000..664eed9c4 --- /dev/null +++ b/packages/solid/tests/__snapshots__/control-flow.test.tsx.nodejs.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SolidJS Renderer - Control Flow Components > Combined Control Flow > should be able to anchor to slot nodes 1`] = ` +"┌─A─────────────────────┐ +└───────────────────────┘ +┌─B─────────────────────┐ +└───────────────────────┘ + + + + + + +" +`; diff --git a/packages/solid/tests/__snapshots__/dynamic-collections.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/dynamic-collections.test.tsx.nodejs.snap new file mode 100644 index 000000000..dd0ed39d7 --- /dev/null +++ b/packages/solid/tests/__snapshots__/dynamic-collections.test.tsx.nodejs.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SolidJS Renderer - Dynamic Collections > Collection Transformations > should handle filtering collections 1`] = ` +"Apple (fruit) +Banana (fruit) + + + +" +`; + +exports[`SolidJS Renderer - Dynamic Collections > Collection Transformations > should handle sorting collections 1`] = ` +"Number: 1 +Number: 1 +Number: 3 +Number: 4 +Number: 5 +" +`; + +exports[`SolidJS Renderer - Dynamic Collections > Collection Transformations > should handle sorting collections 2`] = ` +"Number: 5 +Number: 4 +Number: 3 +Number: 1 +Number: 1 +" +`; + +exports[`SolidJS Renderer - Dynamic Collections > Edge Cases > should handle rapid collection updates 1`] = ` +"First +Second +Fourth +" +`; diff --git a/packages/solid/tests/__snapshots__/dynamic-portal.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/dynamic-portal.test.tsx.nodejs.snap new file mode 100644 index 000000000..13af87172 --- /dev/null +++ b/packages/solid/tests/__snapshots__/dynamic-portal.test.tsx.nodejs.snap @@ -0,0 +1,8 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SolidJS Renderer - Dynamic and Portal Components > Component > should pass props correctly to dynamic components 1`] = ` +"Updated text + + +" +`; diff --git a/packages/solid/tests/__snapshots__/layout.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/layout.test.tsx.nodejs.snap new file mode 100644 index 000000000..8e80d5c47 --- /dev/null +++ b/packages/solid/tests/__snapshots__/layout.test.tsx.nodejs.snap @@ -0,0 +1,223 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SolidJS Renderer Integration Tests > Basic Text Rendering > should render multiline text correctly 1`] = ` +"Line 1 +Line 2 +Line 3 + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Basic Text Rendering > should render simple text correctly 1`] = ` +"Hello World + + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Basic Text Rendering > should render text with dynamic content 1`] = ` +"Counter: 42 + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should auto-enable border when borderColor is set 1`] = ` +"┌──────────────────┐ +│Colored Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should auto-enable border when borderStyle is set 1`] = ` +"┌──────────────────┐ +│With Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should auto-enable border when focusedBorderColor is set 1`] = ` +"┌──────────────────┐ +│Focused Border │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should render absolute positioned boxes 1`] = ` +"┌────────┐ +│Box 1 │ +└────────┘ ┌────────┐ + │Box 2 │ + └────────┘ + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should render basic box layout correctly 1`] = ` +"┌──────────────────┐ +│Inside Box │ +│ │ +│ │ +└──────────────────┘ + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Box Layout Rendering > should render nested boxes correctly 1`] = ` +"┌─Parent Box─────────────────┐ +│ │ +│ │ +│ ┌────────┐ │ +│ │Nested │ │ +│ └────────┘ │ +│ Sibling │ +│ │ +│ │ +└────────────────────────────┘ + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Complex Layouts > should render complex nested layout correctly 1`] = ` +"┌─Complex Layout───────────────────────┐ +│ ┌─────────────┐ │ +│ │Header Sectio│ │ +│ │Menu Item 1 │ │ +│ │Menu Item 2 │ │ +│ └─────────────┘ │ +│ ┌────────────────┐ │ +│ │Content Area │ │ +│ │Some content her│ │ +│ │More content │ │ +│ │Footer text │ │ +│ │ │ │ +│ │ │ │ +│ └────────────────┘ │ +│ Status: Ready │ +└──────────────────────────────────────┘ + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Complex Layouts > should render scrollbox with sticky scroll and spacer 1`] = ` +"┌─scroll area────────────────┐ +│ │ +│┌─hi───────────────────────┐│ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +│└──────────────────────────┘│ +│ │ +│ │ +└────────────────────────────┘ +┌─spacer─────────────────────┐ +│spacer │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────┘ +" +`; + +exports[`SolidJS Renderer Integration Tests > Complex Layouts > should render text with mixed styling and layout 1`] = ` +"┌─────────────────────────────────┐ +│ERROR: Something went wrong │ +│WARNING: Check your settings │ +│SUCCESS: All systems operational │ +│ │ +│ │ +│ │ +└─────────────────────────────────┘ + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Empty and Edge Cases > should handle component with no children 1`] = ` +" + + + + + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Empty and Edge Cases > should handle empty component 1`] = ` +" + + + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Empty and Edge Cases > should handle very small dimensions 1`] = ` +"Hi + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Reactive Updates > should handle conditional rendering 1`] = ` +"Always visible - Conditional t + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Reactive Updates > should handle conditional rendering 2`] = ` +"Always visible + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Reactive Updates > should handle reactive state changes 1`] = ` +"Counter: 0 + + +" +`; + +exports[`SolidJS Renderer Integration Tests > Reactive Updates > should handle reactive state changes 2`] = ` +"Counter: 5 + + +" +`; diff --git a/packages/solid/tests/__snapshots__/line-number-scrollbox.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/line-number-scrollbox.test.tsx.nodejs.snap new file mode 100644 index 000000000..b3a09b4f1 --- /dev/null +++ b/packages/solid/tests/__snapshots__/line-number-scrollbox.test.tsx.nodejs.snap @@ -0,0 +1,371 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > REPRODUCES BUG: single line_number with code in scrollbox has excessive height 1`] = ` +" 1 function hello() { + 2 console.log("Hello, World!"); + 3 return 42; + 4 } + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > VISUAL CHECK: box with line_number should have clean spacing 1`] = ` +" + + ═══ Code Block 1 ═══ + ┌────────────────────────────────────────────┐ + │ 1 const x = 1; │ + │ 2 const y = 2; │ + │ 3 const z = 3; │ + └────────────────────────────────────────────┘ + ═══ Code Block 2 ═══ + ┌────────────────────────────────────────────┐ + │ 1 function test() { │ + │ 2 return 42; │ + │ 3 } │ + └────────────────────────────────────────────┘ + ═══ End ═══ + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > WORKAROUND: flexShrink=0 fixes the height issue 1`] = ` +" 1 function hello() { + 2 console.log("Hello, World!"); + 3 return 42; + 4 } + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > line_number height should match code content height, not double 1`] = ` +"--- START MARKER --- + 1 const x = 1; + 2 const y = 2; +--- END MARKER --- + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > multiple line_number blocks should not overlap - realistic chat scenario 1`] = ` +" Wrote src/hello.ts + 1 export function hello() { + 2 return "Hello, World!"; + 3 } + I've created the hello function. + Wrote src/test.ts + 1 import { hello } from "./hello"; + 2 + 3 test("hello returns greeting", () => { + 4 expect(hello()).toBe("Hello, World!"); + 5 }); + I've also added a test file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > multiple messages with mixed content - verify no overlapping 1`] = ` +" + Let me create a file for you. + Wrote src/greet.ts + 1 export const greet = (name: string) => { + 2 return \`Hello, \${name}!\`; + 3 }; + I've created the greet function. + Wrote src/index.ts + 1 import { greet } from "./greet"; + 2 + 3 console.log(greet("World")); + Error [2:5]: Unused variable + And here's the main file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > scroll behavior - content should remain visible after scroll 1`] = ` +"Message 1 + 1 const a = 1; +Message 2 + 1 const b = 2; + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > scroll behavior - content should remain visible after scroll 2`] = ` +"Message 8 + 1 const var8 = 8; +Message 9 + 1 const var9 = 9; +Message 10 + 1 const var10 = 10; +Message 11 + 1 const var11 = 11; +Message 12 + 1 const var12 = 12; +Message 13 + 1 const var13 = 13; +Message 14 + 1 const var14 = 14; +Message 15 + 1 const var15 = 15; +Message 16 + 1 const var16 = 16; +Message 17 + 1 const var17 = 17; +Message 18 + 1 const var18 = 18; +Message 19 + 1 const var19 = 19; +Message 20 + 1 const var20 = 20; +Message 21 + 1 const var21 = 21; +Message 22 + 1 const var22 = 22; +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > scroll behavior - content should remain visible after scroll 3`] = ` +"Message 8 + 1 const var8 = 8; +Message 9 + 1 const var9 = 9; +Message 10 + 1 const var10 = 10; +Message 11 + 1 const var11 = 11; +Message 12 + 1 const var12 = 12; +Message 13 + 1 const var13 = 13; +Message 14 + 1 const var14 = 14; +Message 15 + 1 const var15 = 15; +Message 16 + 1 const var16 = 16; +Message 17 + 1 const var17 = 17; +Message 18 + 1 const var18 = 18; +Message 19 + 1 const var19 = 19; +Message 20 + 1 const var20 = 20; +Message 21 + 1 const var21 = 21; +Message 22 + 1 const var22 = 22; +" +`; + +exports[`LineNumber in ScrollBox - Height and Overlap Issues > scrollbox with box container around line_number - no excessive height 1`] = ` +" + Message 1 + ┌────────────────────────────────────────────┐ + │ 1 function test() { │ + │ 2 return true; │ + │ 3 } │ + └────────────────────────────────────────────┘ + Message 2 + + + + + + + + + + + + + + + + + + + + + + +" +`; diff --git a/packages/solid/tests/__snapshots__/textarea.test.tsx.nodejs.snap b/packages/solid/tests/__snapshots__/textarea.test.tsx.nodejs.snap new file mode 100644 index 000000000..eb7ec5168 --- /dev/null +++ b/packages/solid/tests/__snapshots__/textarea.test.tsx.nodejs.snap @@ -0,0 +1,613 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Textarea Layout Tests > Basic Textarea Rendering > should render multiline textarea content 1`] = ` +"Line 1 +Line 2 +Line 3 + + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Basic Textarea Rendering > should render simple textarea correctly 1`] = ` +"Hello World + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Basic Textarea Rendering > should render textarea with placeholder 1`] = ` +"Type something here... + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Basic Textarea Rendering > should render textarea with word wrapping 1`] = ` +"This is a very long +line that should +wrap to multiple +lines when word +wrapping is enabled + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Complex Layouts with Multiple Textareas > should handle nested boxes with textareas at different positions 1`] = ` +"┌─Layout Test────────────────────────────────────┐ +│┌──────────────────┐ ┌─────────────────────────┐│ +││Input 1: │ │Input 2: ││ +││Left panel content│ │Right panel with longer ││ +││ │ │content that may wrap ││ +│└──────────────────┘ └─────────────────────────┘│ +│ │ +│┌──────────────────────────────────────────────┐│ +││Bottom input: ││ +││Bottom panel spanning full width ││ +│└──────────────────────────────────────────────┘│ +└────────────────────────────────────────────────┘ + + + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Complex Layouts with Multiple Textareas > should render multiple textareas in a column layout 1`] = ` +"┌─Chat─────────────────────────────────────────────────────┐ +│┌────────────────────────────────────────────────────────┐│ +││User What is the weather like today? ││ +│└────────────────────────────────────────────────────────┘│ +│ │ +│┌────────────────────────────────────────────────────────┐│ +││AI I don't have access to real-time weather data, ││ +││ but I can help you find that information through ││ +││ various weather services. ││ +│└────────────────────────────────────────────────────────┘│ +└──────────────────────────────────────────────────────────┘ + + + + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Edge Cases and Styling > should render empty textarea with placeholder in prompt layout 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ > Enter your prompt here... │ +│ │ +│Ready to chat │ +└────────────────────────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea Layout Tests > Edge Cases and Styling > should render full prompt-like layout with all components 1`] = ` +"┌────────────────────────────────────────────────────────────────────┐ +│ │ +│ > Explain how async/await works in JavaScript and provide some │ +│ examples │ +│ │ +│openai gpt-4-turbo ctrl+p commands│ +└────────────────────────────────────────────────────────────────────┘ + +Tip: Use arrow keys to navigate through history when cursor is at the +start + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Edge Cases and Styling > should render textarea with focused colors 1`] = ` +"┌──────────────────────────────────────┐ +│> │ +│ Focused textarea │ +│ │ +└──────────────────────────────────────┘ + + + + + +" +`; + +exports[`Textarea Layout Tests > Edge Cases and Styling > should render textarea with very long single line 1`] = ` +"┌──────────────────────────────────────┐ +│> │ +│ ThisIsAVeryLongLineWithNoSpacesThat│ +│ WillWrapByCharacterWhenCharWrapping│ +│ IsEnabled │ +│ │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > FlexShrink Regression Tests > should not shrink box when height is set via setter in column layout 1`] = ` +"┌───────────────────────┐ +│Header │ +│ │ +│ │ +│Line1 │ +│Line2 │ +│Line3 │ +│Footer │ +│ │ +└───────────────────────┘ + + + + + +" +`; + +exports[`Textarea Layout Tests > FlexShrink Regression Tests > should not shrink box when width is set via setter 1`] = ` +"┌────────────────────────────┐ +│> Content that takes up │ +│ space │ +└────────────────────────────┘ + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should correctly measure multiline content with unicode 1`] = ` +"┌────────────────────────────┐ +│Hello 世界 │ +│こんにちは │ +│🌟 Emoji 🚀 │ +└────────────────────────────┘ + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should correctly measure text after content change 1`] = ` +"┌──────────────────────────────────────┐ +│Short text │ +└──────────────────────────────────────┘ + + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should correctly measure text after content change 2`] = ` +"┌──────────────────────────────────────┐ +│This is a much longer text that will │ +│definitely wrap to multiple lines │ +│when rendered │ +└──────────────────────────────────────┘ + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle empty to non-empty content transition 1`] = ` +"┌──────────────────────────────────────┐ +│ │ +└──────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle empty to non-empty content transition 2`] = ` +"┌──────────────────────────────────────┐ +│Now with content │ +└──────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle empty to non-empty content transition 3`] = ` +"┌──────────────────────────────────────┐ +│ │ +└──────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle rapid content updates correctly 1`] = ` +"┌────────────────────────────┐ +│Update 4: some text here │ +└────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle width changes with cached measures 1`] = ` +"┌────────────────────────────┐ +│Content that will wrap │ +│differently at different │ +│widths │ +└────────────────────────────┘ + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle width changes with cached measures 2`] = ` +"┌────────────────────────────────────────────────┐ +│Content that will wrap differently at different │ +│widths │ +└────────────────────────────────────────────────┘ + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Measure Cache Edge Cases > should handle width changes with cached measures 3`] = ` +"┌──────────────────┐ +│Content that will │ +│wrap differently │ +│at different │ +│widths │ +└──────────────────┘ + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Prompt-like Layout > should render textarea in prompt-style layout with indicator 1`] = ` +"┌──────────────────────────────────────────────────────────┐ +│ │ +│ > Hello from the prompt │ +│ │ +│provider model-name ctrl+p commands│ +└──────────────────────────────────────────────────────────┘ + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Prompt-like Layout > should render textarea in shell mode with different indicator 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ ! ls -la │ +│ │ +│shell mode │ +└────────────────────────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea Layout Tests > Prompt-like Layout > should render textarea with long wrapping text in prompt layout 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ This is a very long prompt that will wrap │ +│ > across multiple lines in the textarea. It │ +│ should maintain proper layout with the │ +│ indicator on the left. │ +│ │ +│openai gpt-4 │ +└────────────────────────────────────────────────┘ + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should handle very long single-line text in prompt layout 1`] = ` +"┌──────────────────────────────────────┐ +│> │ +│ ThisIsAVeryLongLineWithNoSpacesThat│ +│ WillWrapByCharacterWhenCharWrapping│ +│ IsEnabled │ +│ │ +└──────────────────────────────────────┘ + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should render full prompt layout with text component 1`] = ` +"┌────────────────────────────────────────────────────────────────────┐ +│ │ +│ > Explain how async/await works in JavaScript and provide some │ +│ examples │ +│ │ +│openai gpt-4-turbo ctrl+p commands│ +└────────────────────────────────────────────────────────────────────┘ + +Tip: Use arrow keys to navigate through history when cursor is at the +start + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should render multiline text in prompt layout 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ Line 1: First line of text │ +│ > Line 2: Second line of text │ +│ Line 3: Third line of text │ +│ │ +│multiline example │ +└────────────────────────────────────────────────┘ + + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should render text in prompt-style layout with indicator 1`] = ` +"┌──────────────────────────────────────────────────────────┐ +│ │ +│ > Hello from the prompt │ +│ │ +│provider model-name ctrl+p commands│ +└──────────────────────────────────────────────────────────┘ + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should render text in shell mode with different indicator 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ ! ls -la │ +│ │ +│shell mode │ +└────────────────────────────────────────────────┘ + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should render text with long wrapping content in prompt layout 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ This is a very long prompt that will wrap │ +│ > across multiple lines in the text component. │ +│ It should maintain proper layout with the │ +│ indicator on the left. │ +│ │ +│openai gpt-4 │ +└────────────────────────────────────────────────┘ + + + + + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should update text content reactively in prompt layout 1`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ > Initial text │ +│ │ +└────────────────────────────────────────────────┘ + + + + + + + +" +`; + +exports[`Textarea Layout Tests > Text Component Comparison > should update text content reactively in prompt layout 2`] = ` +"┌────────────────────────────────────────────────┐ +│ │ +│ Updated text that is much longer and should │ +│ > wrap to multiple lines if word wrapping is │ +│ enabled │ +│ │ +└────────────────────────────────────────────────┘ + + + + + +" +`; diff --git a/packages/solid/tests/cursor-behavior.test.tsx b/packages/solid/tests/cursor-behavior.test.tsx index 4112b2784..a42649c8a 100644 --- a/packages/solid/tests/cursor-behavior.test.tsx +++ b/packages/solid/tests/cursor-behavior.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it, beforeEach, afterEach } from "bun:test" import { testRender } from "../index.js" import { createSignal, onMount, Show } from "solid-js" import type { TextareaRenderable } from "@opentui/core" +import { sleep } from "@opentui/core/compat/runtime" let testSetup: Awaited> @@ -394,12 +395,12 @@ describe("Textarea Cursor Behavior Tests", () => { testSetup.renderer.addPostProcessFn(captureOffsets) testSetup.renderer.start() - await Bun.sleep(30) + await sleep(30) viewOffsets.length = 0 await testSetup.mockInput.pasteBracketedText("Line 1\nLine 2\nLine 3") - await Bun.sleep(200) + await sleep(200) testSetup.renderer.pause() await testSetup.renderer.idle() @@ -493,12 +494,12 @@ describe("Textarea Cursor Behavior Tests", () => { testSetup.renderer.addPostProcessFn(captureOffsets) testSetup.renderer.start() - await Bun.sleep(30) + await sleep(30) viewOffsets.length = 0 await testSetup.mockInput.pasteBracketedText("Line 1\nLine 2\nLine 3") - await Bun.sleep(200) + await sleep(200) testSetup.renderer.pause() await testSetup.renderer.idle() @@ -592,12 +593,12 @@ describe("Textarea Cursor Behavior Tests", () => { testSetup.renderer.addPostProcessFn(captureHeight) testSetup.renderer.start() - await Bun.sleep(30) + await sleep(30) heights.length = 0 await testSetup.mockInput.pasteBracketedText("Line 1\nLine 2\nLine 3") - await Bun.sleep(200) + await sleep(200) testSetup.renderer.pause() await testSetup.renderer.idle() diff --git a/packages/solid/tests/destroy-crash.test.tsx b/packages/solid/tests/destroy-crash.test.tsx index eb7ac74ad..c2f7a81c7 100644 --- a/packages/solid/tests/destroy-crash.test.tsx +++ b/packages/solid/tests/destroy-crash.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test" import { createTestRenderer } from "@opentui/core/testing" import { For, createSignal, onCleanup, onMount } from "solid-js" import { render } from "../index.js" +import { sleep } from "@opentui/core/compat/runtime" describe("Renderer destroy with pending Solid updates", () => { it("disposes Solid root when renderer is destroyed externally", async () => { @@ -66,7 +67,7 @@ describe("Renderer destroy with pending Solid updates", () => { await render(() => , testSetup.renderer) await testSetup.renderOnce() - await Bun.sleep(30) + await sleep(30) await testSetup.renderOnce() log(`ticks before destroy: ${intervalTicks}`) @@ -74,11 +75,11 @@ describe("Renderer destroy with pending Solid updates", () => { log("calling renderer.destroy()") testSetup.renderer.destroy() - await Bun.sleep(30) + await sleep(30) const ticksSoonAfterDestroy = intervalTicks log(`ticks soon after destroy: ${ticksSoonAfterDestroy}`) - await Bun.sleep(60) + await sleep(60) const ticksLaterAfterDestroy = intervalTicks log(`ticks later after destroy: ${ticksLaterAfterDestroy}`) diff --git a/packages/solid/tests/destroy-race-repro.test.ts b/packages/solid/tests/destroy-race-repro.test.ts index c1238e0ac..0842d5342 100644 --- a/packages/solid/tests/destroy-race-repro.test.ts +++ b/packages/solid/tests/destroy-race-repro.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" +import { spawnSync } from "@opentui/core/compat/testHelpers" const fixturePath = join(import.meta.dirname, "destroy-race.fixture.tsx") +const bunIt = process.versions.bun ? it : it.skip type Mode = "external" | "helper" | "external-onmount" | "helper-onmount" | "external-active" | "helper-active" const runFixture = (mode: Mode) => { - const result = Bun.spawnSync([process.execPath, fixturePath, mode], { + const result = spawnSync([process.execPath, fixturePath, mode], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", @@ -19,37 +21,37 @@ const runFixture = (mode: Mode) => { } describe("destroy race regressions", () => { - it("does not crash when renderer is destroyed during initial render (external renderer path)", () => { + bunIt("does not crash when renderer is destroyed during initial render (external renderer path)", () => { const { result } = runFixture("external") expect(result.exitCode).toBe(0) }) - it("does not crash when renderer is destroyed during initial render (testRender helper path)", () => { + bunIt("does not crash when renderer is destroyed during initial render (testRender helper path)", () => { const { result } = runFixture("helper") expect(result.exitCode).toBe(0) }) - it("does not crash when renderer is destroyed from onMount (external renderer path)", () => { + bunIt("does not crash when renderer is destroyed from onMount (external renderer path)", () => { const { result } = runFixture("external-onmount") expect(result.exitCode).toBe(0) }) - it("does not crash when renderer is destroyed from onMount (testRender helper path)", () => { + bunIt("does not crash when renderer is destroyed from onMount (testRender helper path)", () => { const { result } = runFixture("helper-onmount") expect(result.exitCode).toBe(0) }) - it("does not crash when renderer is destroyed in an active render pass (external renderer path)", () => { + bunIt("does not crash when renderer is destroyed in an active render pass (external renderer path)", () => { const { result } = runFixture("external-active") expect(result.exitCode).toBe(0) }) - it("does not crash when renderer is destroyed in an active render pass (testRender helper path)", () => { + bunIt("does not crash when renderer is destroyed in an active render pass (testRender helper path)", () => { const { result } = runFixture("helper-active") expect(result.exitCode).toBe(0) diff --git a/packages/solid/tests/diff.test.tsx b/packages/solid/tests/diff.test.tsx index 57c530df7..dffbbadee 100644 --- a/packages/solid/tests/diff.test.tsx +++ b/packages/solid/tests/diff.test.tsx @@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { testRender } from "../index.js" import { SyntaxStyle, RGBA } from "@opentui/core" import { createSignal, Show } from "solid-js" +import { sleep } from "@opentui/core/compat/runtime" let testSetup: Awaited> @@ -57,7 +58,7 @@ describe("DiffRenderable with SolidJS", () => { )) // Wait for automatic initial render - await Bun.sleep(50) + await sleep(50) const boxRenderable = testSetup.renderer.root.getRenderable("root") const diffRenderable = boxRenderable?.getRenderable("test-diff") as any @@ -415,7 +416,7 @@ describe("DiffRenderable with SolidJS", () => { await testSetup.renderOnce() setWrapMode("word") - await Bun.sleep(10) + await sleep(10) await testSetup.renderer.idle() const frameAfterToggle = testSetup.captureCharFrame() @@ -438,7 +439,7 @@ describe("DiffRenderable with SolidJS", () => { )) - await Bun.sleep(10) + await sleep(10) await testSetup.renderer.idle() const frameFromStart = testSetup.captureCharFrame() diff --git a/packages/solid/tests/runtime-plugin-support-node-modules.test.ts b/packages/solid/tests/runtime-plugin-support-node-modules.test.ts index 2754b53d4..748ef33e2 100644 --- a/packages/solid/tests/runtime-plugin-support-node-modules.test.ts +++ b/packages/solid/tests/runtime-plugin-support-node-modules.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" +import { spawnSync } from "@opentui/core/compat/testHelpers" + +const bunIt = process.versions.bun ? it : it.skip describe("solid runtime plugin support in node_modules", () => { - it("rewrites runtime module specifiers for external node_modules modules", () => { + bunIt("rewrites runtime module specifiers for external node_modules modules", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support-node-modules.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/solid/tests/runtime-plugin-support-preload.test.ts b/packages/solid/tests/runtime-plugin-support-preload.test.ts index a0acc7068..10a64675f 100644 --- a/packages/solid/tests/runtime-plugin-support-preload.test.ts +++ b/packages/solid/tests/runtime-plugin-support-preload.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" +import { spawnSync } from "@opentui/core/compat/testHelpers" + +const bunIt = process.versions.bun ? it : it.skip describe("solid runtime plugin support with preload", () => { - it("rewrites external TSX modules even when the preload plugin is already active", () => { + bunIt("rewrites external TSX modules even when the preload plugin is already active", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support-preload.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/solid/tests/runtime-plugin-support.test.ts b/packages/solid/tests/runtime-plugin-support.test.ts index 9ec8c095b..107463e82 100644 --- a/packages/solid/tests/runtime-plugin-support.test.ts +++ b/packages/solid/tests/runtime-plugin-support.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "bun:test" import { join } from "node:path" +import { spawnSync } from "@opentui/core/compat/testHelpers" + +const bunIt = process.versions.bun ? it : it.skip describe("solid runtime plugin support", () => { - it("loads external TSX modules against host runtime modules", () => { + bunIt("loads external TSX modules against host runtime modules", () => { const fixturePath = join(import.meta.dirname, "runtime-plugin-support.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/solid/tests/scrollbox-content.test.tsx b/packages/solid/tests/scrollbox-content.test.tsx index 3e0df2a9f..6007b6791 100644 --- a/packages/solid/tests/scrollbox-content.test.tsx +++ b/packages/solid/tests/scrollbox-content.test.tsx @@ -4,6 +4,7 @@ import { createSignal, createMemo, createEffect, For } from "solid-js" import type { ScrollBoxRenderable } from "../../core/src/renderables/index.js" import { SyntaxStyle } from "../../core/src/syntax-style.js" import { MockTreeSitterClient } from "@opentui/core/testing" +import { sleep } from "@opentui/core/compat/runtime" let testSetup: Awaited> let mockTreeSitterClient: MockTreeSitterClient @@ -364,7 +365,7 @@ world const filler = Array.from({ length: 12 }, (_, i) => `Message ${i + 1}`) setItems([...filler, opencodeMessage]) await testSetup.renderOnce() - await Bun.sleep(20) + await sleep(20) await testSetup.renderOnce() if (scrollRef) { @@ -400,7 +401,7 @@ world for (let width = 100; width >= 80; width -= 1) { testSetup.resize(width, 24) await testSetup.renderOnce() - await Bun.sleep(20) + await sleep(20) await testSetup.renderOnce() if (scrollRef) { scrollRef.scrollTo(scrollRef.scrollHeight) diff --git a/packages/solid/tests/slot-repro-no-preload.test.ts b/packages/solid/tests/slot-repro-no-preload.test.ts index 1d85c97d3..049c79cf8 100644 --- a/packages/solid/tests/slot-repro-no-preload.test.ts +++ b/packages/solid/tests/slot-repro-no-preload.test.ts @@ -9,6 +9,7 @@ import { createSolidSlotRegistry, useRenderer, } from "../index.js" +import { sleep } from "@opentui/core/compat/runtime" type AppSlots = { statusbar: { user: string } @@ -157,7 +158,7 @@ describe("slot behavior stability without preload", () => { } await setup.renderOnce() - await Bun.sleep(0) + await sleep(0) await setup.renderOnce() const settled = setup.renderer.listenerCount("selection") @@ -248,7 +249,7 @@ describe("slot behavior stability without preload", () => { registry.updateOrder("sidebar-only", 5) await setup.renderOnce() - await Bun.sleep(0) + await sleep(0) await setup.renderOnce() expect(mounts).toBe(1) diff --git a/packages/solid/tests/slot.test.tsx b/packages/solid/tests/slot.test.tsx index 8a9943749..6a6420a14 100644 --- a/packages/solid/tests/slot.test.tsx +++ b/packages/solid/tests/slot.test.tsx @@ -4,6 +4,7 @@ import { createContext, createComponent, createSignal, onCleanup, onMount, useCo import { createSlot, createSolidSlotRegistry, Slot, type SolidPlugin } from "../src/plugins/slot.js" import { _render as renderInternal } from "../src/reconciler.js" import { RendererContext } from "../src/elements/index.js" +import { sleep } from "@opentui/core/compat/runtime" interface AppSlots { statusbar: { user: string } @@ -472,7 +473,7 @@ describe("Solid Slot System", () => { const settle = async () => { await testSetup.renderOnce() - await Bun.sleep(0) + await sleep(0) await testSetup.renderOnce() } diff --git a/packages/solid/tests/solid-plugin.test.ts b/packages/solid/tests/solid-plugin.test.ts index f6191dd13..199200ca0 100644 --- a/packages/solid/tests/solid-plugin.test.ts +++ b/packages/solid/tests/solid-plugin.test.ts @@ -1,9 +1,19 @@ import { runtimeModuleIdForSpecifier } from "@opentui/core/runtime-plugin" import { describe, expect, it } from "bun:test" import { mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { createRequire } from "node:module" import { tmpdir } from "node:os" import { join } from "node:path" -import { createSolidTransformPlugin } from "../scripts/solid-plugin.js" +import { spawnSync } from "@opentui/core/compat/testHelpers" + +const require = createRequire(import.meta.url) +const bunDescribe = process.versions.bun ? describe : describe.skip + +type CreateSolidTransformPlugin = (typeof import("../scripts/solid-plugin.js"))["createSolidTransformPlugin"] + +const createSolidTransformPlugin: CreateSolidTransformPlugin = (...args) => { + return require("../scripts/solid-plugin.js").createSolidTransformPlugin(...args) +} type ResolveCallback = (args: { path: string; importer: string }) => unknown | Promise type LoadResult = { contents: string; loader: string } | void @@ -73,7 +83,7 @@ const createTempTsxFile = (source: string): { path: string; dispose: () => void } } -describe("solid transform plugin", () => { +bunDescribe("solid transform plugin", () => { it("does not register runtime module resolvers by default", () => { const { build, resolveFilters, modules } = createMockBuild() createSolidTransformPlugin().setup(build as any) @@ -182,7 +192,7 @@ describe("solid transform plugin", () => { it("transforms runtime-resolved modules end-to-end in a subprocess", () => { const fixturePath = join(import.meta.dirname, "solid-plugin.fixture.ts") - const result = Bun.spawnSync([process.execPath, fixturePath], { + const result = spawnSync([process.execPath, fixturePath], { cwd: join(import.meta.dirname, ".."), stdout: "pipe", stderr: "pipe", diff --git a/packages/solid/vitest.config.ts b/packages/solid/vitest.config.ts new file mode 100644 index 000000000..574bafcaa --- /dev/null +++ b/packages/solid/vitest.config.ts @@ -0,0 +1,76 @@ +import { transformAsync } from "@babel/core" +import ts from "@babel/preset-typescript" +import { createRequire } from "node:module" +import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import solid from "babel-preset-solid" +import { defineConfig } from "vitest/config" + +const require = createRequire(import.meta.url) + +const sourcePath = (path: string): string => { + const searchIndex = path.indexOf("?") + const hashIndex = path.indexOf("#") + const end = [searchIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0] + return end === undefined ? path : path.slice(0, end) +} + +const opentuiSolidTransform = () => ({ + name: "opentui-solid-vitest-transform", + enforce: "pre" as const, + async transform(code: string, id: string) { + const path = sourcePath(id) + if (!/\.(tsx|jsx)(?:[?#].*)?$/.test(id)) { + return null + } + + const transformed = await transformAsync(code, { + filename: path, + configFile: false, + babelrc: false, + presets: [ + [ + solid, + { + moduleName: "@opentui/solid", + generate: "universal", + }, + ], + [ts], + ], + }) + + return { + code: transformed?.code ?? code, + map: transformed?.map ?? null, + } + }, +}) + +export default defineConfig({ + plugins: [opentuiSolidTransform()], + resolve: { + alias: [ + { + find: /^bun:test$/, + replacement: fileURLToPath(new URL("../core/src/compat/test.ts", import.meta.url)), + }, + { + find: /^solid-js\/store$/, + replacement: require.resolve("solid-js/store/dist/store.js"), + }, + { + find: /^solid-js$/, + replacement: require.resolve("solid-js/dist/solid.js"), + }, + ], + }, + ssr: { + noExternal: ["solid-js", /^solid-js\//], + }, + test: { + environment: "node", + resolveSnapshotPath: (testPath, ext) => + join(dirname(testPath), "__snapshots__", `${basename(testPath)}.nodejs${ext}`), + }, +}) From ba1925d7ef499b0e01e4873530b891d4122d0413 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Thu, 9 Apr 2026 22:41:43 -0400 Subject: [PATCH 42/56] fix build error --- packages/core/src/runtime-plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/runtime-plugin.ts b/packages/core/src/runtime-plugin.ts index 5f8fe3ac1..ae47e7cdb 100644 --- a/packages/core/src/runtime-plugin.ts +++ b/packages/core/src/runtime-plugin.ts @@ -455,7 +455,7 @@ export function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): Bun // Register both the resolved path spelling and its canonical realpath so Bun // can reach the loader even if it reports the same file through a different alias. - build.onLoad({ filter: exactPathFilter([resolvedTargetPath, canonicalTargetPath]) }, async (args) => { + build.onLoad({ filter: exactPathFilter([resolvedTargetPath, canonicalTargetPath]) }, async (args: any) => { const loadedPath = normalizeSourcePath(args.path) if (loadedPath !== canonicalTargetPath) { return undefined @@ -568,7 +568,7 @@ export function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): Bun build.onResolve({ filter: exactSpecifierFilter(specifier) }, () => ({ path: moduleId })) } - build.onResolve({ filter: /.*/ }, (args) => { + build.onResolve({ filter: /.*/ }, (args: any) => { if (runtimeModuleIdsBySpecifier.has(args.path) || args.path.startsWith(RUNTIME_MODULE_PREFIX)) { return undefined } From 2a3a68fd8e541202c2b1d352abbec841e6a269f3 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Fri, 10 Apr 2026 00:58:52 -0400 Subject: [PATCH 43/56] changes in react to make it work under node --- packages/react/src/reconciler/host-config.ts | 2 +- packages/react/src/reconciler/reconciler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/reconciler/host-config.ts b/packages/react/src/reconciler/host-config.ts index 0ffbea9dd..8a6f818e5 100644 --- a/packages/react/src/reconciler/host-config.ts +++ b/packages/react/src/reconciler/host-config.ts @@ -2,7 +2,7 @@ import { TextNodeRenderable, TextRenderable, type Renderable } from "@opentui/co import pkgJson from "../../package.json" import { createContext } from "react" import type { HostConfig, ReactContext } from "react-reconciler" -import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants" +import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants.js" import { getComponentCatalogue } from "../components/index.js" import { textNodeKeys, type TextNodeKey } from "../components/text.js" import type { Container, HostContext, Instance, Props, PublicInstance, TextInstance, Type } from "../types/host.js" diff --git a/packages/react/src/reconciler/reconciler.ts b/packages/react/src/reconciler/reconciler.ts index cff788c04..75a2f7d86 100644 --- a/packages/react/src/reconciler/reconciler.ts +++ b/packages/react/src/reconciler/reconciler.ts @@ -1,7 +1,7 @@ import type { RootRenderable } from "@opentui/core" import React from "react" import ReactReconciler from "react-reconciler" -import { ConcurrentRoot } from "react-reconciler/constants" +import { ConcurrentRoot } from "react-reconciler/constants.js" import { hostConfig } from "./host-config.js" export const reconciler = ReactReconciler(hostConfig) From d964bde37e76c74bd45c225c327a5cfff3cb702d Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Fri, 10 Apr 2026 01:13:38 -0400 Subject: [PATCH 44/56] test build articats: dist-test --- package.json | 3 +- packages/core/dist-test/bun/index.test.ts | 48 + packages/core/dist-test/bun/package.json | 14 + packages/core/dist-test/nodejs/index.js | 209 ++++ packages/core/dist-test/nodejs/package.json | 14 + packages/core/package.json | 5 +- packages/react/dist-test/bun/index.test.tsx | 117 +++ packages/react/dist-test/bun/package.json | 17 + .../index.tsx | 159 +++ .../loader.mjs | 21 + .../opentui-jsx.d.ts | 44 + .../package.json | 25 + .../react-reconciler-constants-shim.mjs | 16 + .../react-reconciler-shim.mjs | 39 + .../register-loader.mjs | 2 + .../tsconfig.json | 16 + .../dist-test/nodejs-typescript/index.tsx | 157 +++ .../dist-test/nodejs-typescript/package.json | 20 + .../dist-test/nodejs-typescript/tsconfig.json | 17 + packages/react/package.json | 6 +- .../dist-test/nodejs-typescript/build.mjs | 35 + .../dist-test/nodejs-typescript/index.tsx | 102 ++ .../dist-test/nodejs-typescript/package.json | 20 + packages/solid/package.json | 6 +- scripts/dist-test.ts | 974 ++++++++++++++++++ 25 files changed, 2079 insertions(+), 7 deletions(-) create mode 100644 packages/core/dist-test/bun/index.test.ts create mode 100644 packages/core/dist-test/bun/package.json create mode 100644 packages/core/dist-test/nodejs/index.js create mode 100644 packages/core/dist-test/nodejs/package.json create mode 100644 packages/react/dist-test/bun/index.test.tsx create mode 100644 packages/react/dist-test/bun/package.json create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/index.tsx create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/loader.mjs create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/opentui-jsx.d.ts create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/package.json create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-constants-shim.mjs create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-shim.mjs create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/register-loader.mjs create mode 100644 packages/react/dist-test/nodejs-typescript-vanilla-react18/tsconfig.json create mode 100644 packages/react/dist-test/nodejs-typescript/index.tsx create mode 100644 packages/react/dist-test/nodejs-typescript/package.json create mode 100644 packages/react/dist-test/nodejs-typescript/tsconfig.json create mode 100644 packages/solid/dist-test/nodejs-typescript/build.mjs create mode 100644 packages/solid/dist-test/nodejs-typescript/index.tsx create mode 100644 packages/solid/dist-test/nodejs-typescript/package.json create mode 100644 scripts/dist-test.ts diff --git a/package.json b/package.json index ca9148982..bd00b317e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "publish:solid": "cd packages/solid && bun run publish", "prepare-release": "bun scripts/prepare-release.ts", "test": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test", - "test:nodejs": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test:nodejs" + "test:nodejs": "bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test:nodejs", + "test:dist": "bun scripts/dist-test.ts --build packages/*/dist-test/*" }, "devDependencies": { "oxfmt": "0.41.0", diff --git a/packages/core/dist-test/bun/index.test.ts b/packages/core/dist-test/bun/index.test.ts new file mode 100644 index 000000000..0217332fa --- /dev/null +++ b/packages/core/dist-test/bun/index.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test" +import { createCliRenderer, TextRenderable } from "@opentui/core" +import { createTestRenderer } from "@opentui/core/testing" +import { createRuntimePlugin } from "@opentui/core/runtime-plugin" + +const nativePackageName = `@opentui/core-${process.platform}-${process.arch}` + +describe("@opentui/core dist test (Bun)", () => { + test("imports core public entrypoints", async () => { + const core = await import("@opentui/core") + const testing = await import("@opentui/core/testing") + const runtimePlugin = await import("@opentui/core/runtime-plugin") + + expect(typeof core.createCliRenderer).toBe("function") + expect(typeof core.TextRenderable).toBe("function") + expect(typeof testing.createTestRenderer).toBe("function") + expect(typeof runtimePlugin.createRuntimePlugin).toBe("function") + }) + + test("loads the platform-native package", async () => { + const nativePackage = await import(nativePackageName) + expect(typeof nativePackage.default).toBe("string") + }) + + test("renders a frame with createTestRenderer", async () => { + const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 20, + height: 4, + }) + + try { + const text = new TextRenderable(renderer, { content: "hello bun dist" }) + renderer.root.add(text) + await renderOnce() + + expect(captureCharFrame()).toMatch(/hello bun dist/) + } finally { + renderer.destroy() + } + }) + + test("createRuntimePlugin returns a valid plugin", () => { + const plugin = createRuntimePlugin() + expect(plugin).toBeDefined() + expect(typeof plugin.name).toBe("string") + expect(typeof plugin.setup).toBe("function") + }) +}) diff --git a/packages/core/dist-test/bun/package.json b/packages/core/dist-test/bun/package.json new file mode 100644 index 000000000..509644e54 --- /dev/null +++ b/packages/core/dist-test/bun/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentui/core-dist-test-bun", + "private": true, + "type": "module", + "engines": { + "bun": ">=1.3.0" + }, + "scripts": { + "test": "bun test index.test.ts" + }, + "dependencies": { + "@opentui/core": "*" + } +} diff --git a/packages/core/dist-test/nodejs/index.js b/packages/core/dist-test/nodejs/index.js new file mode 100644 index 000000000..6c6e6f022 --- /dev/null +++ b/packages/core/dist-test/nodejs/index.js @@ -0,0 +1,209 @@ +import assert from "node:assert/strict" +import process from "node:process" + +const nativePackageName = `@opentui/core-${process.platform}-${process.arch}` +const isNodeTest = process.env.NODE_TEST_CONTEXT !== undefined +const isMainModule = import.meta.main + +let corePromise +let testingPromise +let runtimePluginPromise + +const loadCore = async () => { + corePromise ??= import("@opentui/core") + return corePromise +} + +const loadTesting = async () => { + testingPromise ??= import("@opentui/core/testing") + return testingPromise +} + +const loadRuntimePlugin = async () => { + runtimePluginPromise ??= import("@opentui/core/runtime-plugin") + return runtimePluginPromise +} + +export async function createAsciiFontSelectionRoot(renderer) { + const { ASCIIFontRenderable, BoxRenderable, RGBA, TextRenderable } = await loadCore() + + renderer.setBackgroundColor("#0d1117") + + const root = new BoxRenderable(renderer, { + id: "ascii-font-dist-demo-root", + position: "absolute", + left: 1, + top: 1, + width: 76, + height: 20, + backgroundColor: "#161b22", + borderColor: "#50565d", + title: "ASCII Font Dist Demo", + titleAlignment: "center", + border: true, + }) + renderer.root.add(root) + + const subtitle = new TextRenderable(renderer, { + id: "ascii-font-dist-demo-subtitle", + content: "Packed Node.js consumer smoke test", + left: 2, + top: 1, + fg: "#f0f6fc", + }) + root.add(subtitle) + + const instructions = new TextRenderable(renderer, { + id: "ascii-font-dist-demo-instructions", + content: "Drag to select a font. Press C to clear. Press Ctrl+C to exit.", + left: 2, + top: 2, + fg: "#94a3b8", + }) + root.add(instructions) + + const tinyFont = new ASCIIFontRenderable(renderer, { + id: "ascii-font-dist-demo-tiny", + position: "absolute", + left: 2, + top: 4, + text: "NODE", + font: "tiny", + color: RGBA.fromInts(255, 215, 0, 255), + backgroundColor: RGBA.fromInts(0, 0, 32, 255), + selectionBg: "#4a5568", + selectionFg: "#ffffff", + }) + root.add(tinyFont) + + const blockFont = new ASCIIFontRenderable(renderer, { + id: "ascii-font-dist-demo-block", + position: "absolute", + left: 2, + top: 8, + text: "DIST", + font: "block", + color: [RGBA.fromInts(255, 120, 120, 255), RGBA.fromInts(120, 220, 255, 255)], + backgroundColor: RGBA.fromInts(0, 0, 32, 255), + selectionBg: "#4a5568", + selectionFg: "#ffffff", + }) + root.add(blockFont) + + const preview = new TextRenderable(renderer, { + id: "ascii-font-dist-demo-preview", + content: "Expected selection target: NODE", + left: 2, + top: 15, + fg: "#7dd3fc", + }) + root.add(preview) + + const statusText = new TextRenderable(renderer, { + id: "ascii-font-dist-demo-status", + content: "Selection: none", + left: 2, + top: 17, + fg: "#e6edf3", + }) + root.add(statusText) + + let statusMessage = "Selection: none" + const setStatusMessage = (message) => { + statusMessage = message + statusText.content = message + } + + renderer.on("selection", (selection) => { + const selectedText = selection?.getSelectedText() ?? "" + setStatusMessage(selectedText ? `Selection: ${selectedText}` : "Selection: empty") + }) + + renderer.keyInput.on("keypress", (event) => { + const key = event.sequence.toLowerCase() + if (key === "c") { + renderer.clearSelection() + setStatusMessage("Selection cleared") + } + }) + + return { + root, + fonts: [tinyFont, blockFont], + getStatusMessage: () => statusMessage, + destroy: () => { + renderer.clearSelection() + root.destroyRecursively() + }, + } +} + +if (isNodeTest) { + const { default: test } = await import("node:test") + + test("imports core public entrypoints", async () => { + const [core, testing, runtimePlugin] = await Promise.all([loadCore(), loadTesting(), loadRuntimePlugin()]) + + assert.equal(typeof core.createCliRenderer, "function") + assert.equal(typeof core.ASCIIFontRenderable, "function") + assert.equal(typeof testing.createTestRenderer, "function") + assert.equal(typeof runtimePlugin.createRuntimePlugin, "function") + }) + + test("loads the platform-native package", async () => { + try { + const nativePackage = await import(nativePackageName) + assert.equal(typeof nativePackage.default, "string") + } catch (error) { + assert.fail( + `Expected ${nativePackageName} to be installed for the dist test. ` + + `dist-test should install it automatically. Original error: ${error instanceof Error ? error.message : String(error)}`, + ) + } + }) + + test("renders the ASCII font selection demo and supports selection", async () => { + const [{ createTestRenderer }] = await Promise.all([loadTesting()]) + const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 80, + height: 24, + }) + + const demo = await createAsciiFontSelectionRoot(renderer) + + try { + await renderOnce() + + const firstFrame = captureCharFrame() + assert.match(firstFrame, /ASCII Font Dist Demo/) + assert.match(firstFrame, /Drag to select a font/) + assert.match(firstFrame, /Expected selection target: NODE/) + + const [tinyFont] = demo.fonts + renderer.startSelection(tinyFont, tinyFont.x, tinyFont.y) + renderer.updateSelection(tinyFont, tinyFont.x + tinyFont.width, tinyFont.y, { finishDragging: true }) + renderer.emit("selection", renderer.getSelection()) + + await renderOnce() + + assert.equal(renderer.getSelection()?.getSelectedText(), "NODE") + assert.equal(tinyFont.hasSelection(), true) + assert.equal(demo.getStatusMessage(), "Selection: NODE") + assert.match(captureCharFrame(), /Selection: NODE/) + } finally { + demo.destroy() + renderer.destroy() + } + }) +} + +if (isMainModule && !isNodeTest) { + const { createCliRenderer } = await loadCore() + const renderer = await createCliRenderer({ + targetFps: 30, + enableMouseMovement: true, + exitOnCtrlC: true, + }) + + await createAsciiFontSelectionRoot(renderer) +} diff --git a/packages/core/dist-test/nodejs/package.json b/packages/core/dist-test/nodejs/package.json new file mode 100644 index 000000000..db25c3870 --- /dev/null +++ b/packages/core/dist-test/nodejs/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentui/core-dist-test-nodejs", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "test": "node --test index.js" + }, + "dependencies": { + "@opentui/core": "*" + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 2989a6f2a..4c8348a2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,9 +23,10 @@ "bench:text-table": "bun src/benchmark/text-table-benchmark.ts", "bench:ts": "bun src/benchmark/native-span-feed-benchmark.ts --suite=quick --json=src/benchmark/latest-quick-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=default --json=src/benchmark/latest-default-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=large --json=src/benchmark/latest-large-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=all --json=src/benchmark/latest-all-bench-run.json && bun src/benchmark/native-span-feed-async-benchmark.ts --json=src/benchmark/latest-async-bench-run.json", "publish": "bun scripts/publish.ts", - "test:js": "bun test", + "test:bun": "bun test", "test:nodejs": "npx vitest run", - "test": "bun run test:native && bun run test:js && bun run test:nodejs" + "test:dist": "bun ../../scripts/dist-test.ts ./dist-test/*", + "test": "bun run test:native && bun run test:bun && bun run test:nodejs && bun run test:dist" }, "license": "MIT", "devDependencies": { diff --git a/packages/react/dist-test/bun/index.test.tsx b/packages/react/dist-test/bun/index.test.tsx new file mode 100644 index 000000000..b992c17e8 --- /dev/null +++ b/packages/react/dist-test/bun/index.test.tsx @@ -0,0 +1,117 @@ +import { describe, expect, test } from "bun:test" +import { createTestRenderer } from "@opentui/core/testing" +import { createRoot } from "@opentui/react" +import { testRender } from "@opentui/react/test-utils" +import { act, useState, type ReactNode } from "react" + +describe("@opentui/react dist test (Bun)", () => { + test("imports react public entrypoints", async () => { + const react = await import("@opentui/react") + const testUtils = await import("@opentui/react/test-utils") + + expect(typeof react.createRoot).toBe("function") + expect(typeof react.useKeyboard).toBe("function") + expect(typeof react.useRenderer).toBe("function") + expect(typeof testUtils.testRender).toBe("function") + }) + + test("renders simple text via testRender", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + Hello from React Bun dist test, + { width: 40, height: 4 }, + ) + + try { + await renderOnce() + expect(captureCharFrame()).toMatch(/Hello from React Bun dist test/) + } finally { + renderer.destroy() + } + }) + + test("renders nested box layout", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Line A + Line B + , + { width: 30, height: 6 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + expect(frame).toMatch(/Line A/) + expect(frame).toMatch(/Line B/) + } finally { + renderer.destroy() + } + }) + + test("renders a stateful component", async () => { + function Counter({ initial }: { initial: number }): ReactNode { + const [count] = useState(initial) + return {`Count: ${count}`} + } + + const { renderer, renderOnce, captureCharFrame } = await testRender( + , + { width: 20, height: 4 }, + ) + + try { + await renderOnce() + expect(captureCharFrame()).toMatch(/Count: 42/) + } finally { + renderer.destroy() + } + }) + + test("renders a box with border", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Boxed content + , + { width: 30, height: 8 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + expect(frame).toMatch(/Greetings/) + expect(frame).toMatch(/Boxed content/) + } finally { + renderer.destroy() + } + }) + + test("uses createRoot directly with createTestRenderer", async () => { + let root: ReturnType | null = null + const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 30, + height: 4, + onDestroy() { + act(() => { + root?.unmount() + root = null + }) + }, + }) + + // @ts-expect-error - required for React act() to work in test environment + globalThis.IS_REACT_ACT_ENVIRONMENT = true + root = createRoot(renderer) + + try { + act(() => { + root.render(Direct root render) + }) + await renderOnce() + expect(captureCharFrame()).toMatch(/Direct root render/) + } finally { + renderer.destroy() + // @ts-expect-error + globalThis.IS_REACT_ACT_ENVIRONMENT = false + } + }) +}) diff --git a/packages/react/dist-test/bun/package.json b/packages/react/dist-test/bun/package.json new file mode 100644 index 000000000..1ff91ba00 --- /dev/null +++ b/packages/react/dist-test/bun/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentui/react-dist-test-bun", + "private": true, + "type": "module", + "engines": { + "bun": ">=1.3.0" + }, + "scripts": { + "test": "bun test index.test.tsx" + }, + "dependencies": { + "@opentui/core": "*", + "@opentui/react": "*", + "react": ">=19.0.0", + "@types/react": "^19.0.0" + } +} diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/index.tsx b/packages/react/dist-test/nodejs-typescript-vanilla-react18/index.tsx new file mode 100644 index 000000000..519418db8 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/index.tsx @@ -0,0 +1,159 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { createCliRenderer } from "@opentui/core" +import { createTestRenderer } from "@opentui/core/testing" +import { createRoot } from "@opentui/react" +import { testRender } from "@opentui/react/test-utils" +import { useState, useEffect, type ReactNode } from "react" +// In React 18, act is exported from react-dom/test-utils +import { act } from "react-dom/test-utils" + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("@opentui/react dist test (Node.js + TypeScript, vanilla React 18)", () => { + it("imports react public entrypoints", async () => { + const react = await import("@opentui/react") + const testUtils = await import("@opentui/react/test-utils") + + assert.equal(typeof react.createRoot, "function") + assert.equal(typeof react.useKeyboard, "function") + assert.equal(typeof react.useRenderer, "function") + assert.equal(typeof testUtils.testRender, "function") + }) + + it("renders simple text via testRender", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + Hello from React 18 dist test, + { width: 40, height: 4 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Hello from React 18 dist test/) + } finally { + renderer.destroy() + } + }) + + it("renders nested box layout", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Line A + Line B + , + { width: 30, height: 6 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Line A/) + assert.match(frame, /Line B/) + } finally { + renderer.destroy() + } + }) + + it("renders a stateful component", async () => { + function Counter({ initial }: { initial: number }): ReactNode { + const [count] = useState(initial) + return {`Count: ${count}`} + } + + const { renderer, renderOnce, captureCharFrame } = await testRender( + , + { width: 20, height: 4 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Count: 42/) + } finally { + renderer.destroy() + } + }) + + it("renders a box with border", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Boxed content + , + { width: 30, height: 8 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Greetings/) + assert.match(frame, /Boxed content/) + } finally { + renderer.destroy() + } + }) + + it("uses createRoot directly with createTestRenderer", async () => { + let root: ReturnType | null = null + const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 30, + height: 4, + onDestroy() { + act(() => { + root?.unmount() + root = null + }) + }, + }) + + // @ts-expect-error - required for React act() to work in test environment + globalThis.IS_REACT_ACT_ENVIRONMENT = true + root = createRoot(renderer) + + try { + act(() => { + root.render(Direct root render) + }) + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Direct root render/) + } finally { + renderer.destroy() + // @ts-expect-error + globalThis.IS_REACT_ACT_ENVIRONMENT = false + } + }) +}) + +// --------------------------------------------------------------------------- +// Interactive app — runs when executed directly: node dist/index.js +// --------------------------------------------------------------------------- + +function InteractiveApp(): ReactNode { + const [counter, setCounter] = useState(0) + const [dots, setDots] = useState("") + + useEffect(() => { + const interval = setInterval(() => { + setCounter((c) => c + 1) + setDots((d) => (d.length >= 3 ? "" : d + ".")) + }, 500) + return () => clearInterval(interval) + }, []) + + return ( + + + {`Counter: ${counter}${dots}`} + + Press Ctrl+C to exit + + ) +} + +if (process.env.NODE_TEST_CONTEXT === undefined && import.meta.main) { + const renderer = await createCliRenderer() + createRoot(renderer).render() +} diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/loader.mjs b/packages/react/dist-test/nodejs-typescript-vanilla-react18/loader.mjs new file mode 100644 index 000000000..b15f6e718 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/loader.mjs @@ -0,0 +1,21 @@ +// Node.js module resolution hook that redirects react-reconciler imports +// to local ESM shims, bridging API differences between 0.29 (React 18) and 0.32 (React 19). +import { pathToFileURL } from "node:url" +import { resolve as pathResolve } from "node:path" +import { fileURLToPath } from "node:url" + +const dir = fileURLToPath(new URL(".", import.meta.url)) + +const shims = { + "react-reconciler/constants.js": pathToFileURL(pathResolve(dir, "react-reconciler-constants-shim.mjs")).href, + "react-reconciler/constants": pathToFileURL(pathResolve(dir, "react-reconciler-constants-shim.mjs")).href, + "react-reconciler": pathToFileURL(pathResolve(dir, "react-reconciler-shim.mjs")).href, +} + +export function resolve(specifier, context, nextResolve) { + const shimUrl = shims[specifier] + if (shimUrl) { + return { url: shimUrl, shortCircuit: true } + } + return nextResolve(specifier, context) +} diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/opentui-jsx.d.ts b/packages/react/dist-test/nodejs-typescript-vanilla-react18/opentui-jsx.d.ts new file mode 100644 index 000000000..f7a57024a --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/opentui-jsx.d.ts @@ -0,0 +1,44 @@ +import type { + BoxProps, + TextProps, + SpanProps, + CodeProps, + DiffProps, + MarkdownProps, + InputProps, + TextareaProps, + SelectProps, + ScrollBoxProps, + AsciiFontProps, + TabSelectProps, + LineNumberProps, + LineBreakProps, + LinkProps, +} from "@opentui/react" + +declare module "react" { + namespace JSX { + interface IntrinsicElements { + box: BoxProps + text: TextProps + span: SpanProps + code: CodeProps + diff: DiffProps + markdown: MarkdownProps + input: InputProps + textarea: TextareaProps + select: SelectProps + scrollbox: ScrollBoxProps + "ascii-font": AsciiFontProps + "tab-select": TabSelectProps + "line-number": LineNumberProps + b: SpanProps + i: SpanProps + u: SpanProps + strong: SpanProps + em: SpanProps + br: LineBreakProps + a: LinkProps + } + } +} diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/package.json b/packages/react/dist-test/nodejs-typescript-vanilla-react18/package.json new file mode 100644 index 000000000..22e7f0159 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/package.json @@ -0,0 +1,25 @@ +{ + "name": "@opentui/react-dist-test-nodejs-typescript-vanilla-react18", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "npx tsgo --project tsconfig.json", + "test": "node --import ./register-loader.mjs --test dist/index.js" + }, + "dependencies": { + "@opentui/core": "*", + "@opentui/react": "*", + "@typescript/native-preview": "latest", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@types/node": "^24.0.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "overrides": { + "react-reconciler": "0.29.2" + } +} diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-constants-shim.mjs b/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-constants-shim.mjs new file mode 100644 index 000000000..f3ac174d8 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-constants-shim.mjs @@ -0,0 +1,16 @@ +// ESM re-export of react-reconciler/constants for React 18's CJS build, +// which Node.js cannot statically analyze for named exports. +// Also polyfills NoEventPriority (added in react-reconciler 0.32 / React 19). +import { createRequire } from "node:module" +const require = createRequire(import.meta.url) +const constants = require("react-reconciler/constants") +export const { + ConcurrentRoot, + ContinuousEventPriority, + DefaultEventPriority, + DiscreteEventPriority, + IdleEventPriority, + LegacyRoot, +} = constants +// NoEventPriority was added in react-reconciler 0.32; in 0.29 it's 0. +export const NoEventPriority = constants.NoEventPriority ?? 0 diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-shim.mjs b/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-shim.mjs new file mode 100644 index 000000000..c3f268034 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/react-reconciler-shim.mjs @@ -0,0 +1,39 @@ +// ESM wrapper for react-reconciler that patches API differences between +// react-reconciler 0.29 (React 18) and 0.32 (React 19). +import { createRequire } from "node:module" +const require = createRequire(import.meta.url) +const Reconciler = require("react-reconciler") +const constants = require("react-reconciler/constants") + +function PatchedReconciler(hostConfig) { + // react-reconciler 0.29 requires getCurrentEventPriority in the host config, + // but 0.32 removed it. Provide a default that returns DefaultEventPriority. + const patchedConfig = { ...hostConfig } + if (!patchedConfig.getCurrentEventPriority) { + patchedConfig.getCurrentEventPriority = () => constants.DefaultEventPriority + } + + const instance = Reconciler(patchedConfig) + + // 0.32 added flushSyncWork; 0.29 has flushSync instead. + if (!instance.flushSyncWork && instance.flushSync) { + instance.flushSyncWork = instance.flushSync + } + + // 0.32 allows injectIntoDevTools() with no args; 0.29 requires a config object. + const originalInject = instance.injectIntoDevTools + instance.injectIntoDevTools = function (config) { + if (!config) { + return originalInject.call(this, { + bundleType: 0, + version: "18.3.1", + rendererPackageName: "@opentui/react", + }) + } + return originalInject.call(this, config) + } + + return instance +} + +export default PatchedReconciler diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/register-loader.mjs b/packages/react/dist-test/nodejs-typescript-vanilla-react18/register-loader.mjs new file mode 100644 index 000000000..4abfc2c2c --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/register-loader.mjs @@ -0,0 +1,2 @@ +import { register } from "node:module" +register("./loader.mjs", import.meta.url) diff --git a/packages/react/dist-test/nodejs-typescript-vanilla-react18/tsconfig.json b/packages/react/dist-test/nodejs-typescript-vanilla-react18/tsconfig.json new file mode 100644 index 000000000..afd750ff9 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript-vanilla-react18/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "verbatimModuleSyntax": true, + "types": ["node"] + }, + "include": ["index.tsx", "opentui-jsx.d.ts"] +} diff --git a/packages/react/dist-test/nodejs-typescript/index.tsx b/packages/react/dist-test/nodejs-typescript/index.tsx new file mode 100644 index 000000000..a6a5e8024 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript/index.tsx @@ -0,0 +1,157 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { createCliRenderer } from "@opentui/core" +import { createTestRenderer } from "@opentui/core/testing" +import { createRoot } from "@opentui/react" +import { testRender } from "@opentui/react/test-utils" +import { act, useState, useEffect, type ReactNode } from "react" + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("@opentui/react dist test (Node.js + TypeScript)", () => { + it("imports react public entrypoints", async () => { + const react = await import("@opentui/react") + const testUtils = await import("@opentui/react/test-utils") + + assert.equal(typeof react.createRoot, "function") + assert.equal(typeof react.useKeyboard, "function") + assert.equal(typeof react.useRenderer, "function") + assert.equal(typeof testUtils.testRender, "function") + }) + + it("renders simple text via testRender", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + Hello from React dist test, + { width: 40, height: 4 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Hello from React dist test/) + } finally { + renderer.destroy() + } + }) + + it("renders nested box layout", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Line A + Line B + , + { width: 30, height: 6 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Line A/) + assert.match(frame, /Line B/) + } finally { + renderer.destroy() + } + }) + + it("renders a stateful component", async () => { + function Counter({ initial }: { initial: number }): ReactNode { + const [count] = useState(initial) + return {`Count: ${count}`} + } + + const { renderer, renderOnce, captureCharFrame } = await testRender( + , + { width: 20, height: 4 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Count: 42/) + } finally { + renderer.destroy() + } + }) + + it("renders a box with border", async () => { + const { renderer, renderOnce, captureCharFrame } = await testRender( + + Boxed content + , + { width: 30, height: 8 }, + ) + + try { + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Greetings/) + assert.match(frame, /Boxed content/) + } finally { + renderer.destroy() + } + }) + + it("uses createRoot directly with createTestRenderer", async () => { + let root: ReturnType | null = null + const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({ + width: 30, + height: 4, + onDestroy() { + act(() => { + root?.unmount() + root = null + }) + }, + }) + + // @ts-expect-error - required for React act() to work in test environment + globalThis.IS_REACT_ACT_ENVIRONMENT = true + root = createRoot(renderer) + + try { + act(() => { + root.render(Direct root render) + }) + await renderOnce() + const frame = captureCharFrame() + assert.match(frame, /Direct root render/) + } finally { + renderer.destroy() + // @ts-expect-error + globalThis.IS_REACT_ACT_ENVIRONMENT = false + } + }) +}) + +// --------------------------------------------------------------------------- +// Interactive app — runs when executed directly: node dist/index.js +// --------------------------------------------------------------------------- + +function InteractiveApp(): ReactNode { + const [counter, setCounter] = useState(0) + const [dots, setDots] = useState("") + + useEffect(() => { + const interval = setInterval(() => { + setCounter((c) => c + 1) + setDots((d) => (d.length >= 3 ? "" : d + ".")) + }, 500) + return () => clearInterval(interval) + }, []) + + return ( + + + {`Counter: ${counter}${dots}`} + + Press Ctrl+C to exit + + ) +} + +if (process.env.NODE_TEST_CONTEXT === undefined && import.meta.main) { + const renderer = await createCliRenderer() + createRoot(renderer).render() +} diff --git a/packages/react/dist-test/nodejs-typescript/package.json b/packages/react/dist-test/nodejs-typescript/package.json new file mode 100644 index 000000000..5bdb38ed9 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentui/react-dist-test-nodejs-typescript", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "npx tsgo --project tsconfig.json", + "test": "node --test dist/index.js" + }, + "dependencies": { + "@opentui/core": "*", + "@opentui/react": "*", + "@typescript/native-preview": "latest", + "@types/react": "^19.0.0", + "@types/node": "^24.0.0", + "react": ">=19.0.0" + } +} diff --git a/packages/react/dist-test/nodejs-typescript/tsconfig.json b/packages/react/dist-test/nodejs-typescript/tsconfig.json new file mode 100644 index 000000000..e11ea4962 --- /dev/null +++ b/packages/react/dist-test/nodejs-typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "jsxImportSource": "@opentui/react", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "verbatimModuleSyntax": true, + "types": ["node"] + }, + "include": ["index.tsx"] +} diff --git a/packages/react/package.json b/packages/react/package.json index 5ac3a698a..e3ae55cd5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,8 +39,10 @@ "build:examples": "bun examples/build.ts", "build:dev": "bun run scripts/build.ts --dev", "publish": "bun run scripts/publish.ts", - "test": "bun test", - "test:nodejs": "npx vitest run" + "test:bun": "bun test", + "test:dist": "bun ../../scripts/dist-test.ts ./dist-test/*", + "test:nodejs": "npx vitest run", + "test": "bun run test:bun && bun run test:nodejs && bun run test:dist" }, "devDependencies": { "@types/bun": "latest", diff --git a/packages/solid/dist-test/nodejs-typescript/build.mjs b/packages/solid/dist-test/nodejs-typescript/build.mjs new file mode 100644 index 000000000..aeb177016 --- /dev/null +++ b/packages/solid/dist-test/nodejs-typescript/build.mjs @@ -0,0 +1,35 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { transformAsync } from "@babel/core" +import ts from "@babel/preset-typescript" +import solid from "babel-preset-solid" + +const rootDir = dirname(fileURLToPath(import.meta.url)) +const inputPath = join(rootDir, "index.tsx") +const outputDir = join(rootDir, "dist") +const outputPath = join(outputDir, "index.js") + +const input = readFileSync(inputPath, "utf8") +const transformed = await transformAsync(input, { + filename: inputPath, + configFile: false, + babelrc: false, + presets: [ + [ + solid, + { + moduleName: "@opentui/solid", + generate: "universal", + }, + ], + [ts], + ], +}) + +if (!transformed?.code) { + throw new Error(`Failed to transform ${inputPath}`) +} + +mkdirSync(outputDir, { recursive: true }) +writeFileSync(outputPath, transformed.code) diff --git a/packages/solid/dist-test/nodejs-typescript/index.tsx b/packages/solid/dist-test/nodejs-typescript/index.tsx new file mode 100644 index 000000000..635a41acf --- /dev/null +++ b/packages/solid/dist-test/nodejs-typescript/index.tsx @@ -0,0 +1,102 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { render, testRender, useRenderer } from "@opentui/solid" +import { onMount } from "solid-js" + +const initialDraft = "Welcome to the Solid dist test." + +export function SolidDistTextareaDemo() { + const renderer = useRenderer() + + onMount(() => { + renderer.setBackgroundColor("#111827") + }) + + return ( + + +