From 69a797616a5054efdfa0e3ff335779f00618c758 Mon Sep 17 00:00:00 2001 From: Mike Pitre <12040919+mikepitre@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:52:51 -0400 Subject: [PATCH 01/20] chore(repo): add the integration/expo-native package and its unit test job The package installs its own dependencies with npm ci from its own lockfile, outside the pnpm workspace. The Expo Native Runner Tests job runs its unit tests and tsc when a pull request changes it. Co-Authored-By: Claude Opus 5.5 --- .changeset/expo-verify-skill.md | 2 + .github/workflows/ci.yml | 58 + .prettierignore | 15 + eslint.config.mjs | 1 + integration/expo-native/.gitignore | 3 + integration/expo-native/package-lock.json | 1783 +++++++++++++++++++++ integration/expo-native/package.json | 20 + integration/expo-native/tsconfig.json | 16 + 8 files changed, 1898 insertions(+) create mode 100644 .changeset/expo-verify-skill.md create mode 100644 integration/expo-native/.gitignore create mode 100644 integration/expo-native/package-lock.json create mode 100644 integration/expo-native/package.json create mode 100644 integration/expo-native/tsconfig.json diff --git a/.changeset/expo-verify-skill.md b/.changeset/expo-verify-skill.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-skill.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee010ea28b4..6a47ac61ac6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,64 @@ jobs: pnpm changeset status --since=origin/main; fi + expo-native-runner-tests: + needs: [check-permissions] + name: Expo Native Runner Tests + runs-on: 'blacksmith-8vcpu-ubuntu-2204' + defaults: + run: + shell: bash + timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 5 }} + + env: + PACKAGE: integration/expo-native + WATCHED: >- + integration/expo-native + .github/workflows/ci.yml + .github/workflows/verify-remote.yml + .github/actionlint.yaml + integration/templates/expo-native + packages/expo + packages/expo-biometrics + packages/expo-google-signin + packages/expo-native-components + + steps: + - name: Checkout Repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + fetch-depth: 2 # The pull request merge commit and its parents, to see what the PR changes + fetch-tags: false + show-progress: false + + - name: Check whether the package changed + id: changed + env: + EVENT: ${{ github.event_name }} + run: | + if [ "$EVENT" != "pull_request" ] || ! git diff --quiet HEAD^1 HEAD -- $WATCHED; then + echo "package=true" >> "$GITHUB_OUTPUT" + fi + + - name: Setup Node + if: steps.changed.outputs.package == 'true' + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24.15.0 + + - name: Install the package's own dependencies + if: steps.changed.outputs.package == 'true' + run: npm ci --prefix "$PACKAGE" + + - name: Run the package's unit tests, which include the check of the copied core against its manifest + if: steps.changed.outputs.package == 'true' + run: npm test --prefix "$PACKAGE" + + - name: Typecheck the package + if: steps.changed.outputs.package == 'true' + run: npm run typecheck --prefix "$PACKAGE" + build-packages: needs: [check-permissions] name: Build Packages diff --git a/.prettierignore b/.prettierignore index fc97a58a963..bc6c89c0c5c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,3 +29,18 @@ CLAUDE.md # Written by `eslint --suppress-rule` / `--prune-suppressions`; ESLint owns the formatting. /**/eslint-suppressions.json + +# Copied byte for byte from another repo; do not reformat. +integration/expo-native/src/core/ +integration/expo-native/src/platform/android/ +integration/expo-native/src/platform/ios/ +integration/expo-native/specs/fixtures.ts +integration/expo-native/specs/support/ +integration/expo-native/testing/ +integration/expo-native/e2e.config.ts +integration/expo-native/test/*.ts +!integration/expo-native/test/freshness.test.ts +!integration/expo-native/test/host.test.ts +!integration/expo-native/test/remote-host.test.ts +integration/expo-native/.verify/ +integration/expo-native/specs/explored/ diff --git a/eslint.config.mjs b/eslint.config.mjs index d915da10caa..a5a521fd643 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -326,6 +326,7 @@ export default tseslint.config([ '**/build/*', '**/coverage/*', '**/dist/*', + '**/integration/expo-native/**/*', '**/integration/templates/**/*', '**/node_modules/**', '*.snap', diff --git a/integration/expo-native/.gitignore b/integration/expo-native/.gitignore new file mode 100644 index 00000000000..603ba938f6c --- /dev/null +++ b/integration/expo-native/.gitignore @@ -0,0 +1,3 @@ +.e2e/ +.verify/ +specs/explored/ diff --git a/integration/expo-native/package-lock.json b/integration/expo-native/package-lock.json new file mode 100644 index 00000000000..b3679103700 --- /dev/null +++ b/integration/expo-native/package-lock.json @@ -0,0 +1,1783 @@ +{ + "name": "verify-clerk-expo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "verify-clerk-expo", + "devDependencies": { + "@e2e-dev/github": "0.4.0", + "@e2e-dev/mobile": "0.10.0", + "@types/node": "24.19.1", + "ai": "7.0.128", + "e2e": "0.18.0", + "typescript": "7.0.2" + }, + "engines": { + "node": ">=24.8.0 <25" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "4.0.104", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.104.tgz", + "integrity": "sha512-4lTyt8a4BMfVz4QGo+VO5odakmYMX5kq+xiRgdQGGY7o/jP2WSasp6LvIvYvt/lHzz1MfE+xKTOJhz7mMwjynA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.21.tgz", + "integrity": "sha512-UpbC9C1oht8dhfKPbVXSLRZS3DI8uk8n5v2uMjBPNIYGsC2kL045ywq7D8Hh9KgnH9rP5W/GxQdYtWScRouqlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@e2e-dev/github": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@e2e-dev/github/-/github-0.4.0.tgz", + "integrity": "sha512-h4F51Ncwk9VWgdPgAJCjXvqQrBd6sQSt3yvpl9XNJkLTOccndr3A6eAfw6iY49KvNOpZicdQ/glDHxs1c4Lm7Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^22.22.3 || >=24.8.0" + }, + "peerDependencies": { + "e2e": ">=0.15.0 <1" + } + }, + "node_modules/@e2e-dev/mobile": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@e2e-dev/mobile/-/mobile-0.10.0.tgz", + "integrity": "sha512-Oz6ePagl8S/WnCQlfw0WuABpGAgMKK0mTshzuO/p8XEdttwBfO9sJsAWG7PiRF9dwEyU1IkCu8eNdtv/pk3akA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "agent-device": "0.21.22", + "pngjs": "7.0.0", + "zod": "4.6.1" + }, + "engines": { + "node": "^22.22.3 || >=24.8.0" + }, + "peerDependencies": { + "ai": "^7.0.0", + "e2e": ">=0.15.0 <1" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.0.tgz", + "integrity": "sha512-09BHFaNVBe5pB2AhzCfLj0cqmsKLERvZQ4FblzP90UMGLaXVqdZPByFjURwsUYCmxonUW2yAwgTTvle+8O2/8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.0.tgz", + "integrity": "sha512-+6b0LdsQLmHsvS7J6sQJ4weTpzUn61fU1JKS+321/+IyDYoHW5CR2Tgqcs6GgAAQvlesCo6yX3xttgmx+XGtJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.152.0.tgz", + "integrity": "sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.152.0.tgz", + "integrity": "sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.152.0.tgz", + "integrity": "sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.152.0.tgz", + "integrity": "sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.152.0.tgz", + "integrity": "sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.152.0.tgz", + "integrity": "sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.152.0.tgz", + "integrity": "sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.152.0.tgz", + "integrity": "sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.152.0.tgz", + "integrity": "sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.152.0.tgz", + "integrity": "sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.152.0.tgz", + "integrity": "sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.152.0.tgz", + "integrity": "sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.152.0.tgz", + "integrity": "sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.152.0.tgz", + "integrity": "sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.152.0.tgz", + "integrity": "sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.152.0.tgz", + "integrity": "sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.152.0.tgz", + "integrity": "sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.152.0.tgz", + "integrity": "sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.152.0.tgz", + "integrity": "sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.152.0.tgz", + "integrity": "sha512-fsq/7JVJo8YAmMPnt3Z7akMdMoCokRND5jjawa5DxuKJBEaI0Dl3ZqEXFlLabKoZhlApIexZ7aycXFcjcTcWvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxc-transform/binding-android-arm-eabi": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm-eabi/-/binding-android-arm-eabi-0.152.0.tgz", + "integrity": "sha512-GBVYZdUggANq7ndUuE9HBi1jO6d2+oud3cevfQkpqrV7JsDNwQU5UCGa8rZCvzjIvyn6ikQuJu+sj4RvLyN6Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-android-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.152.0.tgz", + "integrity": "sha512-pqeCLGUnozzyJE9Ed9js2w+V1lHQ43qPImMfgTh7it+a3LJqUjAt+OesJ6pqkPycRFV6b0pUWZm6EkpM8mk7ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-arm64/-/binding-darwin-arm64-0.152.0.tgz", + "integrity": "sha512-0d68wdepj2bjnDijPdvC/3MnrZcs8hoCp19qnGX2tyxnSIPevgqa9RX5i31ex5JMczht/VGp4aKeV1p12dwzQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-x64/-/binding-darwin-x64-0.152.0.tgz", + "integrity": "sha512-jFTRTyZzYhqSrxkaI9hTwClPTXtdqRyPSXUFGLJmStYS+b6qnhbI1ottivWdAg0HJXytyg8YeqgUJWSu2lU/jA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-freebsd-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-freebsd-x64/-/binding-freebsd-x64-0.152.0.tgz", + "integrity": "sha512-L4Y8IfrcaxjK9kK3ddRAyZPa6nzJHn6fgkVIKoBlP53vmCobPL1mtuIdXQThD0VmV6e4aSAJg2An6qkdjeZC8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-gnueabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.152.0.tgz", + "integrity": "sha512-GrohqmIKEK/YXCzAGP8XGQrPVBdUHPpvqdhvw+VutpWZofxDOq39Zed+Gqlea2mycXo+/UNeq1194eaAYkqKQQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-musleabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.152.0.tgz", + "integrity": "sha512-wUI+JoZEs4+GntxjK2YOKtQDG822wv+aGYm1OUZ2+NAgi8ghOTUP3RH/GX8Vq0qJ2Yw3OwKLUIZzufXklQ7pCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.152.0.tgz", + "integrity": "sha512-EcY52e0BOfynOA/brCInvQO3ZSukbAEc+BsbpRoRdJ4qV10NK0aS/FheBvkj/V3cFidee6QlrUpqgpN4WKV+Sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.152.0.tgz", + "integrity": "sha512-L4OGw0UuuyYKAPnUvvW+tvq6nS/tBoolVuPekNJqYnDYfe5Nh1619ImfhP0o0Wz+SG4OQW35ByRWjVf/oK3x0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-ppc64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.152.0.tgz", + "integrity": "sha512-5K9qNjNKlyhBdGkkIhYFonO/H3Yc37miy5cXefTB71aPOv9jv6HQrDuj6AJcBkggbmkeyEJhlFamUbGMsMGBiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.152.0.tgz", + "integrity": "sha512-WlGvnZhmRItWqjsGd2OBKPan75FbqpF4HPuGL7P6f7Oz1VHm6IatnwCK2MI16QXH7BEs6GRohZHpmgThc0V/uw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.152.0.tgz", + "integrity": "sha512-TWZX7QGkU4LF9NS+HBUVWsbQXWDuWt58r+L/31gSJ/iHCJw/3jM2yD408IRVjXYX5jFgmsJbxrUlW3xlVeAL3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-s390x-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.152.0.tgz", + "integrity": "sha512-VIP7463d0nIAGFv2VXb4mRf20qrpZg0kNHBw8E3fLuHj0bt4HVHDgsqAgeFWOonWwSwsM415TFjNmHhlaStCLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.152.0.tgz", + "integrity": "sha512-hwnHMVCnGIoS/eMh7T836mJpWim2iCSWGlCS+aZ+SjhEEUvSSUE54ljua7bk25vaj4L84cFnruTCa7HqyaQ2YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-musl/-/binding-linux-x64-musl-0.152.0.tgz", + "integrity": "sha512-5NPItMgM9GfIW0SbpOXvEFIijin5VcNEParmBEm9j+RXr8Ko7pZ3a1ydk/Sy+EOvaSTkXMY6TkVyLfczwdudSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-openharmony-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-openharmony-arm64/-/binding-openharmony-arm64-0.152.0.tgz", + "integrity": "sha512-pNzMq4AhBUV8E/cc41SgYTjzW/AWq1DtEKz1l56Mn1TKfidJwP8sGnIw0EXhD6zKFUlPoqkWfvoe5TiDAYeT+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-arm64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.152.0.tgz", + "integrity": "sha512-PU6cGmVDYo2HHG9ul6vKj189YJPZrcscdWzQakJD6NU4CqvqISbZLUHwz6DVe0v8CbOe2d24zUdipILaRofZ5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-ia32-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.152.0.tgz", + "integrity": "sha512-Sdcoky580dPgj9Qp+i4zrCV7NpIYZW6bBrcQvOwyDmNlJQ2iYK/V3Q3FGNHLulrxgXrpzqQwMcLs8KRIg0u0yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-x64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.152.0.tgz", + "integrity": "sha512-QnN+/Hsig/vI9zoCNVbbqv0GmZWUGqlPg2ZTEyVw/PvgAXd8pRRfN6CxdKEtJTnaVwJFVYaw/5LR6ZWCVBwDeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vitest/expect": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-5.0.3.tgz", + "integrity": "sha512-N/VGY122AqBx25OB4EatqlVu8PBayvm9dQzpJ8kVq/pOvqAhXTE5MAmoSMzdB8klDU9wcgSEUliwgMNMtxPa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "5.0.3", + "@vitest/utils": "5.0.3", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.3.tgz", + "integrity": "sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.3.tgz", + "integrity": "sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "5.0.3", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@workflow/serde": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0.tgz", + "integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/agent-device": { + "version": "0.21.22", + "resolved": "https://registry.npmjs.org/agent-device/-/agent-device-0.21.22.tgz", + "integrity": "sha512-ndUh5b+lZ2fRzzY80TTl/KmxR4bh+3FezSH0UGCjPCpN//1trSqtzzNUWunc4woY/vW3DEiPx5xYIwG+fBOXWA==", + "dev": true, + "license": "MIT", + "bin": { + "agent-device": "bin/agent-device.mjs" + }, + "engines": { + "node": ">=22.12" + }, + "peerDependencies": { + "ai": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } + } + }, + "node_modules/ai": { + "version": "7.0.128", + "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.128.tgz", + "integrity": "sha512-WMKN83DodYtH52ETPHq0jrSwj3wU0aHkhLHXMrwMRabC/aMpIqeWsXJgjkUwrOqkaKHWONdupk/2ttPRvAyFcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "4.0.104", + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ai/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/e2e": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/e2e/-/e2e-0.18.0.tgz", + "integrity": "sha512-mVIJtGXYBMLRK12CESeOowuh86nDDBhjfHIuyELuSFbaEmzl4EC4tVhm3T4Y6jidEGnGexZOATzlQ7tM/xw8JA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.21", + "@clack/prompts": "1.8.1", + "@jridgewell/remapping": "2.3.5", + "@modelcontextprotocol/server": "2.3.0", + "@oxc-project/runtime": "0.152.0", + "@vitest/expect": "5.0.3", + "commander": "15.0.0", + "get-tsconfig": "4.14.3", + "oxc-parser": "0.152.0", + "oxc-transform": "0.152.0", + "picocolors": "1.1.1", + "pngjs": "7.0.0", + "zod": "4.6.1" + }, + "bin": { + "e2e": "dist/cli/bin.js" + }, + "engines": { + "node": "^22.22.3 || >=24.8.0" + }, + "peerDependencies": { + "@ai-sdk/anthropic": "^4.0.0", + "@ai-sdk/google": "^4.0.0", + "@ai-sdk/openai": "^4.0.0", + "@ai-sdk/openai-compatible": "^3.0.0", + "@ai-sdk/xai": "^5.0.0", + "ai": "^7.0.0" + }, + "peerDependenciesMeta": { + "@ai-sdk/anthropic": { + "optional": true + }, + "@ai-sdk/google": { + "optional": true + }, + "@ai-sdk/openai": { + "optional": true + }, + "@ai-sdk/openai-compatible": { + "optional": true + }, + "@ai-sdk/xai": { + "optional": true + }, + "ai": { + "optional": true + } + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/oxc-parser": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.152.0.tgz", + "integrity": "sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.152.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.152.0", + "@oxc-parser/binding-android-arm64": "0.152.0", + "@oxc-parser/binding-darwin-arm64": "0.152.0", + "@oxc-parser/binding-darwin-x64": "0.152.0", + "@oxc-parser/binding-freebsd-x64": "0.152.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.152.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.152.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.152.0", + "@oxc-parser/binding-linux-arm64-musl": "0.152.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.152.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.152.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.152.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.152.0", + "@oxc-parser/binding-linux-x64-gnu": "0.152.0", + "@oxc-parser/binding-linux-x64-musl": "0.152.0", + "@oxc-parser/binding-openharmony-arm64": "0.152.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.152.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.152.0", + "@oxc-parser/binding-win32-x64-msvc": "0.152.0" + } + }, + "node_modules/oxc-transform": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/oxc-transform/-/oxc-transform-0.152.0.tgz", + "integrity": "sha512-9rs8qImolzWswXGlhmye9Yz9Mce9hOe4Pivb7rSEhF/6VKn1Q122BUe9Mr1O9xQAoBX+SrF6xXfdgxKHeQf+uQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-transform/binding-android-arm-eabi": "0.152.0", + "@oxc-transform/binding-android-arm64": "0.152.0", + "@oxc-transform/binding-darwin-arm64": "0.152.0", + "@oxc-transform/binding-darwin-x64": "0.152.0", + "@oxc-transform/binding-freebsd-x64": "0.152.0", + "@oxc-transform/binding-linux-arm-gnueabihf": "0.152.0", + "@oxc-transform/binding-linux-arm-musleabihf": "0.152.0", + "@oxc-transform/binding-linux-arm64-gnu": "0.152.0", + "@oxc-transform/binding-linux-arm64-musl": "0.152.0", + "@oxc-transform/binding-linux-ppc64-gnu": "0.152.0", + "@oxc-transform/binding-linux-riscv64-gnu": "0.152.0", + "@oxc-transform/binding-linux-riscv64-musl": "0.152.0", + "@oxc-transform/binding-linux-s390x-gnu": "0.152.0", + "@oxc-transform/binding-linux-x64-gnu": "0.152.0", + "@oxc-transform/binding-linux-x64-musl": "0.152.0", + "@oxc-transform/binding-openharmony-arm64": "0.152.0", + "@oxc-transform/binding-win32-arm64-msvc": "0.152.0", + "@oxc-transform/binding-win32-ia32-msvc": "0.152.0", + "@oxc-transform/binding-win32-x64-msvc": "0.152.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.1.tgz", + "integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/integration/expo-native/package.json b/integration/expo-native/package.json new file mode 100644 index 00000000000..72189b7f02e --- /dev/null +++ b/integration/expo-native/package.json @@ -0,0 +1,20 @@ +{ + "name": "verify-clerk-expo", + "private": true, + "type": "module", + "scripts": { + "test": "node --test test/*.test.ts", + "typecheck": "tsc -p ." + }, + "devDependencies": { + "@e2e-dev/github": "0.4.0", + "@e2e-dev/mobile": "0.10.0", + "@types/node": "24.19.1", + "ai": "7.0.128", + "e2e": "0.18.0", + "typescript": "7.0.2" + }, + "engines": { + "node": ">=24.8.0 <25" + } +} diff --git a/integration/expo-native/tsconfig.json b/integration/expo-native/tsconfig.json new file mode 100644 index 00000000000..a4cd3716872 --- /dev/null +++ b/integration/expo-native/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "specs/**/*.ts", "test/**/*.ts", "testing/**/*.ts", "e2e.config.ts"] +} From 37926fc141db9095677b2d55caae598a79772ed0 Mon Sep 17 00:00:00 2001 From: Mike Pitre <12040919+mikepitre@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:52:51 -0400 Subject: [PATCH 02/20] test(expo): add the shared core of the e2e package, copied from clerk-ios and clerk-android Every file in this commit is a byte copy of the same path under e2e-tests/ on main of clerk/clerk-ios or clerk/clerk-android, except three. specs/app.ts names this repository's test app, src/platform/ios/simulator.ts holds its simctl commands, and test/session-agent.test.ts is the shared test without its one case that names the app sources of clerk-ios. src/core/MANIFEST lists the shared files with their hashes, and a unit test fails when one drifts. Co-Authored-By: Claude Opus 5.5 --- integration/expo-native/e2e.config.ts | 5 + integration/expo-native/specs/app.ts | 39 + integration/expo-native/specs/fixtures.ts | 139 ++ .../expo-native/specs/support/agent.ts | 63 + .../expo-native/specs/support/busy-runner.ts | 34 + .../expo-native/specs/support/clerk.ts | 176 ++ .../expo-native/specs/support/config.ts | 61 + .../expo-native/specs/support/device.ts | 64 + integration/expo-native/specs/support/fill.ts | 85 + .../expo-native/specs/support/inputs.ts | 166 ++ .../expo-native/specs/support/launch.ts | 152 ++ .../expo-native/specs/support/secret.ts | 99 + .../expo-native/specs/support/tapping.ts | 21 + .../expo-native/specs/support/types.ts | 95 + .../expo-native/specs/support/typing.ts | 126 ++ .../expo-native/specs/support/waiting.ts | 160 ++ integration/expo-native/src/core/MANIFEST | 52 + integration/expo-native/src/core/agent.ts | 41 + integration/expo-native/src/core/broker.ts | 208 ++ integration/expo-native/src/core/claims.ts | 63 + integration/expo-native/src/core/clerk.ts | 82 + integration/expo-native/src/core/cli.ts | 378 ++++ integration/expo-native/src/core/devices.ts | 229 +++ integration/expo-native/src/core/driver.ts | 438 +++++ integration/expo-native/src/core/e2e.ts | 276 +++ integration/expo-native/src/core/evidence.ts | 94 + integration/expo-native/src/core/exec.ts | 79 + .../expo-native/src/core/github-report.ts | 73 + .../expo-native/src/core/instances/base.json | 295 +++ .../src/core/instances/instances.ts | 264 +++ .../src/core/instances/platform.ts | 423 +++++ .../src/core/instances/settings.ts | 261 +++ .../src/core/instances/throwaway.ts | 503 +++++ integration/expo-native/src/core/keys.ts | 12 + integration/expo-native/src/core/launch.d.mts | 7 + integration/expo-native/src/core/launch.mjs | 82 + integration/expo-native/src/core/ledgers.ts | 81 + integration/expo-native/src/core/manifest.ts | 50 + integration/expo-native/src/core/publish.ts | 255 +++ .../expo-native/src/core/remote/backend.ts | 344 ++++ .../expo-native/src/core/remote/github.ts | 280 +++ .../expo-native/src/core/remote/handoff.ts | 287 +++ .../expo-native/src/core/remote/preflight.ts | 233 +++ .../expo-native/src/core/remote/protocol.ts | 146 ++ .../expo-native/src/core/remote/recipe.ts | 22 + .../src/core/remote/session-agent.ts | 572 ++++++ .../expo-native/src/core/remote/session.ts | 109 ++ .../expo-native/src/core/remote/settings.ts | 83 + .../expo-native/src/core/remote/tunnel.ts | 12 + integration/expo-native/src/core/slot.ts | 73 + integration/expo-native/src/core/state.ts | 1 + integration/expo-native/src/core/types.ts | 452 +++++ integration/expo-native/src/core/verbs.ts | 758 ++++++++ integration/expo-native/src/core/workspace.ts | 237 +++ .../src/platform/android/emulator.ts | 62 + .../expo-native/src/platform/android/local.ts | 452 +++++ .../expo-native/src/platform/android/sdk.ts | 170 ++ .../src/platform/android/session-lane.ts | 27 + .../expo-native/src/platform/ios/local.ts | 287 +++ .../expo-native/src/platform/ios/simulator.ts | 19 + integration/expo-native/test/agent.test.ts | 237 +++ integration/expo-native/test/android.test.ts | 551 ++++++ .../expo-native/test/app-start.test.ts | 258 +++ .../expo-native/test/borrowed-device.test.ts | 69 + integration/expo-native/test/broker.test.ts | 381 ++++ .../expo-native/test/busy-runner.test.ts | 85 + integration/expo-native/test/claims.test.ts | 48 + integration/expo-native/test/clerk.test.ts | 58 + integration/expo-native/test/cli.test.ts | 273 +++ .../expo-native/test/device-command.test.ts | 167 ++ integration/expo-native/test/down.test.ts | 161 ++ integration/expo-native/test/driver.test.ts | 629 +++++++ integration/expo-native/test/e2e.test.ts | 216 +++ integration/expo-native/test/end-run.test.ts | 33 + integration/expo-native/test/evidence.test.ts | 738 ++++++++ integration/expo-native/test/fill.test.ts | 160 ++ integration/expo-native/test/git-env.test.ts | 63 + .../expo-native/test/github-report.test.ts | 249 +++ integration/expo-native/test/handoff.test.ts | 772 ++++++++ integration/expo-native/test/inputs.test.ts | 140 ++ .../expo-native/test/instances.test.ts | 1677 +++++++++++++++++ .../expo-native/test/lease-flow.test.ts | 252 +++ integration/expo-native/test/ledgers.test.ts | 115 ++ integration/expo-native/test/lock.test.ts | 51 + .../expo-native/test/processes.test.ts | 45 + .../expo-native/test/remote-backend.test.ts | 296 +++ .../test/remote-lease-flow.test.ts | 233 +++ integration/expo-native/test/remote.test.ts | 245 +++ .../expo-native/test/run-groups.test.ts | 830 ++++++++ integration/expo-native/test/seam.test.ts | 101 + integration/expo-native/test/secrets.test.ts | 104 + .../expo-native/test/session-agent.test.ts | 634 +++++++ .../test/session-build-limit.test.ts | 132 ++ integration/expo-native/test/settings.test.ts | 249 +++ .../expo-native/test/something-ran.test.ts | 50 + integration/expo-native/test/state.test.ts | 45 + integration/expo-native/test/tapping.test.ts | 86 + .../expo-native/test/test-users.test.ts | 184 ++ integration/expo-native/test/typing.test.ts | 327 ++++ integration/expo-native/test/waiting.test.ts | 333 ++++ .../expo-native/testing/claim-taker.ts | 5 + .../expo-native/testing/fake-agent-device.ts | 11 + integration/expo-native/testing/fake-clerk.ts | 247 +++ .../expo-native/testing/fake-instances.ts | 26 + .../testing/fake-session-device.ts | 20 + .../expo-native/testing/fake-tunnel.ts | 2 + integration/expo-native/testing/git-env.ts | 19 + .../expo-native/testing/lock-holder.ts | 12 + .../expo-native/testing/sample-inputs.ts | 19 + 109 files changed, 21995 insertions(+) create mode 100644 integration/expo-native/e2e.config.ts create mode 100644 integration/expo-native/specs/app.ts create mode 100644 integration/expo-native/specs/fixtures.ts create mode 100644 integration/expo-native/specs/support/agent.ts create mode 100644 integration/expo-native/specs/support/busy-runner.ts create mode 100644 integration/expo-native/specs/support/clerk.ts create mode 100644 integration/expo-native/specs/support/config.ts create mode 100644 integration/expo-native/specs/support/device.ts create mode 100644 integration/expo-native/specs/support/fill.ts create mode 100644 integration/expo-native/specs/support/inputs.ts create mode 100644 integration/expo-native/specs/support/launch.ts create mode 100644 integration/expo-native/specs/support/secret.ts create mode 100644 integration/expo-native/specs/support/tapping.ts create mode 100644 integration/expo-native/specs/support/types.ts create mode 100644 integration/expo-native/specs/support/typing.ts create mode 100644 integration/expo-native/specs/support/waiting.ts create mode 100644 integration/expo-native/src/core/MANIFEST create mode 100644 integration/expo-native/src/core/agent.ts create mode 100644 integration/expo-native/src/core/broker.ts create mode 100644 integration/expo-native/src/core/claims.ts create mode 100644 integration/expo-native/src/core/clerk.ts create mode 100644 integration/expo-native/src/core/cli.ts create mode 100644 integration/expo-native/src/core/devices.ts create mode 100644 integration/expo-native/src/core/driver.ts create mode 100644 integration/expo-native/src/core/e2e.ts create mode 100644 integration/expo-native/src/core/evidence.ts create mode 100644 integration/expo-native/src/core/exec.ts create mode 100644 integration/expo-native/src/core/github-report.ts create mode 100644 integration/expo-native/src/core/instances/base.json create mode 100644 integration/expo-native/src/core/instances/instances.ts create mode 100644 integration/expo-native/src/core/instances/platform.ts create mode 100644 integration/expo-native/src/core/instances/settings.ts create mode 100644 integration/expo-native/src/core/instances/throwaway.ts create mode 100644 integration/expo-native/src/core/keys.ts create mode 100644 integration/expo-native/src/core/launch.d.mts create mode 100644 integration/expo-native/src/core/launch.mjs create mode 100644 integration/expo-native/src/core/ledgers.ts create mode 100644 integration/expo-native/src/core/manifest.ts create mode 100644 integration/expo-native/src/core/publish.ts create mode 100644 integration/expo-native/src/core/remote/backend.ts create mode 100644 integration/expo-native/src/core/remote/github.ts create mode 100644 integration/expo-native/src/core/remote/handoff.ts create mode 100644 integration/expo-native/src/core/remote/preflight.ts create mode 100644 integration/expo-native/src/core/remote/protocol.ts create mode 100644 integration/expo-native/src/core/remote/recipe.ts create mode 100644 integration/expo-native/src/core/remote/session-agent.ts create mode 100644 integration/expo-native/src/core/remote/session.ts create mode 100644 integration/expo-native/src/core/remote/settings.ts create mode 100644 integration/expo-native/src/core/remote/tunnel.ts create mode 100644 integration/expo-native/src/core/slot.ts create mode 100644 integration/expo-native/src/core/state.ts create mode 100644 integration/expo-native/src/core/types.ts create mode 100644 integration/expo-native/src/core/verbs.ts create mode 100644 integration/expo-native/src/core/workspace.ts create mode 100644 integration/expo-native/src/platform/android/emulator.ts create mode 100644 integration/expo-native/src/platform/android/local.ts create mode 100644 integration/expo-native/src/platform/android/sdk.ts create mode 100644 integration/expo-native/src/platform/android/session-lane.ts create mode 100644 integration/expo-native/src/platform/ios/local.ts create mode 100644 integration/expo-native/src/platform/ios/simulator.ts create mode 100644 integration/expo-native/test/agent.test.ts create mode 100644 integration/expo-native/test/android.test.ts create mode 100644 integration/expo-native/test/app-start.test.ts create mode 100644 integration/expo-native/test/borrowed-device.test.ts create mode 100644 integration/expo-native/test/broker.test.ts create mode 100644 integration/expo-native/test/busy-runner.test.ts create mode 100644 integration/expo-native/test/claims.test.ts create mode 100644 integration/expo-native/test/clerk.test.ts create mode 100644 integration/expo-native/test/cli.test.ts create mode 100644 integration/expo-native/test/device-command.test.ts create mode 100644 integration/expo-native/test/down.test.ts create mode 100644 integration/expo-native/test/driver.test.ts create mode 100644 integration/expo-native/test/e2e.test.ts create mode 100644 integration/expo-native/test/end-run.test.ts create mode 100644 integration/expo-native/test/evidence.test.ts create mode 100644 integration/expo-native/test/fill.test.ts create mode 100644 integration/expo-native/test/git-env.test.ts create mode 100644 integration/expo-native/test/github-report.test.ts create mode 100644 integration/expo-native/test/handoff.test.ts create mode 100644 integration/expo-native/test/inputs.test.ts create mode 100644 integration/expo-native/test/instances.test.ts create mode 100644 integration/expo-native/test/lease-flow.test.ts create mode 100644 integration/expo-native/test/ledgers.test.ts create mode 100644 integration/expo-native/test/lock.test.ts create mode 100644 integration/expo-native/test/processes.test.ts create mode 100644 integration/expo-native/test/remote-backend.test.ts create mode 100644 integration/expo-native/test/remote-lease-flow.test.ts create mode 100644 integration/expo-native/test/remote.test.ts create mode 100644 integration/expo-native/test/run-groups.test.ts create mode 100644 integration/expo-native/test/seam.test.ts create mode 100644 integration/expo-native/test/secrets.test.ts create mode 100644 integration/expo-native/test/session-agent.test.ts create mode 100644 integration/expo-native/test/session-build-limit.test.ts create mode 100644 integration/expo-native/test/settings.test.ts create mode 100644 integration/expo-native/test/something-ran.test.ts create mode 100644 integration/expo-native/test/state.test.ts create mode 100644 integration/expo-native/test/tapping.test.ts create mode 100644 integration/expo-native/test/test-users.test.ts create mode 100644 integration/expo-native/test/typing.test.ts create mode 100644 integration/expo-native/test/waiting.test.ts create mode 100644 integration/expo-native/testing/claim-taker.ts create mode 100644 integration/expo-native/testing/fake-agent-device.ts create mode 100644 integration/expo-native/testing/fake-clerk.ts create mode 100644 integration/expo-native/testing/fake-instances.ts create mode 100644 integration/expo-native/testing/fake-session-device.ts create mode 100644 integration/expo-native/testing/fake-tunnel.ts create mode 100644 integration/expo-native/testing/git-env.ts create mode 100644 integration/expo-native/testing/lock-holder.ts create mode 100644 integration/expo-native/testing/sample-inputs.ts diff --git a/integration/expo-native/e2e.config.ts b/integration/expo-native/e2e.config.ts new file mode 100644 index 00000000000..a2cc581db48 --- /dev/null +++ b/integration/expo-native/e2e.config.ts @@ -0,0 +1,5 @@ +import { app } from './specs/app.ts'; +import { composeE2EConfig } from './specs/support/config.ts'; +import { readTarget } from './specs/support/inputs.ts'; + +export default composeE2EConfig(app, readTarget(app, process.env), process.env); diff --git a/integration/expo-native/specs/app.ts b/integration/expo-native/specs/app.ts new file mode 100644 index 00000000000..570c569cb8d --- /dev/null +++ b/integration/expo-native/specs/app.ts @@ -0,0 +1,39 @@ +import type { TestApp } from './support/inputs.ts'; +import type { AppEntry, Platform } from './support/types.ts'; + +export const APP_ID = 'com.clerk.exponativebuildfixture'; + +const ANDROID_ACTIVITY = '.MainActivity'; +const DEV_CLIENT_SCHEME = 'exp+clerk-expo-native-build-fixture'; + +export function devClientEntry(platform: Platform, devServer: string): AppEntry { + return platform === 'ios' + ? { + kind: 'dev-client', + launchArguments: [ + '--initialUrl', + devServer, + '-EXDevMenuShowsAtLaunch', + 'NO', + '-EXDevMenuIsOnboardingFinished', + 'YES', + '-EXDevMenuShowFloatingActionButton', + 'NO', + ], + openLink: null, + androidActivity: null, + } + : { + kind: 'dev-client', + launchArguments: [], + openLink: `${DEV_CLIENT_SCHEME}://expo-development-client/?url=${encodeURIComponent(devServer)}`, + androidActivity: ANDROID_ACTIVITY, + }; +} + +export const app: TestApp = { + platforms: ['ios', 'android'], + id: (): string => APP_ID, + entry: (platform: Platform, devServer: string | null): AppEntry => + devServer === null ? { kind: 'binary' } : devClientEntry(platform, devServer), +}; diff --git a/integration/expo-native/specs/fixtures.ts b/integration/expo-native/specs/fixtures.ts new file mode 100644 index 00000000000..e9f18ac3090 --- /dev/null +++ b/integration/expo-native/specs/fixtures.ts @@ -0,0 +1,139 @@ +import { execFile } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; +import { join } from 'node:path'; +import { test as base } from '@e2e-dev/mobile'; +import { expect, type Locator } from 'e2e'; +import { app as testApp } from './app.ts'; +import { busyWaits, onceTheRunnerIsFree, type BusyWait } from './support/busy-runner.ts'; +import { aboutToLaunch, testUsers } from './support/clerk.ts'; +import { ASSERTION_TIMEOUT_MS, PACKAGE_DIR } from './support/config.ts'; +import { agentDevice, deviceCommand } from './support/device.ts'; +import { fillField } from './support/fill.ts'; +import { readClerk, readRun, readTarget, type Target } from './support/inputs.ts'; +import { appLauncher } from './support/launch.ts'; +import { redact } from './support/secret.ts'; +import { tapOnceUncovered } from './support/tapping.ts'; +import { + APP_ELEMENT_IDS, + CLERK_TEST_CODE, + signedInText, + type AppLocators, + type HostFixture, + type LaunchOptions, + type SeededUser, + type TestEmail, +} from './support/types.ts'; +import { errorScreenElseSavePasswordPrompt, onScreen, signedInAs, signedOut, until, untilVisible, type Sight, type WaitPace, type WaitScreen } from './support/waiting.ts'; + +export { expect, CLERK_TEST_CODE }; + +const LAUNCH_TIMEOUT_MS = 60_000; +const POLL_MS = 400; +const TYPING_DELAY_MS = 40; +const CONFIRM_READS = 5; +const ONLY_E2E_WORKER_SLOT = 0; + +const RUN = readRun(process.env); + +const AGENT_DEVICE = join(PACKAGE_DIR, 'node_modules', '.bin', 'agent-device'); + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const runnerBusy = (): BusyWait => busyWaits(6, () => sleep(5_000)); + +const randomId = (): string => randomBytes(8).toString('hex'); + +const BUILDS_THIS_WORKER_INSTALLED = new Set(); + +async function adb(target: Target, args: readonly string[]): Promise { + const result = await deviceCommand(target.device, target.platform, args); + if (result.code !== 0) throw new Error(`adb ${args[0]} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`); +} + +function typeOnDevice(target: Target, [command, ...operands]: readonly [string, ...string[]]): Promise { + const device = agentDevice(target.device, target.platform); + return new Promise((resolve, reject) => { + const env = { ...process.env, ...device.env }; + execFile(AGENT_DEVICE, [command, '--delay-ms', String(TYPING_DELAY_MS), '--session', `${target.session}-${ONLY_E2E_WORKER_SLOT}`, ...device.selector, '--', ...operands], { env }, (error, _stdout, stderr) => { + if (error === null) resolve(); + else reject(new Error(redact(`agent-device ${command} failed: ${stderr.trim() || error.message}`))); + }); + }); +} + +export const test = base.extend<{ host: HostFixture }>({ + host: async ({ app: e2eApp, device, screen, platform }, use) => { + const target = readTarget(testApp, process.env); + if (target.platform !== platform) throw new Error(`e2e runs the ${platform} target and the inputs name ${target.platform}; pass --target ${target.platform}`); + const clerk = readClerk(process.env); + const users = testUsers(clerk, RUN); + const appId = testApp.id(platform); + const app = Object.fromEntries(Object.entries(APP_ELEMENT_IDS).map(([name, id]) => [name, screen.getByTestId(id)])) as AppLocators; + const launchApp = appLauncher( + { platform, id: appId, entry: testApp.entry(platform, target.build.devServer), buildPath: target.build.path, publishableKey: clerk.publishableKey, run: RUN }, + { + installApp: (path) => device.installApp(path), + signInTicket: users.signInTicket, + aboutToLaunch: () => aboutToLaunch(clerk.access), + openApp: (id, options) => (options === undefined ? device.openApp(id) : device.openApp(id, { relaunch: true, launchArguments: [...options.launchArguments] })), + adb: (args) => adb(target, args), + }, + BUILDS_THIS_WORKER_INSTALLED, + randomId, + ); + + const waitScreen: WaitScreen = { + home: app, + errorScreenElsePrompt: device.locator(errorScreenElseSavePasswordPrompt(APP_ELEMENT_IDS.error)), + dismissPrompt: () => screen.getByRole('button', { name: 'Not Now' }).tap({ timeout: ASSERTION_TIMEOUT_MS }), + }; + const pace = (timeoutMs: number): WaitPace => ({ timeoutMs, now: Date.now, wait: () => sleep(POLL_MS) }); + const waitFor = (sight: Sight, what: string, timeoutMs: number): Promise => until(sight, what, waitScreen, pace(timeoutMs)); + + const describeSignedIn = (who: SeededUser | TestEmail): string => + typeof who === 'string' ? `"${signedInText(who)}" with a user ID and a session ID on the home` : `"${signedInText(who.email)}" with user ID ${who.id} and a session ID on the home`; + + function landing(options: LaunchOptions): { readonly sight: Sight; readonly what: string } { + const on = (locator: Locator, what: string) => ({ sight: onScreen(locator, what), what }); + if (options.landsOn !== undefined) return on(options.landsOn, 'the screen the spec names in landsOn'); + const user = options.signedInAs ?? null; + if (user !== null) return { sight: signedInAs(app, user), what: describeSignedIn(user) }; + if (options.keepStorage === true) { + throw new Error('a launch with keepStorage and no signedInAs has no landing the fixture can know; pass landsOn with the locator the launch should show'); + } + return on(app.signIn, 'the "Sign in" button on the home'); + } + + const tap = (control: Locator, busy: BusyWait): Promise => + tapOnceUncovered(() => control.tap({ timeout: ASSERTION_TIMEOUT_MS }), { timeoutMs: ASSERTION_TIMEOUT_MS, now: Date.now, wait: () => sleep(POLL_MS) }, busy); + + const host: HostFixture = { + runId: RUN, + app, + newEmail: async () => users.newEmail(), + newPhone: users.newPhone, + seedUser: users.seed, + async launch(options = {}) { + const lands = landing(options); + const launchId = await launchApp(options); + await waitFor(lands.sight, `${lands.what} after launch ${launchId}`, LAUNCH_TIMEOUT_MS); + }, + expectSignedInAs: (who, timeoutMs = ASSERTION_TIMEOUT_MS) => waitFor(signedInAs(app, who), describeSignedIn(who), timeoutMs), + expectSignedOut: (timeoutMs = ASSERTION_TIMEOUT_MS) => waitFor(signedOut(app), '"Signed out" and no user ID or session ID on the home', timeoutMs), + expectVisible: (target, what, timeoutMs = ASSERTION_TIMEOUT_MS) => untilVisible(target, what, waitScreen, pace(timeoutMs)), + async screenshot(label) { + await onceTheRunnerIsFree(() => e2eApp.screenshot(label), runnerBusy()); + }, + tap: (control) => tap(control, runnerBusy()), + fill: async (field, text) => + fillField( + field, + typeof text === 'string' ? text : text.use('device-input', (value) => value), + { platform, focused: device.locator('role=textbox focused'), texts: device.locator('role=text'), type: (command) => typeOnDevice(target, command) }, + { tapTimeoutMs: ASSERTION_TIMEOUT_MS, reads: CONFIRM_READS, now: Date.now, wait: () => sleep(POLL_MS) }, + runnerBusy(), + ), + }; + await use(host); + }, +}); diff --git a/integration/expo-native/specs/support/agent.ts b/integration/expo-native/specs/support/agent.ts new file mode 100644 index 00000000000..9ee11d184ad --- /dev/null +++ b/integration/expo-native/specs/support/agent.ts @@ -0,0 +1,63 @@ +import { readFileSync, statSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { Secret } from './secret.ts'; + +export const DEFAULT_AGENT_MODEL = 'anthropic/claude-haiku-5.5'; +export const BACKUP_AGENT_MODEL = 'openai/gpt-6-luna-fast'; + +type Env = Readonly>; +type GatewayKey = Secret<'ai-gateway-key'>; + +export type AgentCredential = + | { readonly variable: 'AI_GATEWAY_API_KEY'; readonly key: GatewayKey } + | { readonly variable: 'AI_GATEWAY_API_KEY_FILE'; readonly file: string; readonly key: GatewayKey }; + +export interface Agent { + readonly model: string; + readonly backup: string; + readonly credential: AgentCredential; +} + +export class UnusableAgentKey extends Error { + readonly what: string; + readonly fix: string; + constructor(what: string, fix: string) { + super(`${what}; ${fix}`); + this.what = what; + this.fix = fix; + } +} + +export const isSet = (value: string | undefined): value is string => value !== undefined && value.trim() !== ''; + +function keyFromFile(file: string): GatewayKey { + const unusable = (why: string) => new UnusableAgentKey(`AI_GATEWAY_API_KEY_FILE names ${file}, which ${why}`, 'unset AI_GATEWAY_API_KEY_FILE, or point it at a file that holds the Vercel AI Gateway key'); + let mode: number; + try { + mode = statSync(file).mode; + } catch { + throw unusable('does not exist'); + } + if ((mode & 0o077) !== 0) throw new UnusableAgentKey(`${file} can be read by other users of this machine`, `chmod 600 ${file}`); + let value: string; + try { + value = readFileSync(file, 'utf8').trim(); + } catch (error) { + throw unusable(`cannot be read (${(error as NodeJS.ErrnoException).code ?? (error as Error).message})`); + } + if (value === '') throw new UnusableAgentKey(`${file} holds no key`, `put the Vercel AI Gateway key in ${file}, or unset AI_GATEWAY_API_KEY_FILE`); + return new Secret('ai-gateway-key', value); +} + +function credential(env: Env): AgentCredential | null { + if (isSet(env.AI_GATEWAY_API_KEY)) return { variable: 'AI_GATEWAY_API_KEY', key: new Secret('ai-gateway-key', env.AI_GATEWAY_API_KEY.trim()) }; + if (!isSet(env.AI_GATEWAY_API_KEY_FILE)) return null; + const file = resolve(env.AI_GATEWAY_API_KEY_FILE); + return { variable: 'AI_GATEWAY_API_KEY_FILE', file, key: keyFromFile(file) }; +} + +export function readAgent(env: Env): Agent | null { + const found = credential(env); + if (found === null) return null; + return { model: DEFAULT_AGENT_MODEL, backup: BACKUP_AGENT_MODEL, credential: found }; +} diff --git a/integration/expo-native/specs/support/busy-runner.ts b/integration/expo-native/specs/support/busy-runner.ts new file mode 100644 index 00000000000..cd93d1a4a84 --- /dev/null +++ b/integration/expo-native/specs/support/busy-runner.ts @@ -0,0 +1,34 @@ +const STILL_FINISHING = /iOS (?:automation )?runner is still finishing a (?:previous )?command/; + +export const isRunnerBusy = (error: unknown): boolean => error instanceof Error && STILL_FINISHING.test(error.message); + +export interface BusyWait { + waited(): Promise; +} + +export function busyWaits(most: number, wait: () => Promise): BusyWait { + let left = most; + return { + async waited() { + if (left <= 0) return false; + left -= 1; + await wait(); + return true; + }, + }; +} + +export const NO_BUSY_WAIT: BusyWait = busyWaits(0, async () => undefined); + +export async function onceTheRunnerIsFree(step: () => Promise, busy: BusyWait): Promise { + let refusal: unknown; + for (;;) { + try { + return await step(); + } catch (error) { + if (!isRunnerBusy(error)) throw error; + refusal ??= error; + if (!(await busy.waited())) throw refusal; + } + } +} diff --git a/integration/expo-native/specs/support/clerk.ts b/integration/expo-native/specs/support/clerk.ts new file mode 100644 index 00000000000..02e41c83ed5 --- /dev/null +++ b/integration/expo-native/specs/support/clerk.ts @@ -0,0 +1,176 @@ +import { randomBytes, randomInt } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import net from 'node:net'; +import { runIdParts, type ClerkAccess, type ClerkInstance } from './inputs.ts'; +import { SHORTEST_SECRET, Secret, protect } from './secret.ts'; +import { commandsFor } from './typing.ts'; +import type { RunId, SecretLike, SeedOptions, SeededUser, TestEmail, TestPhone } from './types.ts'; + +export const BACKEND_API_URL = 'https://api.clerk.com/v1'; + +export const BACKEND_CALLS = { + createUser: { method: 'POST', path: '/users' }, + signInToken: { method: 'POST', path: '/sign_in_tokens' }, + usersByPhone: { method: 'GET', path: '/users' }, +} as const; + +export type BackendCall = keyof typeof BACKEND_CALLS; + +export const ABOUT_TO_LAUNCH = { method: 'POST', path: '/about_to_launch' } as const; + +export const TICKET_SECONDS = 120; + +// Node gives each address of a host 250 ms to connect before it tries the next, and fails the request when +// all of them miss it. On a network with no IPv6 route that fails about one request in five to api.clerk.com. +export const CONNECT_ATTEMPT_MS = 2_500; +net.setDefaultAutoSelectFamilyAttemptTimeout(CONNECT_ATTEMPT_MS); + +const notTestIdentity = (value: string, kind: string): Error => new Error(`${value} is not a ${kind}; use an address with +clerk_test or a 555-0100..0199 phone that this run created`); + +export function runEmailPrefix(run: RunId): string { + const { time, tag } = runIdParts(run); + return `${time}${tag}`; +} + +export function newTestEmail(run: RunId): TestEmail { + return parseTestEmail(`${runEmailPrefix(run)}${randomInt(36 ** 4).toString(36).padStart(4, '0')}+clerk_test@example.com`); +} + +const TEST_EMAIL = /^[a-z0-9._-]+\+clerk_test@[a-z0-9.-]+\.[a-z]+$/; +export function parseTestEmail(value: string): TestEmail { + if (!TEST_EMAIL.test(value)) throw notTestIdentity(value, '+clerk_test email'); + return value as TestEmail; +} + +export function parseTestPhone(value: string): TestPhone { + const digits = value.replace(/[^0-9]/g, ''); + const national = digits.length === 11 && digits.startsWith('1') ? digits.slice(1) : digits; + if (!/^[2-9]\d{2}55501\d{2}$/.test(national)) throw notTestIdentity(value, '555-0100..0199 test phone'); + return `+1${national}` as TestPhone; +} + +export const TEST_PHONES: readonly TestPhone[] = Array.from({ length: 100 }, (_, n) => parseTestPhone(`+1201555${String(100 + n).padStart(4, '0')}`)); + +export function typedPassword(value: string): Secret<'password'> { + const password = new Secret('password', value); + for (const typedAtOnce of commandsFor(value)) if (typedAtOnce.length >= SHORTEST_SECRET) protect(typedAtOnce); + return password; +} + +export function newTestPassword(): Secret<'password'> { + return typedPassword(`${randomBytes(12).toString('hex')}Aa1!`); +} + +export const runPassword = (run: RunId): Secret<'password'> => typedPassword(`Verify-${run}-Pw1!`); + +export interface TestUsers { + newEmail(): TestEmail; + newPhone(): Promise; + seed(options?: SeedOptions): Promise; + signInTicket(user: SeededUser): Promise>; +} + +interface Reply { + readonly status: number; + readonly json: unknown; +} + +const ok = (reply: Reply): boolean => reply.status >= 200 && reply.status < 300; + +const errorsOf = (reply: Reply): readonly { readonly code?: string; readonly message?: string; readonly long_message?: string }[] => (reply.json as { errors?: { code?: string; message?: string; long_message?: string }[] } | null)?.errors ?? []; + +const phoneIsTaken = (reply: Reply): boolean => reply.status === 422 && errorsOf(reply).some((error) => error.code === 'form_identifier_exists'); + +function refusal(call: BackendCall, reply: Reply): Error { + const said = errorsOf(reply).map((error) => error.long_message ?? error.message ?? error.code ?? 'no reason given').join('; '); + const { method, path } = BACKEND_CALLS[call]; + return new Error(`Clerk ${method} ${path} answered ${reply.status}${said === '' ? '' : `: ${said}`}`); +} + +function endpoint(access: ClerkAccess): { readonly baseUrl: string; readonly bearer: SecretLike } { + if (access.kind === 'secret-key') return { baseUrl: BACKEND_API_URL, bearer: access.key }; + return { baseUrl: access.url, bearer: new Secret('stand-in-token', readFileSync(access.tokenFile, 'utf8')) }; +} + +export async function aboutToLaunch(access: ClerkAccess, fetchImpl: typeof fetch = fetch): Promise { + if (access.kind !== 'stand-in') return; + const { baseUrl, bearer } = endpoint(access); + const response = await bearer.use('bapi-authorization', (plain) => + fetchImpl(`${baseUrl}${ABOUT_TO_LAUNCH.path}`, { method: ABOUT_TO_LAUNCH.method, headers: { Authorization: `Bearer ${plain}` }, signal: AbortSignal.timeout(60_000) }), + ); + if (response.ok) return; + const said = errorsOf({ status: response.status, json: await response.json().catch(() => null) }).map((error) => error.long_message ?? error.message ?? error.code); + throw new Error(`the verify CLI was not ready for the app to launch: ${said.join('; ') || `it answered ${response.status}`}`); +} + +export function testUsers(clerk: ClerkInstance, run: RunId, fetchImpl: typeof fetch = fetch): TestUsers { + runPassword(run); + const handedOut = new Set(); + + async function ask(call: BackendCall, sent: { readonly query?: string; readonly body?: unknown }): Promise { + const { method, path } = BACKEND_CALLS[call]; + const { baseUrl, bearer } = endpoint(clerk.access); + const response = await bearer.use('bapi-authorization', (plain) => + fetchImpl(`${baseUrl}${path}${sent.query === undefined ? '' : `?${sent.query}`}`, { + method, + headers: { Authorization: `Bearer ${plain}`, 'Content-Type': 'application/json' }, + ...(sent.body === undefined ? {} : { body: JSON.stringify(sent.body) }), + signal: AbortSignal.timeout(30_000), + }), + ); + const text = await response.text(); + try { + return { status: response.status, json: text.length > 0 ? JSON.parse(text) : null }; + } catch { + return { status: response.status, json: null }; + } + } + + function phonesNotHandedOut(): readonly TestPhone[] { + const first = Math.floor(Math.random() * TEST_PHONES.length); + return TEST_PHONES.map((_, i) => TEST_PHONES[(first + i) % TEST_PHONES.length]!).filter((phone) => !handedOut.has(phone)); + } + + const everyPhoneTaken = (otherwise: string): Error => new Error(`every 555-0100..0199 test phone is taken on this instance; delete its test users${otherwise}`); + + return { + newEmail: () => newTestEmail(run), + async newPhone() { + for (const phone of phonesNotHandedOut()) { + const holders = await ask('usersByPhone', { query: `phone_number=${encodeURIComponent(phone)}` }); + if (!ok(holders)) throw refusal('usersByPhone', holders); + if (Array.isArray(holders.json) && holders.json.length > 0) continue; + handedOut.add(phone); + return phone; + } + throw everyPhoneTaken(''); + }, + async seed(options = {}) { + const email = newTestEmail(run); + const password = options.password === true ? newTestPassword() : null; + for (const phone of options.phone === true ? phonesNotHandedOut() : [null]) { + const created = await ask('createUser', { + body: { + email_address: [email], + ...(phone === null ? {} : { phone_number: [phone] }), + ...(password === null ? { skip_password_requirement: true } : { password: password.use('bapi-user-password', (plain) => plain), bypass_client_trust: true }), + }, + }); + if (phone !== null && phoneIsTaken(created)) continue; + if (!ok(created)) throw refusal('createUser', created); + const id = (created.json as { id?: unknown } | null)?.id; + if (typeof id !== 'string') throw new Error('Clerk created a user without an id'); + if (phone !== null) handedOut.add(phone); + return { id, email, phone, password }; + } + throw everyPhoneTaken(', or seed without a phone'); + }, + async signInTicket(user) { + const minted = await ask('signInToken', { body: { user_id: user.id, expires_in_seconds: TICKET_SECONDS } }); + if (!ok(minted)) throw refusal('signInToken', minted); + const token = (minted.json as { token?: unknown } | null)?.token; + if (typeof token !== 'string') throw new Error('Clerk returned a sign-in token without a token'); + return new Secret('ticket', token); + }, + }; +} diff --git a/integration/expo-native/specs/support/config.ts b/integration/expo-native/specs/support/config.ts new file mode 100644 index 00000000000..2ede77495bc --- /dev/null +++ b/integration/expo-native/specs/support/config.ts @@ -0,0 +1,61 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { E2EConfig } from 'e2e'; +import { mobile, type DeviceProvider } from '@e2e-dev/mobile'; +import { readAgent } from './agent.ts'; +import type { Device, Target, TestApp } from './inputs.ts'; +import { Secret } from './secret.ts'; + +export const ASSERTION_TIMEOUT_MS = 10_000; +export const TEST_TIMEOUT_MS = 240_000; + +export const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); + +type Env = Readonly>; + +export function engineDevice(device: Device): readonly string[] | DeviceProvider { + if (device.kind === 'local') return [device.id]; + const token = new Secret('session-bearer', readFileSync(device.tokenFile, 'utf8').trim()); + return { + name: 'borrowed', + acquire: async ({ env }) => ({ + id: device.id, + deviceId: device.id, + device: device.id, + daemon: token.use('e2e-provider-lease', (authToken) => ({ baseUrl: device.daemonUrl, authToken })), + client: { cwd: env.AGENT_DEVICE_STATE_DIR ?? join(env.HOME ?? homedir(), '.agent-device') }, + }), + release: async () => {}, + }; +} + +const requireOnlyForAnAgent = createRequire(import.meta.url); + +function agentConfig(env: Env): Pick { + const agent = readAgent(env); + if (agent === null) return {}; + const { createGateway } = requireOnlyForAnAgent('ai') as typeof import('ai'); + const gateway = agent.credential.key.use('gateway-provider', (apiKey) => createGateway({ apiKey })); + return { agents: { default: { model: gateway(agent.model), providerOptions: { gateway: { models: [agent.backup] } } } }, cache: 'off' }; +} + +export function composeE2EConfig(app: TestApp, target: Target, env: Env): E2EConfig { + return { + tests: ['specs/**/*.e2e.ts'], + targets: [ + { + name: target.platform, + engine: mobile({ platform: target.platform, device: engineDevice(target.device), session: target.session, videoTouches: false }), + app: { bundleId: app.id(target.platform), appPath: target.build.path ?? undefined }, + }, + ], + workers: 1, + timeout: TEST_TIMEOUT_MS, + assertionTimeout: ASSERTION_TIMEOUT_MS, + trace: 'off', + ...agentConfig(env), + }; +} diff --git a/integration/expo-native/specs/support/device.ts b/integration/expo-native/specs/support/device.ts new file mode 100644 index 00000000000..96b26ba2edb --- /dev/null +++ b/integration/expo-native/specs/support/device.ts @@ -0,0 +1,64 @@ +import { execFile } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import type { Device } from './inputs.ts'; +import { Secret } from './secret.ts'; +import type { Platform } from './types.ts'; + +export interface CommandLine { + readonly command: string; + readonly args: readonly string[]; +} + +export interface CommandResult { + readonly code: number; + readonly stdout: string; + readonly stderr: string; +} + +export type CommandRunner = (command: string, args: readonly string[]) => Promise; + +export function agentDevice(device: Device, platform: Platform): { readonly selector: readonly string[]; readonly env: Readonly> } { + const selector = ['--platform', platform, platform === 'ios' ? '--udid' : '--serial', device.id]; + if (device.kind === 'local') return { selector, env: {} }; + const token = new Secret('session-bearer', readFileSync(device.tokenFile, 'utf8').trim()); + return { + selector: [...selector, '--daemon-base-url', device.daemonUrl], + env: token.use('agent-device-daemon', (plain) => ({ AGENT_DEVICE_DAEMON_AUTH_TOKEN: plain })), + }; +} + +const REVERSE_FLAGS: ReadonlySet = new Set(['--list', '--no-rebind', '--remove', '--remove-all']); + +export function deviceToolCommand(platform: Platform, deviceId: string, args: readonly string[], adb = 'adb'): CommandLine | null { + if (platform !== 'android') return null; + const [subcommand, ...rest] = args; + const allowed = subcommand === 'shell' || (subcommand === 'reverse' && rest.every((arg) => REVERSE_FLAGS.has(arg) || /^tcp:\d{1,5}$/.test(arg))); + return allowed ? { command: adb, args: ['-s', deviceId, ...args] } : null; +} + +function localAdb(env: Readonly>, home: string): string { + const byDefault = process.platform === 'darwin' ? join(home, 'Library', 'Android', 'sdk') : join(home, 'Android', 'Sdk'); + const roots = [env.ANDROID_HOME, env.ANDROID_SDK_ROOT, byDefault].filter((root): root is string => root !== undefined && root !== ''); + return roots.map((root) => join(root, 'platform-tools', 'adb')).find((adb) => existsSync(adb)) ?? 'adb'; +} + +const run: CommandRunner = (command, args) => + new Promise((resolve) => { + execFile(command, [...args], (error, stdout, stderr) => { + if (error === null) resolve({ code: 0, stdout, stderr }); + else resolve(typeof error.code === 'number' ? { code: error.code, stdout, stderr } : { code: 127, stdout, stderr: stderr + error.message }); + }); + }); + +export async function deviceCommand( + device: Device, + platform: Platform, + args: readonly string[], + options: { readonly runner?: CommandRunner; readonly env?: Readonly>; readonly home?: string } = {}, +): Promise { + const local = deviceToolCommand(platform, device.id, args, localAdb(options.env ?? process.env, options.home ?? homedir())); + if (local === null) throw new Error(`no ${platform} device command starts with ${args[0] ?? 'nothing'}; on Android use adb shell or adb reverse arguments, and on iOS pass what the app needs as launch arguments`); + return (options.runner ?? run)(local.command, local.args); +} diff --git a/integration/expo-native/specs/support/fill.ts b/integration/expo-native/specs/support/fill.ts new file mode 100644 index 00000000000..024a88699cd --- /dev/null +++ b/integration/expo-native/specs/support/fill.ts @@ -0,0 +1,85 @@ +import { onceTheRunnerIsFree, type BusyWait } from './busy-runner.ts'; +import { tapOnceUncovered } from './tapping.ts'; +import { typeConfirmed } from './typing.ts'; +import type { Platform } from './types.ts'; + +interface Frame { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +export interface FieldNode { + inputValue(): Promise; + boundingBox(): Promise; +} + +export interface NamedField { + tap(options: { readonly timeout: number }): Promise; + all(): Promise; +} + +export interface FocusedInput extends FieldNode { + count(): Promise; + tap(options: { readonly timeout: number }): Promise; +} + +export interface FillDevice { + readonly platform: Platform; + readonly focused: FocusedInput; + readonly texts: { allTextContents(): Promise }; + type(command: readonly [string, ...string[]]): Promise; +} + +export interface FillPace { + readonly tapTimeoutMs: number; + readonly reads: number; + now(): number; + wait(): Promise; +} + +export async function fillField(field: NamedField, text: string, device: FillDevice, pace: FillPace, busy: BusyWait): Promise { + const { focused } = device; + const tap = (): Promise => tapOnceUncovered(() => field.tap({ timeout: pace.tapTimeoutMs }), { timeoutMs: pace.tapTimeoutMs, now: pace.now, wait: pace.wait }, busy); + await tap(); + const read = (reading: () => Promise, unreadable: T): Promise => onceTheRunnerIsFree(reading, busy).catch(() => unreadable); + const frame = (): Promise => read(async () => ((await focused.count()) === 1 ? await focused.boundingBox() : null), null); + const wordsShown = (): Promise => read(async () => (await device.texts.allTextContents()).map((text) => text.replace(/\P{L}+/gu, '')), null); + const input = await frame(); + const wordsBeforeTyping = await wordsShown(); + let namedInput: FieldNode | null = null; + await typeConfirmed( + { + type: (value) => device.type(['type', value]), + async valueIfReadable() { + const now = await frame(); + if (input === null || now === null || now.x !== input.x || now.y !== input.y || now.width !== input.width || now.height !== input.height) return null; + const value = await read(() => focused.inputValue(), null); + if (value !== '') return value; + const words = await wordsShown(); + return wordsBeforeTyping !== null && words !== null && words.every((shown) => wordsBeforeTyping.includes(shown)) ? '' : null; + }, + valuesOfTheNamedNodes: () => + read(async () => { + const values: string[] = []; + for (const node of await field.all()) { + const value = await node.inputValue(); + if (value !== '') namedInput = node; + values.push(value); + } + return values; + }, []), + refocus: () => onceTheRunnerIsFree(() => focused.tap({ timeout: pace.tapTimeoutMs }), busy), + tapAgain: tap, + async replace(value) { + const box = input ?? (await namedInput?.boundingBox().catch(() => null)) ?? null; + if (box !== null) await device.type(['fill', String(Math.round(box.x + box.width / 2)), String(Math.round(box.y + box.height / 2)), value]); + }, + ...(device.platform === 'android' ? { nothingFocused: async () => (await focused.count()) !== 1 } : {}), + }, + text, + { reads: pace.reads, wait: pace.wait }, + busy, + ); +} diff --git a/integration/expo-native/specs/support/inputs.ts b/integration/expo-native/specs/support/inputs.ts new file mode 100644 index 00000000000..2da4da7bc6e --- /dev/null +++ b/integration/expo-native/specs/support/inputs.ts @@ -0,0 +1,166 @@ +import { randomBytes } from 'node:crypto'; +import { Secret } from './secret.ts'; +import type { AppEntry, Platform, PublishableKey, RunId } from './types.ts'; + +type Env = Readonly>; + +export type Device = + | { readonly kind: 'local'; readonly id: string } + | { readonly kind: 'borrowed'; readonly id: string; readonly daemonUrl: string; readonly tokenFile: string }; + +export interface AppBuild { + readonly path: string | null; + readonly devServer: string | null; +} + +export interface Target { + readonly platform: Platform; + readonly device: Device; + readonly session: string; + readonly build: AppBuild; +} + +export type ClerkAccess = + | { readonly kind: 'secret-key'; readonly key: Secret<'clerk-secret-key'> } + | { readonly kind: 'stand-in'; readonly url: string; readonly tokenFile: string }; + +export interface ClerkInstance { + readonly publishableKey: PublishableKey; + readonly access: ClerkAccess; +} + +export interface Inputs { + readonly target: Target; + readonly clerk: ClerkInstance; + readonly run: RunId; +} + +export interface TestApp { + readonly platforms: readonly [Platform, ...Platform[]]; + id(platform: Platform): string; + entry(platform: Platform, devServer: string | null): AppEntry; +} + +const NAMES = { + platform: 'CLERK_E2E_PLATFORM', + device: 'CLERK_E2E_DEVICE', + session: 'CLERK_E2E_DEVICE_SESSION', + daemonUrl: 'CLERK_E2E_DEVICE_DAEMON_URL', + daemonTokenFile: 'CLERK_E2E_DEVICE_DAEMON_TOKEN_FILE', + appPath: 'CLERK_E2E_APP_PATH', + devServer: 'CLERK_E2E_DEV_SERVER', + publishableKey: 'CLERK_PUBLISHABLE_KEY', + secretKey: 'CLERK_SECRET_KEY', + apiUrl: 'CLERK_E2E_API_URL', + apiTokenFile: 'CLERK_E2E_API_TOKEN_FILE', + run: 'CLERK_E2E_RUN_ID', +} as const; + +export const INPUT_VARIABLES: readonly string[] = Object.values(NAMES); + +const given = (env: Env, name: string): string | null => { + const value = env[name]?.trim(); + return value === undefined || value === '' ? null : value; +}; + +const DEVICES: Readonly> = { + ios: { id: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, expects: 'the UDID of a simulator; a name is not accepted', listedBy: 'xcrun simctl list devices booted' }, + android: { id: /^emulator-\d+$/, expects: 'the emulator- serial of an emulator; an AVD name, a host:port address, and the serial of a phone are not accepted', listedBy: 'adb devices' }, +}; + +function readPlatform(app: TestApp, env: Env): Platform { + const named = given(env, NAMES.platform); + const [only, ...others] = app.platforms; + if (named === null) { + if (others.length === 0) return only; + throw new Error(`${NAMES.platform} is not set, and this test app runs on ${app.platforms.join(' and ')}; set it to one of them`); + } + const platform = app.platforms.find((candidate) => candidate === named); + if (platform === undefined) throw new Error(`${NAMES.platform} is ${named}, and this test app runs on ${app.platforms.join(' and ')}`); + return platform; +} + +function readDevice(platform: Platform, env: Env): Device { + const id = given(env, NAMES.device); + const { id: shape, expects, listedBy } = DEVICES[platform]; + if (id === null) throw new Error(`${NAMES.device} is not set; set it to the id of a booted device, which \`${listedBy}\` prints`); + if (!shape.test(id)) throw new Error(`${NAMES.device} is ${id}, which is not ${expects}, so copy the id that \`${listedBy}\` prints`); + const daemonUrl = given(env, NAMES.daemonUrl); + if (daemonUrl === null) return { kind: 'local', id }; + if (!/^https?:\/\/\S+$/.test(daemonUrl)) throw new Error(`${NAMES.daemonUrl} is ${daemonUrl}, which is not the http or https address of an agent-device daemon`); + const tokenFile = given(env, NAMES.daemonTokenFile); + if (tokenFile === null) throw new Error(`${NAMES.daemonUrl} is set and ${NAMES.daemonTokenFile} is not; the daemon of a borrowed device needs its token file`); + return { kind: 'borrowed', id, daemonUrl, tokenFile }; +} + +export function readTarget(app: TestApp, env: Env): Target { + const platform = readPlatform(app, env); + return { + platform, + device: readDevice(platform, env), + session: given(env, NAMES.session) ?? `clerk-e2e-${platform}`, + build: { path: given(env, NAMES.appPath), devServer: given(env, NAMES.devServer) }, + }; +} + +const LOOPBACK = /^http:\/\/127\.0\.0\.1:\d{1,5}(\/|$)/; + +export function readClerk(env: Env): ClerkInstance { + const publishableKey = given(env, NAMES.publishableKey); + if (publishableKey === null) throw new Error(`${NAMES.publishableKey} is not set; set it to the publishable key of a development instance`); + if (!publishableKey.startsWith('pk_test_')) throw new Error(`${NAMES.publishableKey} is not a pk_test_ key; these tests create and sign in users, so they run against a development instance only`); + const secretKey = given(env, NAMES.secretKey); + const url = given(env, NAMES.apiUrl); + if (secretKey !== null && url !== null) throw new Error(`${NAMES.secretKey} and ${NAMES.apiUrl} are both set; set one, so there is one way to reach Clerk`); + if (secretKey !== null) { + if (!secretKey.startsWith('sk_test_')) throw new Error(`${NAMES.secretKey} is not an sk_test_ key; these tests create and sign in users, so they run against a development instance only`); + return { publishableKey: publishableKey as PublishableKey, access: { kind: 'secret-key', key: new Secret('clerk-secret-key', secretKey) } }; + } + if (url === null) throw new Error(`${NAMES.secretKey} is not set; set it to the secret key of the same development instance as ${NAMES.publishableKey}`); + if (!LOOPBACK.test(url)) throw new Error(`${NAMES.apiUrl} is ${url}, which is not an http://127.0.0.1: address; only a stand-in on this machine may take the place of Clerk's Backend API`); + const tokenFile = given(env, NAMES.apiTokenFile); + if (tokenFile === null) throw new Error(`${NAMES.apiUrl} is set and ${NAMES.apiTokenFile} is not; the stand-in at that address needs its token file`); + return { publishableKey: publishableKey as PublishableKey, access: { kind: 'stand-in', url, tokenFile } }; +} + +const RUN_ID = /^r\d{8}-(?