From 6bd73e5d27a2552d6d4d1f5f12473bd86ac1ff64 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 21:03:47 +0000 Subject: [PATCH 1/3] build: verify the published tarball instead of grepping the bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --- .github/workflows/ci.yml | 21 +- .github/workflows/package-smoke.yml | 2 + package.json | 58 +- pnpm-lock.yaml | 656 ++++++++++++++++++---- scripts/check-affected/checks.ts | 8 + scripts/check-affected/model.test.ts | 7 +- scripts/check-affected/model.ts | 8 + scripts/check-affected/run.test.ts | 1 + scripts/check-bundle-dependencies.ts | 59 -- scripts/check-package.ts | 229 ++++++++ src/__tests__/npm-package-scripts.test.ts | 18 +- tsdown.config.ts | 14 + 12 files changed, 866 insertions(+), 215 deletions(-) delete mode 100644 scripts/check-bundle-dependencies.ts create mode 100644 scripts/check-package.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec468b1d1a..0f49baf7ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,29 +170,22 @@ jobs: - name: Setup build toolchain uses: ./.github/actions/setup-node-pnpm - - name: Build and pack CLI + - name: Build CLI run: | pnpm build pnpm check:bundle-owner-files - mkdir -p .tmp/node-compat - npm pack --ignore-scripts --pack-destination .tmp/node-compat + # The build runs on the default toolchain Node and the package is verified on the minimum + # supported Node, so this job covers what a user on `engines.node` floor actually installs. - name: Setup Node.js 22.12 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: '22.12' - - name: Install and run global CLI on Node.js 22.12 - run: | - set -euo pipefail - prefix="$RUNNER_TEMP/agent-device-node-22-12" - tarball="$(find "$GITHUB_WORKSPACE/.tmp/node-compat" -name 'agent-device-*.tgz' -print -quit)" - test -n "$tarball" - npm install --global --prefix "$prefix" --ignore-scripts "$tarball" - "$prefix/bin/agent-device" --version - "$prefix/bin/agent-device" help - "$prefix/bin/agent-device" devices --json - "$prefix/bin/agent-device" doctor --remote --json + # Packs, lints the tarball with publint/attw, installs it outside the workspace, and imports + # every published entry point before running the CLI. See scripts/check-package.ts. + - name: Verify the published package on Node.js 22.12 + run: pnpm check:package fallow: name: Fallow Code Quality diff --git a/.github/workflows/package-smoke.yml b/.github/workflows/package-smoke.yml index 295792e5bf..17236918f3 100644 --- a/.github/workflows/package-smoke.yml +++ b/.github/workflows/package-smoke.yml @@ -8,6 +8,7 @@ on: - 'pnpm-workspace.yaml' - 'packages/**' - 'tsdown.config.ts' + - 'scripts/check-package.ts' - 'scripts/build-android-helper.sh' - 'scripts/build-xcuitest-apple.sh' - 'scripts/package-android-helper.sh' @@ -30,6 +31,7 @@ on: - 'pnpm-workspace.yaml' - 'packages/**' - 'tsdown.config.ts' + - 'scripts/check-package.ts' - 'scripts/build-android-helper.sh' - 'scripts/build-xcuitest-apple.sh' - 'scripts/package-android-helper.sh' diff --git a/package.json b/package.json index 450df9a7ff..f11e7d2a5b 100644 --- a/package.json +++ b/package.json @@ -19,52 +19,52 @@ "types": "dist/src/index.d.ts", "exports": { ".": { - "import": "./dist/src/index.js", - "types": "./dist/src/index.d.ts" + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" }, "./io": { - "import": "./dist/src/io.js", - "types": "./dist/src/io.d.ts" + "types": "./dist/src/io.d.ts", + "import": "./dist/src/io.js" }, "./artifacts": { - "import": "./dist/src/artifacts.js", - "types": "./dist/src/artifacts.d.ts" + "types": "./dist/src/artifacts.d.ts", + "import": "./dist/src/artifacts.js" }, "./metro": { - "import": "./dist/src/metro.js", - "types": "./dist/src/metro.d.ts" + "types": "./dist/src/metro.d.ts", + "import": "./dist/src/metro.js" }, "./batch": { - "import": "./dist/src/batch.js", - "types": "./dist/src/batch.d.ts" + "types": "./dist/src/batch.d.ts", + "import": "./dist/src/batch.js" }, "./remote-config": { - "import": "./dist/src/remote-config.js", - "types": "./dist/src/remote-config.d.ts" + "types": "./dist/src/remote-config.d.ts", + "import": "./dist/src/remote-config.js" }, "./install-source": { - "import": "./dist/src/install-source.js", - "types": "./dist/src/install-source.d.ts" + "types": "./dist/src/install-source.d.ts", + "import": "./dist/src/install-source.js" }, "./android-adb": { - "import": "./dist/src/android-adb.js", - "types": "./dist/src/android-adb.d.ts" + "types": "./dist/src/android-adb.d.ts", + "import": "./dist/src/android-adb.js" }, "./limrun": { - "import": "./dist/src/limrun.js", - "types": "./dist/src/limrun.d.ts" + "types": "./dist/src/limrun.d.ts", + "import": "./dist/src/limrun.js" }, "./contracts": { - "import": "./dist/src/contracts.js", - "types": "./dist/src/contracts.d.ts" + "types": "./dist/src/contracts.d.ts", + "import": "./dist/src/contracts.js" }, "./selectors": { - "import": "./dist/src/selectors.js", - "types": "./dist/src/selectors.d.ts" + "types": "./dist/src/selectors.d.ts", + "import": "./dist/src/selectors.js" }, "./finders": { - "import": "./dist/src/finders.js", - "types": "./dist/src/finders.d.ts" + "types": "./dist/src/finders.d.ts", + "import": "./dist/src/finders.js" } }, "engines": { @@ -103,7 +103,7 @@ "package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist", "build:macos-helper": "swift build -c release --package-path apple/macos-helper", "build:macos-helper:clean": "swift package --package-path apple/macos-helper clean && pnpm build:macos-helper", - "package:npm": "pnpm build && pnpm check:bundle-dependencies && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android", + "package:npm": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android && pnpm check:package", "ad": "node bin/agent-device.mjs", "bench:help-conformance": "node scripts/help-conformance-bench.mjs", "maestro:conformance": "node --experimental-strip-types --test packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts", @@ -134,7 +134,7 @@ "depgraph:test": "node --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts", "check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues", "check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts", - "check:bundle-dependencies": "node --experimental-strip-types scripts/check-bundle-dependencies.ts", + "check:package": "node --experimental-strip-types scripts/check-package.ts", "check:command-docs": "vitest run --project unit-core src/__tests__/command-doc-coverage.test.ts", "check:replay-compat": "node --experimental-strip-types scripts/check-replay-compat-provenance.ts", "check:freerange": "fr", @@ -142,7 +142,7 @@ "sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs", "check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check", "version": "pnpm sync:mcp-metadata && git add server.json", - "check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files && pnpm check:bundle-dependencies", + "check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files && pnpm check:package", "check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "pnpm check:mcp-metadata && pnpm package:npm", @@ -242,7 +242,6 @@ ], "dependencies": { "@limrun/api": "^0.24.5", - "pngjs": "^7.0.0", "yaml": "^2.9.0" }, "devDependencies": { @@ -255,6 +254,7 @@ "@agent-device/provider-webdriver": "workspace:*", "@agent-device/replay-test": "workspace:*", "@agent-device/xml": "workspace:*", + "@arethetypeswrong/cli": "^0.18.5", "@chenglou/freerange": "^0.0.1", "@stryker-mutator/core": "9.6.1", "@stryker-mutator/vitest-runner": "9.6.1", @@ -266,6 +266,8 @@ "oxc-parser": "^0.138.0", "oxfmt": "^0.42.0", "oxlint": "^1.69.0", + "pngjs": "^7.0.0", + "publint": "^0.3.22", "tsdown": "^0.22.4", "typescript": "^7.0.2", "vite": "^8.0.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51903212b3..ed78a3a86a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@limrun/api': specifier: ^0.24.5 version: 0.24.5(supports-color@7.2.0) - pngjs: - specifier: ^7.0.0 - version: 7.0.0 yaml: specifier: ^2.9.0 version: 2.9.0 @@ -51,6 +48,9 @@ importers: '@agent-device/xml': specifier: workspace:* version: link:packages/xml + '@arethetypeswrong/cli': + specifier: ^0.18.5 + version: 0.18.5 '@chenglou/freerange': specifier: ^0.0.1 version: 0.0.1 @@ -84,9 +84,15 @@ importers: oxlint: specifier: ^1.69.0 version: 1.69.0 + pngjs: + specifier: ^7.0.0 + version: 7.0.0 + publint: + specifier: ^0.3.22 + version: 0.3.22 tsdown: specifier: ^0.22.4 - version: 0.22.4(typescript@7.0.2) + version: 0.22.4(@arethetypeswrong/core@0.18.5)(publint@0.3.22)(typescript@7.0.2) typescript: specifier: ^7.0.2 version: 7.0.2 @@ -177,13 +183,25 @@ importers: devDependencies: '@callstack/rspress-preset': specifier: ^0.6.6 - version: 0.6.6(@rsbuild/core@2.0.11)(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 0.6.6(@rsbuild/core@2.0.11)(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rspress/core': specifier: ^2.0.12 - version: 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2) + version: 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) packages: + '@andrewbranch/untar.js@1.0.3': + resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} + + '@arethetypeswrong/cli@0.18.5': + resolution: {integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==} + engines: {node: '>=20'} + hasBin: true + + '@arethetypeswrong/core@0.18.5': + resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} + engines: {node: '>=20'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -358,6 +376,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + '@callstack/rspress-preset@0.6.6': resolution: {integrity: sha512-JvhV9eYZLx3maNOqBdSQOsteg2kMH+OWUEGsItP95AEp9ht7mpk4KZXS1I2AuRvq3PKfYw6PTYtsjOzWQKHP+w==} peerDependencies: @@ -374,6 +395,10 @@ packages: resolution: {integrity: sha512-RCdvTZX66Dp5roRrld+2GH4tJV+uyo21nEsF/lxwDBjzDFagG9CnJ7go5Qim2ZDHTC40lQWNF1AprDxTDQTxfg==} hasBin: true + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -585,6 +610,9 @@ packages: '@limrun/api@0.24.5': resolution: {integrity: sha512-bgIs+KcP9xITj1v2gMe8wJrvqPakl1PlXtvxlO6Akci8ijU7V27RWQAq7JBaA89AzwUb5EvLc+nmAMMiKl/g1A==} + '@loaderkit/resolve@1.0.6': + resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -983,6 +1011,10 @@ packages: cpu: [x64] os: [win32] + '@publint/pack@0.1.6': + resolution: {integrity: sha512-3uVNyGcVplhPZSLVyeIpL7+cIRn1YCSNHLG/rUIlBQMVH8YuN9++YF+5+UDIIO9RW98dujiUoTltO7RDB5bFJA==} + engines: {node: '>=18'} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -1326,6 +1358,10 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -1746,10 +1782,29 @@ packages: resolution: {integrity: sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==} engines: {node: '>= 14'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansis@4.3.1: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1807,10 +1862,18 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1826,10 +1889,25 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1837,9 +1915,20 @@ packages: collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1915,6 +2004,12 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.1: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} @@ -1923,6 +2018,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2025,6 +2124,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -2044,6 +2146,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -2116,6 +2222,9 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} @@ -2167,6 +2276,10 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -2302,6 +2415,10 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2322,6 +2439,17 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2518,6 +2646,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2538,11 +2670,18 @@ packages: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -2554,6 +2693,10 @@ packages: nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2593,6 +2736,9 @@ packages: vite-plus: optional: true + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -2600,6 +2746,15 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -2648,6 +2803,11 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + publint@0.3.22: + resolution: {integrity: sha512-6Z/scsr5CA7APdwyF35EY88CqgDj1textWuY788DVTJYPCWVv/Wn9G6KmLnrVRnStgYcahqN4wCDLZGSbQJ69w==} + engines: {node: '>=18'} + hasBin: true + pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} @@ -2769,6 +2929,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2816,6 +2980,10 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2877,6 +3045,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2894,9 +3066,17 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-final-newline@4.0.0: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} @@ -2911,6 +3091,17 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3000,6 +3191,11 @@ packages: resolution: {integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==} engines: {node: '>= 16.0.0'} + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -3026,6 +3222,10 @@ packages: unhead@2.1.15: resolution: {integrity: sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA==} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -3063,6 +3263,10 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -3172,6 +3376,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -3184,6 +3392,10 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3192,6 +3404,14 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + yoctocolors@2.2.0: resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} @@ -3213,6 +3433,29 @@ packages: snapshots: + '@andrewbranch/untar.js@1.0.3': {} + + '@arethetypeswrong/cli@0.18.5': + dependencies: + '@arethetypeswrong/core': 0.18.5 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.8.5 + + '@arethetypeswrong/core@0.18.5': + dependencies: + '@andrewbranch/untar.js': 1.0.3 + '@loaderkit/resolve': 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.5.2 + semver: 7.8.5 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -3226,7 +3469,7 @@ snapshots: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 @@ -3261,14 +3504,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) '@babel/traverse': 7.29.7(supports-color@7.2.0) semver: 6.3.1 transitivePeerDependencies: @@ -3276,24 +3519,24 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7': + '@babel/helper-member-expression-to-functions@7.29.7(supports-color@7.2.0)': dependencies: '@babel/traverse': 7.29.7(supports-color@7.2.0) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': dependencies: '@babel/traverse': 7.29.7(supports-color@7.2.0) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7 + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.7(supports-color@7.2.0) transitivePeerDependencies: @@ -3305,16 +3548,16 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0) '@babel/helper-optimise-call-expression': 7.29.7 '@babel/traverse': 7.29.7(supports-color@7.2.0) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@7.2.0)': dependencies: '@babel/traverse': 7.29.7(supports-color@7.2.0) '@babel/types': 7.29.7 @@ -3344,10 +3587,10 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) transitivePeerDependencies: @@ -3368,7 +3611,7 @@ snapshots: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 @@ -3376,41 +3619,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/preset-typescript@7.28.5(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -3444,11 +3687,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@callstack/rspress-preset@0.6.6(@rsbuild/core@2.0.11)(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@braidai/lang@1.1.2': {} + + '@callstack/rspress-preset@0.6.6(@rsbuild/core@2.0.11)(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@callstack/rspress-theme': 0.6.6(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2) - '@rspress/plugin-sitemap': 2.0.8(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2)) + '@callstack/rspress-theme': 0.6.6(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + '@rspress/plugin-sitemap': 2.0.8(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)) '@vercel/analytics': 2.0.1(react@19.2.7) rsbuild-plugin-open-graph: 1.1.2(@rsbuild/core@2.0.11) zod: 4.3.6 @@ -3464,9 +3709,9 @@ snapshots: - vue - vue-router - '@callstack/rspress-theme@0.6.6(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@callstack/rspress-theme@0.6.6(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2) + '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -3474,6 +3719,9 @@ snapshots: dependencies: typescript: 6.0.3 + '@colors/colors@1.5.0': + optional: true + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -3681,7 +3929,11 @@ snapshots: - supports-color - utf-8-validate - '@mdx-js/mdx@3.1.1': + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + + '@mdx-js/mdx@3.1.1(supports-color@7.2.0)': dependencies: '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.5 @@ -3693,14 +3945,14 @@ snapshots: estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0) markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 recma-jsx: 1.0.1(acorn@8.16.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + rehype-recma: 1.0.0(supports-color@7.2.0) + remark-mdx: 3.1.1(supports-color@7.2.0) + remark-parse: 11.0.0(supports-color@7.2.0) remark-rehype: 11.1.2 source-map: 0.7.6 unified: 11.0.5 @@ -3920,6 +4172,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.69.0': optional: true + '@publint/pack@0.1.6': + dependencies: + tinyexec: 1.2.4 + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -4099,9 +4355,9 @@ snapshots: optionalDependencies: '@rspack/core': 2.0.6(@swc/helpers@0.5.23) - '@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2)': + '@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@mdx-js/mdx': 3.1.1 + '@mdx-js/mdx': 3.1.1(supports-color@7.2.0) '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.2.7) '@rsbuild/core': 2.0.11 '@rsbuild/plugin-react': 2.0.0(@rsbuild/core@2.0.11)(@rspack/core@2.0.6(@swc/helpers@0.5.23)) @@ -4114,9 +4370,9 @@ snapshots: copy-to-clipboard: 3.3.3 flexsearch: 0.8.212 hast-util-heading-rank: 3.0.0 - hast-util-to-jsx-runtime: 2.3.6 - mdast-util-mdx: 3.0.0 - mdast-util-mdxjs-esm: 2.0.1 + hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0) + mdast-util-mdx: 3.0.0(supports-color@7.2.0) + mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) medium-zoom: 1.1.0 nprogress: 0.2.0 react: 19.2.7 @@ -4127,11 +4383,11 @@ snapshots: react-router-dom: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) rehype-external-links: 3.0.0 rehype-raw: 7.0.0 - remark-cjk-friendly: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - remark-cjk-friendly-gfm-strikethrough: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - remark-gfm: 4.0.1 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + remark-cjk-friendly: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5) + remark-cjk-friendly-gfm-strikethrough: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5) + remark-gfm: 4.0.1(supports-color@7.2.0) + remark-mdx: 3.1.1(supports-color@7.2.0) + remark-parse: 11.0.0(supports-color@7.2.0) remark-stringify: 11.0.0 scroll-into-view-if-needed: 3.1.0 shiki: 4.0.2 @@ -4149,9 +4405,9 @@ snapshots: - micromark-util-types - supports-color - '@rspress/plugin-sitemap@2.0.8(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))': + '@rspress/plugin-sitemap@2.0.8(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))': dependencies: - '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2) + '@rspress/core': 2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) '@rspress/shared@2.0.12': dependencies: @@ -4213,6 +4469,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@4.6.0': {} + '@sindresorhus/merge-streams@4.0.0': {} '@standard-schema/spec@1.1.0': {} @@ -4261,9 +4519,9 @@ snapshots: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/generator': 7.29.7 '@babel/parser': 7.29.3 - '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@stryker-mutator/api': 9.6.1 '@stryker-mutator/util': 9.6.1 angular-html-parser: 10.4.0 @@ -4549,8 +4807,22 @@ snapshots: angular-html-parser@10.4.0: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansis@4.3.1: {} + any-promise@1.3.0: {} + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.0: @@ -4599,8 +4871,15 @@ snapshots: chai@6.2.2: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.6.2: {} + char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -4611,14 +4890,45 @@ snapshots: chardet@2.2.0: {} + cjs-module-lexer@1.4.3: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-width@4.1.0: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} collapse-white-space@2.1.0: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@14.0.3: {} compute-scroll-into-view@3.1.1: {} @@ -4678,10 +4988,16 @@ snapshots: emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} + + emojilib@2.4.0: {} + empathic@2.0.1: {} entities@6.0.1: {} + environment@1.1.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -4803,6 +5119,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.8.3: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -4816,6 +5134,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -4894,7 +5214,7 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-estree@3.1.3: + hast-util-to-estree@3.1.3(supports-color@7.2.0): dependencies: '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.5 @@ -4904,9 +5224,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@7.2.0) + mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) + mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) property-information: 7.1.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -4929,7 +5249,7 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@7.2.0): dependencies: '@types/estree': 1.0.8 '@types/hast': 3.0.4 @@ -4938,9 +5258,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@7.2.0) + mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) + mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) property-information: 7.1.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -4975,6 +5295,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + highlight.js@10.7.3: {} + hookable@6.1.0: {} hookable@6.1.1: {} @@ -5015,6 +5337,8 @@ snapshots: is-decimal@2.0.1: {} + is-fullwidth-code-point@3.0.0: {} + is-hexadecimal@2.0.1: {} is-plain-obj@4.1.0: {} @@ -5105,6 +5429,8 @@ snapshots: longest-streak@3.1.0: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5127,6 +5453,19 @@ snapshots: markdown-table@3.0.4: {} + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.2.2 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + + marked@9.1.6: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -5136,14 +5475,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@7.2.0) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -5161,67 +5500,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@7.2.0): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@7.2.0) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@7.2.0) + mdast-util-gfm-table: 2.0.0(supports-color@7.2.0) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@7.2.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@7.2.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 @@ -5229,7 +5568,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -5238,23 +5577,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0: + mdast-util-mdx@3.0.0(supports-color@7.2.0): dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-mdx-expression: 2.0.1(supports-color@7.2.0) + mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) + mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@7.2.0): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -5313,11 +5652,11 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): + micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0)): dependencies: devlop: 1.1.0 get-east-asian-width: 1.5.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@7.2.0) micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) micromark-util-character: 2.1.1 micromark-util-chunked: 2.0.1 @@ -5334,10 +5673,10 @@ snapshots: optionalDependencies: micromark-util-types: 2.0.2 - micromark-extension-cjk-friendly@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): + micromark-extension-cjk-friendly@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0)): dependencies: devlop: 1.1.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@7.2.0) micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) micromark-util-chunked: 2.0.1 micromark-util-resolve-all: 2.0.1 @@ -5568,7 +5907,7 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@7.2.0): dependencies: '@types/debug': 4.1.13 debug: 4.4.3(supports-color@7.2.0) @@ -5596,6 +5935,8 @@ snapshots: dependencies: brace-expansion: 5.0.8 + mri@1.2.0: {} + ms@2.1.3: {} mutation-server-protocol@0.4.1: @@ -5612,8 +5953,21 @@ snapshots: mute-stream@3.0.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.12: {} + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-releases@2.0.51: {} npm-run-path@6.0.0: @@ -5623,6 +5977,8 @@ snapshots: nprogress@0.2.0: {} + object-assign@4.1.1: {} + object-inspect@1.13.4: {} obug@2.1.1: {} @@ -5708,6 +6064,8 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.69.0 '@oxlint/binding-win32-x64-msvc': 1.69.0 + package-manager-detector@1.8.0: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -5720,6 +6078,14 @@ snapshots: parse-ms@4.0.0: {} + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -5754,6 +6120,13 @@ snapshots: proxy-from-env@2.1.0: {} + publint@0.3.22: + dependencies: + '@publint/pack': 0.1.6 + package-manager-detector: 1.8.0 + picocolors: 1.1.1 + sade: 1.8.1 + pure-rand@8.4.2: {} qs@6.15.1: @@ -5851,17 +6224,17 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 - rehype-recma@1.0.0: + rehype-recma@1.0.0(supports-color@7.2.0): dependencies: '@types/estree': 1.0.8 '@types/hast': 3.0.4 - hast-util-to-estree: 3.1.3 + hast-util-to-estree: 3.1.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color - remark-cjk-friendly-gfm-strikethrough@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): + remark-cjk-friendly-gfm-strikethrough@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5): dependencies: - micromark-extension-cjk-friendly-gfm-strikethrough: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) + micromark-extension-cjk-friendly-gfm-strikethrough: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0)) unified: 11.0.5 optionalDependencies: '@types/mdast': 4.0.4 @@ -5869,9 +6242,9 @@ snapshots: - micromark - micromark-util-types - remark-cjk-friendly@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): + remark-cjk-friendly@2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5): dependencies: - micromark-extension-cjk-friendly: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) + micromark-extension-cjk-friendly: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0)) unified: 11.0.5 optionalDependencies: '@types/mdast': 4.0.4 @@ -5879,28 +6252,28 @@ snapshots: - micromark - micromark-util-types - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@7.2.0) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@7.2.0) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1: + remark-mdx@3.1.1(supports-color@7.2.0): dependencies: - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@7.2.0) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -5920,6 +6293,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-pkg-maps@1.0.0: {} @@ -5988,6 +6363,10 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + safer-buffer@2.1.2: {} scheduler@0.27.0: {} @@ -6053,6 +6432,10 @@ snapshots: signal-exit@4.1.0: {} + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + source-map-js@1.2.1: {} source-map@0.7.6: {} @@ -6063,11 +6446,21 @@ snapshots: std-env@4.0.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-final-newline@4.0.0: {} style-to-js@1.1.21: @@ -6082,6 +6475,19 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + tinybench@2.9.0: {} tinyexec@1.0.4: {} @@ -6110,7 +6516,7 @@ snapshots: trough@2.2.0: {} - tsdown@0.22.4(typescript@7.0.2): + tsdown@0.22.4(@arethetypeswrong/core@0.18.5)(publint@0.3.22)(typescript@7.0.2): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -6128,6 +6534,8 @@ snapshots: tree-kill: 1.2.2 unconfig-core: 7.5.0 optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + publint: 0.3.22 typescript: 7.0.2 transitivePeerDependencies: - '@ts-macro/tsc' @@ -6149,6 +6557,8 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.8 + typescript@5.6.1-rc: {} + typescript@6.0.3: {} typescript@7.0.2: @@ -6189,6 +6599,8 @@ snapshots: dependencies: hookable: 6.1.0 + unicode-emoji-modifier-base@1.0.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -6244,6 +6656,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + validate-npm-package-name@5.0.1: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -6312,12 +6726,32 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.21.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@2.9.0: {} + yargs-parser@20.2.9: {} + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yoctocolors@2.2.0: {} yuku-ast@0.1.7: diff --git a/scripts/check-affected/checks.ts b/scripts/check-affected/checks.ts index 64b762029a..75775003e9 100644 --- a/scripts/check-affected/checks.ts +++ b/scripts/check-affected/checks.ts @@ -80,6 +80,14 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [ ciJobs: ['Packaged CLI Node 22.12'], localRunnable: true, }, + { + id: 'package', + label: 'Published package (publint, attw, clean-install resolution)', + kind: { type: 'script', script: 'check:package' }, + ciJobs: ['Packaged CLI Node 22.12', 'Build npm package'], + // Needs a `pnpm build` output and the npm registry, both of which local runs already have. + localRunnable: true, + }, { id: 'vitest-related', label: 'Tests related by Vitest module graph', diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts index 65c70c7d31..b4d50793f6 100644 --- a/scripts/check-affected/model.test.ts +++ b/scripts/check-affected/model.test.ts @@ -88,9 +88,13 @@ test('MCP metadata change selects the mcp-metadata check', () => { assert.deepEqual(ids(['server.json']), ['mcp-metadata']); }); -test('public package surface change selects the build via exports', () => { +test('public package surface change selects the build and the published-package gate via exports', () => { const result = ids(['src/index.ts']); assert.ok(result.includes('build')); + // A public entry is the one surface a consumer resolves by name, so building it is not enough: + // check:package proves it still imports from an install with no workspace links. + assert.ok(result.includes('package')); + assert.ok(!ids(['src/selectors/index.ts']).includes('package')); }); test('docs-only change selects no checks and records the docs paths', () => { @@ -205,6 +209,7 @@ test('every catalog command resolves against package scripts', () => { 'check:fallow': 'x', 'check:mcp-metadata': 'x', build: 'x', + 'check:package': 'x', 'check:unit': 'x', 'check:coverage-changed': 'x', 'test:coverage': 'x', diff --git a/scripts/check-affected/model.ts b/scripts/check-affected/model.ts index fda1e377b8..be957b13ce 100644 --- a/scripts/check-affected/model.ts +++ b/scripts/check-affected/model.ts @@ -28,6 +28,7 @@ export type CheckId = | 'fallow' | 'mcp-metadata' | 'build' + | 'package' | 'vitest-related' | 'unit' | 'coverage' @@ -51,6 +52,7 @@ export const ALL_CHECKS: readonly CheckId[] = [ 'fallow', 'mcp-metadata', 'build', + 'package', 'vitest-related', 'unit', 'coverage', @@ -338,6 +340,12 @@ const buildOwnership: OwnershipRule = ({ file }, input) => { if ((input.packageEntryFiles ?? []).includes(file)) { selections.push( reason('build', file, 'own:public-surface', 'public package entry affects declarations'), + reason( + 'package', + file, + 'own:public-surface', + 'a public entry must still resolve from a clean install', + ), ); } return selections; diff --git a/scripts/check-affected/run.test.ts b/scripts/check-affected/run.test.ts index f36c372daa..bbbcae1f0b 100644 --- a/scripts/check-affected/run.test.ts +++ b/scripts/check-affected/run.test.ts @@ -96,6 +96,7 @@ const ALL_SCRIPTS: Record = { 'check:fallow': 'x', 'check:mcp-metadata': 'x', build: 'x', + 'check:package': 'x', 'check:unit': 'x', 'check:coverage-changed': 'x', 'test:integration:provider': 'x', diff --git a/scripts/check-bundle-dependencies.ts b/scripts/check-bundle-dependencies.ts deleted file mode 100644 index 691e974ce8..0000000000 --- a/scripts/check-bundle-dependencies.ts +++ /dev/null @@ -1,59 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { parseSync } from 'oxc-parser'; -import { walkFiles } from './lib/walk-files.ts'; - -const repoRoot = path.resolve(import.meta.dirname, '..'); -const distRoot = path.join(repoRoot, 'dist', 'src'); - -function moduleSpecifiers(file: string, source: string): string[] { - const record = parseSync(file, source).module; - return [ - ...record.staticImports.map((entry) => entry.moduleRequest.value), - ...record.staticExports.flatMap((entry) => - entry.entries.flatMap((exported) => moduleRequestValue(exported.moduleRequest)), - ), - ...record.dynamicImports.flatMap((entry) => - dynamicModuleRequestValue(source, entry.moduleRequest), - ), - ]; -} - -function moduleRequestValue(request: { value?: string } | undefined): string[] { - return request?.value ? [request.value] : []; -} - -function dynamicModuleRequestValue( - source: string, - request: { start: number; end: number }, -): string[] { - const raw = source.slice(request.start, request.end); - const literal = /^(['"])([^'"]*)\1$/.exec(raw); - return literal?.[2] ? [literal[2]] : []; -} - -const bundleFiles = walkFiles(distRoot).filter( - (file) => file.endsWith('.js') || file.endsWith('.d.ts'), -); -if (bundleFiles.length === 0) { - throw new Error('No dist/src JavaScript files found. Run `pnpm build` first.'); -} - -const leaks = bundleFiles.flatMap((file) => { - const source = fs.readFileSync(file, 'utf8'); - return moduleSpecifiers(file, source) - .filter((specifier) => specifier.startsWith('@agent-device/')) - .map((specifier) => ({ file: path.relative(repoRoot, file), specifier })); -}); - -if (leaks.length > 0) { - const details = leaks.map(({ file, specifier }) => `- ${specifier} in ${file}`).join('\n'); - throw new Error( - `Private workspace dependencies escaped the production bundle:\n${details}\n` + - 'Published installs cannot resolve private @agent-device packages.', - ); -} - -process.stdout.write( - `Verified ${bundleFiles.length} production module files contain no private workspace imports.\n`, -); diff --git a/scripts/check-package.ts b/scripts/check-package.ts new file mode 100644 index 0000000000..1c39d2eae7 --- /dev/null +++ b/scripts/check-package.ts @@ -0,0 +1,229 @@ +/** + * The publishing gate: packs the tarball npm would publish, then proves it sound from a clean + * consumer install rather than from the workspace. Published 0.20.4 broke on `agent-device devices` + * with an unresolvable `@agent-device/ad-script` import (#1577) because nothing between `pnpm build` + * and `npm publish` ever resolved the package the way a user does — every check here is a check that + * would have caught it. + * + * Checked, in order: + * 1. `publint` — packaging metadata against the packed tarball (exports/bin/types conditions). + * 2. `attw` — declaration resolution for the module systems this package supports. + * 3. runtime dependency closure — every bare specifier the shipped files import is a Node builtin + * or a declared `dependencies` entry, and every declared entry is actually imported. + * 4. every `exports` subpath imports, and the `bin` runs, from outside the workspace. + * + * Step 3 is the static half and step 4 the runtime half of the same question: nothing the package + * imports may depend on workspace linking. Keep both — a specifier reachable only through a lazy + * dynamic import stays invisible to step 4, and a `require` computed at runtime stays invisible to + * step 3. + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import { builtinModules } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import { parseSync } from 'oxc-parser'; +import { walkFiles } from './lib/walk-files.ts'; + +type PackedManifest = { + dependencies?: Record; + exports: Record; + bin: Record; +}; + +const repoRoot = path.resolve(import.meta.dirname, '..'); +// `npm install` resolves `file:` tarballs through the real path, and macOS `/var` is a symlink to +// `/private/var`; resolving up front keeps the paths this script prints equal to the ones npm uses. +const workDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'agent-device-package-')); +const consumerDir = path.join(workDir, 'consumer'); +const builtins = new Set(builtinModules.flatMap((name) => [name, `node:${name}`])); + +/** Stdout is captured for the callers that parse it; stderr passes through so failures are readable. */ +function run(command: string, args: string[], cwd: string): string { + return execFileSync(command, args, { + cwd, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'inherit'], + }); +} + +function localBin(name: string): string { + return path.join(repoRoot, 'node_modules', '.bin', name); +} + +function step(message: string): void { + process.stdout.write(`${message}\n`); +} + +/** Packs with `--ignore-scripts` so the pack cannot re-enter `prepack` and recurse. */ +function packTarball(): string { + if (!fs.existsSync(path.join(repoRoot, 'dist', 'src'))) { + throw new Error('No dist/src build found. Run `pnpm build` first.'); + } + const packed = JSON.parse( + run('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', workDir], repoRoot), + ) as [{ filename: string }]; + return path.join(workDir, packed[0].filename); +} + +function lintTarball(tarball: string): void { + run(localBin('publint'), ['--strict', tarball], repoRoot); + // This package is ESM-only (`type: module`, `engines.node >= 22.12`), so the `esm-only` profile is + // the honest one: it keeps every ESM and bundler resolution failing, and drops the CJS-consumer and + // pre-`exports` node10 rules that no supported consumer can hit. + run(localBin('attw'), ['--profile', 'esm-only', tarball], repoRoot); +} + +/** + * Installs the tarball into a project outside the workspace. `node_modules` here is built by npm + * from the registry, so a workspace-only specifier has nothing to resolve against — exactly the + * position a user installing from npm is in. + */ +function installIntoCleanConsumer(tarball: string): string { + fs.mkdirSync(consumerDir); + fs.writeFileSync( + path.join(consumerDir, 'package.json'), + `${JSON.stringify({ name: 'agent-device-package-check', private: true, type: 'module' }, null, 2)}\n`, + ); + run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], consumerDir); + return path.join(consumerDir, 'node_modules', 'agent-device'); +} + +function packageNameOf(specifier: string): string { + const segments = specifier.split('/'); + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]!; +} + +function moduleSpecifiers(file: string, source: string): string[] { + const record = parseSync(file, source).module; + return [ + ...record.staticImports.map((entry) => entry.moduleRequest.value), + ...record.staticExports.flatMap((entry) => + entry.entries.flatMap((exported) => + exported.moduleRequest?.value ? [exported.moduleRequest.value] : [], + ), + ), + ...record.dynamicImports.flatMap((entry) => { + const raw = source.slice(entry.moduleRequest.start, entry.moduleRequest.end); + const literal = /^(['"])([^'"]*)\1$/.exec(raw); + return literal?.[2] ? [literal[2]] : []; + }), + ]; +} + +/** + * Both directions matter. An import the manifest does not declare breaks the install; a declared + * dependency nothing imports is an install every user pays for and no code reaches — how `pngjs` + * stayed in `dependencies` after `tsdown.config.ts` started inlining it. + */ +function auditDependencyClosure(installedRoot: string, manifest: PackedManifest): void { + const declared = new Set(Object.keys(manifest.dependencies ?? {})); + const importedBy = new Map(); + const undeclared: string[] = []; + + for (const file of walkFiles(installedRoot, (file) => /\.(?:m?js|d\.ts)$/.test(file))) { + const relative = path.relative(installedRoot, file); + for (const specifier of moduleSpecifiers(file, fs.readFileSync(file, 'utf8'))) { + if (specifier.startsWith('.') || specifier.startsWith('/') || builtins.has(specifier)) + continue; + const name = packageNameOf(specifier); + if (!declared.has(name)) undeclared.push(`${specifier} in ${relative}`); + importedBy.set(name, [...(importedBy.get(name) ?? []), relative]); + } + } + + const unused = [...declared].filter((name) => !importedBy.has(name)); + if (undeclared.length > 0 || unused.length > 0) { + throw new Error( + [ + 'The published dependency closure does not match what the package imports.', + ...(undeclared.length > 0 + ? [ + 'Imported but not declared in "dependencies" (a published install cannot resolve these):', + ...undeclared.map((entry) => ` - ${entry}`), + 'Declare the package, or add it to `deps.alwaysBundle` in tsdown.config.ts.', + ] + : []), + ...(unused.length > 0 + ? [ + 'Declared in "dependencies" but never imported (every user installs these for nothing):', + ...unused.map((name) => ` - ${name}`), + 'Remove the dependency, or stop bundling it in tsdown.config.ts.', + ] + : []), + ].join('\n'), + ); + } + step(`Verified the dependency closure: ${[...importedBy.keys()].sort().join(', ')}.`); +} + +/** Imports every documented entry point in one process so a failure names the subpath that broke. */ +function importEveryExport(manifest: PackedManifest): void { + const specifiers = Object.keys(manifest.exports).map((subpath) => + path.posix.join('agent-device', subpath), + ); + const probe = path.join(consumerDir, 'import-exports.mjs'); + fs.writeFileSync( + probe, + `const specifiers = ${JSON.stringify(specifiers)}; +const failures = []; +for (const specifier of specifiers) { + try { + await import(specifier); + } catch (error) { + failures.push(' - ' + specifier + ': ' + error.message); + } +} +if (failures.length > 0) { + process.stderr.write('Published entry points failed to import:\\n' + failures.join('\\n') + '\\n'); + process.exit(1); +} +`, + ); + run(process.execPath, [probe], consumerDir); + step(`Imported all ${specifiers.length} published entry points from a clean install.`); +} + +/** + * `devices` and `doctor --remote` are the cheapest commands that load the daemon bundle and the + * remote-config graph — the lazily imported halves of the CLI that no `--version` or `help` run + * reaches, and where the 0.20.4 unresolved import actually surfaced. Every command is device-free and + * offline. `--state-dir` keeps the daemon they start out of the developer's `~/.agent-device`, and + * `daemon stop` leaves nothing running behind the check. + */ +function smokeTestBin(installedRoot: string, manifest: PackedManifest): void { + const binPath = path.join(installedRoot, manifest.bin['agent-device']!); + const stateDir = ['--state-dir', path.join(workDir, 'state')]; + const version = run(process.execPath, [binPath, '--version'], consumerDir).trim(); + run(process.execPath, [binPath, 'help'], consumerDir); + try { + for (const args of [ + ['devices', '--json'], + ['doctor', '--remote', '--json'], + ]) { + run(process.execPath, [binPath, ...args, ...stateDir], consumerDir); + } + } finally { + run(process.execPath, [binPath, 'daemon', 'stop', ...stateDir], consumerDir); + } + step(`Ran the published CLI ${version} on Node ${process.versions.node}.`); +} + +step(`Packing and verifying agent-device in ${workDir}`); +try { + const tarball = packTarball(); + lintTarball(tarball); + const installedRoot = installIntoCleanConsumer(tarball); + const manifest = JSON.parse( + fs.readFileSync(path.join(installedRoot, 'package.json'), 'utf8'), + ) as PackedManifest; + auditDependencyClosure(installedRoot, manifest); + importEveryExport(manifest); + smokeTestBin(installedRoot, manifest); +} catch (error) { + process.stderr.write(`Package verification failed. Retained ${workDir} for inspection.\n`); + throw error; +} +fs.rmSync(workDir, { recursive: true, force: true }); +step('The package npm would publish is sound.'); diff --git a/src/__tests__/npm-package-scripts.test.ts b/src/__tests__/npm-package-scripts.test.ts index 4179ad2ede..51eecb600c 100644 --- a/src/__tests__/npm-package-scripts.test.ts +++ b/src/__tests__/npm-package-scripts.test.ts @@ -16,6 +16,10 @@ const packageSmokeWorkflow = fs.readFileSync( path.join(repoRoot, '.github', 'workflows', 'package-smoke.yml'), 'utf8', ); +const packagedCliWorkflow = fs.readFileSync( + path.join(repoRoot, '.github', 'workflows', 'ci.yml'), + 'utf8', +); function script(name: string): string { const value = packageJson.scripts[name]; @@ -55,10 +59,11 @@ test('Fallow exposes one changed-code gate and an explicit full-tree audit', () assert.equal(script('fallow:all'), 'fallow --summary'); }); -test('the npm package build covers every package-owned build output', () => { +// `check:package` verifies the tarball, so it has to observe every build output the package ships — +// it runs last, after the Apple and Android payloads exist, not next to the JS build. +test('the npm package build covers every package-owned build output, then verifies the result', () => { assert.deepEqual(script('package:npm').split(' && '), [ 'pnpm build', - 'pnpm check:bundle-dependencies', 'pnpm build:xcuitest:ios', 'pnpm build:xcuitest:macos', 'pnpm build:xcuitest:tvos', @@ -66,6 +71,7 @@ test('the npm package build covers every package-owned build output', () => { 'pnpm build:macos-helper:clean', 'pnpm package:apple-runner:npm', 'pnpm build:android', + 'pnpm check:package', ]); assert.deepEqual(script('build:android').split(' && '), [ @@ -73,3 +79,11 @@ test('the npm package build covers every package-owned build output', () => { 'pnpm package:android-ime-helper:npm', ]); }); + +// The gate reads the packed tarball, so `prepack` is the last point where a broken package can still +// be stopped. Publishing runs it; nothing else guarantees the tarball is ever verified. +test('publishing cannot skip the package gate', () => { + assert.match(script('package:npm'), /pnpm check:package$/); + assert.match(script('check:tooling'), /pnpm check:package$/); + assert.match(packagedCliWorkflow, /run: pnpm check:package/); +}); diff --git a/tsdown.config.ts b/tsdown.config.ts index 0f27d7a218..57f4629db7 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -76,6 +76,20 @@ export default defineConfig({ deps: { alwaysBundle: [/^@agent-device\//, 'pngjs'], }, + inputOptions: { + // A build with missing workspace links resolves nothing under `alwaysBundle` and emits the + // specifiers as externals instead. That is how 0.20.4 shipped an unresolvable + // `@agent-device/ad-script` import: rolldown warned, exited 0, and `prepack` packed the result. + // An unresolved import in a bundle that is supposed to inline its workspace is never a warning. + onLog(level, log, handler) { + if (log.code === 'UNRESOLVED_IMPORT') { + throw new Error( + `${log.message}\nRun \`pnpm install\` to restore workspace links: unresolved imports would ship as unresolvable externals.`, + ); + } + handler(level, log); + }, + }, format: 'esm', platform: 'node', target: 'es2022', From 316533f7649b1b68bf67bee5b913a3aa95095a38 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 21:18:34 +0000 Subject: [PATCH 2/3] fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --- .fallowrc.json | 6 +- .github/workflows/ci.yml | 6 +- scripts/check-package.ts | 97 +++++++++++++---------- src/__tests__/npm-package-scripts.test.ts | 8 +- 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/.fallowrc.json b/.fallowrc.json index 6c9fb0d23f..31a5282231 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -50,7 +50,11 @@ "yaml", // @agent-device/provider-limrun owns the source import, while the published // root build externalizes @limrun/api and retains runtime imports in packed chunks. - "@limrun/api" + "@limrun/api", + // Packaging linters with no importable API surface here: scripts/check-package.ts runs their + // `node_modules/.bin` shims as subprocesses, which dependency analysis cannot follow to an import. + "publint", + "@arethetypeswrong/cli" ], "ignoreExports": [ { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f49baf7ac..5fc6978d41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,8 +184,12 @@ jobs: # Packs, lints the tarball with publint/attw, installs it outside the workspace, and imports # every published entry point before running the CLI. See scripts/check-package.ts. + # + # Runs the script directly rather than through `pnpm check:package`: the repo's pinned pnpm + # requires Node >= 22.13 and refuses to start on the 22.12 floor this job exists to cover. The + # gate itself only needs `node` and `npm`, so it is the package.json script minus the launcher. - name: Verify the published package on Node.js 22.12 - run: pnpm check:package + run: node --experimental-strip-types scripts/check-package.ts fallow: name: Fallow Code Quality diff --git a/scripts/check-package.ts b/scripts/check-package.ts index 1c39d2eae7..eff2cd0d94 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -48,9 +48,18 @@ function run(command: string, args: string[], cwd: string): string { }); } -function localBin(name: string): string { - return path.join(repoRoot, 'node_modules', '.bin', name); -} +/** + * The two established packaging linters, run against the tarball rather than the working tree. + * Invoked through their `node_modules/.bin` shims and not `pnpm exec`, because this gate also runs on + * the minimum supported Node, which the repo's pinned pnpm refuses to start on. + */ +const TARBALL_LINTERS = [ + { dependency: 'publint', bin: 'publint', args: ['--strict'] }, + // The `esm-only` profile is the honest one for this package (`type: module`, + // `engines.node >= 22.12`): it keeps every ESM and bundler resolution failing, and drops the + // CJS-consumer and pre-`exports` node10 rules that no supported consumer can hit. + { dependency: '@arethetypeswrong/cli', bin: 'attw', args: ['--profile', 'esm-only'] }, +] as const; function step(message: string): void { process.stdout.write(`${message}\n`); @@ -68,11 +77,14 @@ function packTarball(): string { } function lintTarball(tarball: string): void { - run(localBin('publint'), ['--strict', tarball], repoRoot); - // This package is ESM-only (`type: module`, `engines.node >= 22.12`), so the `esm-only` profile is - // the honest one: it keeps every ESM and bundler resolution failing, and drops the CJS-consumer and - // pre-`exports` node10 rules that no supported consumer can hit. - run(localBin('attw'), ['--profile', 'esm-only', tarball], repoRoot); + for (const linter of TARBALL_LINTERS) { + run( + path.join(repoRoot, 'node_modules', '.bin', linter.bin), + [...linter.args, tarball], + repoRoot, + ); + } + step(`Linted the tarball with ${TARBALL_LINTERS.map((linter) => linter.bin).join(' and ')}.`); } /** @@ -112,6 +124,28 @@ function moduleSpecifiers(file: string, source: string): string[] { ]; } +function isExternalPackage(specifier: string): boolean { + return !specifier.startsWith('.') && !specifier.startsWith('/') && !builtins.has(specifier); +} + +/** Maps each package the shipped files import to the files importing it. */ +function shippedImports(installedRoot: string): Map { + const importedBy = new Map(); + for (const file of walkFiles(installedRoot, (file) => /\.(?:m?js|d\.ts)$/.test(file))) { + const relative = path.relative(installedRoot, file); + const specifiers = moduleSpecifiers(file, fs.readFileSync(file, 'utf8')); + for (const specifier of specifiers.filter(isExternalPackage)) { + const name = packageNameOf(specifier); + importedBy.set(name, [...(importedBy.get(name) ?? []), `${specifier} in ${relative}`]); + } + } + return importedBy; +} + +function mismatch(heading: string, entries: readonly string[], fix: string): string[] { + return entries.length === 0 ? [] : [heading, ...entries.map((entry) => ` - ${entry}`), fix]; +} + /** * Both directions matter. An import the manifest does not declare breaks the install; a declared * dependency nothing imports is an install every user pays for and no code reaches — how `pngjs` @@ -119,40 +153,23 @@ function moduleSpecifiers(file: string, source: string): string[] { */ function auditDependencyClosure(installedRoot: string, manifest: PackedManifest): void { const declared = new Set(Object.keys(manifest.dependencies ?? {})); - const importedBy = new Map(); - const undeclared: string[] = []; - - for (const file of walkFiles(installedRoot, (file) => /\.(?:m?js|d\.ts)$/.test(file))) { - const relative = path.relative(installedRoot, file); - for (const specifier of moduleSpecifiers(file, fs.readFileSync(file, 'utf8'))) { - if (specifier.startsWith('.') || specifier.startsWith('/') || builtins.has(specifier)) - continue; - const name = packageNameOf(specifier); - if (!declared.has(name)) undeclared.push(`${specifier} in ${relative}`); - importedBy.set(name, [...(importedBy.get(name) ?? []), relative]); - } - } + const importedBy = shippedImports(installedRoot); + const problems = [ + ...mismatch( + 'Imported but not declared in "dependencies" (a published install cannot resolve these):', + [...importedBy].filter(([name]) => !declared.has(name)).flatMap(([, sites]) => sites), + 'Declare the package, or add it to `deps.alwaysBundle` in tsdown.config.ts.', + ), + ...mismatch( + 'Declared in "dependencies" but never imported (every user installs these for nothing):', + [...declared].filter((name) => !importedBy.has(name)), + 'Remove the dependency, or stop bundling it in tsdown.config.ts.', + ), + ]; - const unused = [...declared].filter((name) => !importedBy.has(name)); - if (undeclared.length > 0 || unused.length > 0) { + if (problems.length > 0) { throw new Error( - [ - 'The published dependency closure does not match what the package imports.', - ...(undeclared.length > 0 - ? [ - 'Imported but not declared in "dependencies" (a published install cannot resolve these):', - ...undeclared.map((entry) => ` - ${entry}`), - 'Declare the package, or add it to `deps.alwaysBundle` in tsdown.config.ts.', - ] - : []), - ...(unused.length > 0 - ? [ - 'Declared in "dependencies" but never imported (every user installs these for nothing):', - ...unused.map((name) => ` - ${name}`), - 'Remove the dependency, or stop bundling it in tsdown.config.ts.', - ] - : []), - ].join('\n'), + `The published dependency closure does not match what the package imports.\n${problems.join('\n')}`, ); } step(`Verified the dependency closure: ${[...importedBy.keys()].sort().join(', ')}.`); diff --git a/src/__tests__/npm-package-scripts.test.ts b/src/__tests__/npm-package-scripts.test.ts index 51eecb600c..a12c1ad192 100644 --- a/src/__tests__/npm-package-scripts.test.ts +++ b/src/__tests__/npm-package-scripts.test.ts @@ -85,5 +85,11 @@ test('the npm package build covers every package-owned build output, then verifi test('publishing cannot skip the package gate', () => { assert.match(script('package:npm'), /pnpm check:package$/); assert.match(script('check:tooling'), /pnpm check:package$/); - assert.match(packagedCliWorkflow, /run: pnpm check:package/); + // The minimum-Node job runs the script directly — the repo's pinned pnpm needs a newer Node than + // the floor that job covers — so it must still name the same entry point the script does. + assert.match(script('check:package'), /scripts\/check-package\.ts$/); + assert.match( + packagedCliWorkflow, + /run: node --experimental-strip-types scripts\/check-package\.ts/, + ); }); From e72e1560fb399adfb0d57b0af105f35a8e36e70c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 22:16:09 +0000 Subject: [PATCH 3/3] fix(publishing): read every literal resolution form in the closure audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --- .../__tests__/package-closure-audit.test.ts | 165 +++++++++++ scripts/check-package.ts | 92 +----- scripts/lib/shipped-imports.ts | 261 ++++++++++++++++++ src/__tests__/npm-package-scripts.test.ts | 15 + vitest.config.ts | 4 + 5 files changed, 455 insertions(+), 82 deletions(-) create mode 100644 scripts/__tests__/package-closure-audit.test.ts create mode 100644 scripts/lib/shipped-imports.ts diff --git a/scripts/__tests__/package-closure-audit.test.ts b/scripts/__tests__/package-closure-audit.test.ts new file mode 100644 index 0000000000..ae1da09260 --- /dev/null +++ b/scripts/__tests__/package-closure-audit.test.ts @@ -0,0 +1,165 @@ +/** + * Behavioral tests for the publishing gate's dependency-closure audit, run against fixture packages + * that stand in for packed output. + * + * The gate itself (scripts/check-package.ts) needs a real `npm pack` behind minutes of Swift and + * Android builds, so every check that runs it can only observe a *healthy* package passing. That + * leaves the interesting direction — does a malformed package actually fail? — untested, which is + * how the audit came to read 0 of the 99 dynamic imports in the built bundle while looking covered. + * These fixtures assert the failure direction, one resolution form at a time. + * + * Each fixture is spelled the way the minifier spells it: no-substitution template literals, short + * aliased `createRequire` bindings. That is what the packed files look like, so that is what the + * audit has to be able to read. + */ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, test } from 'vitest'; +import { auditDependencyClosure, type PackedManifest } from '../lib/shipped-imports.ts'; + +const tempRoots: string[] = []; + +afterEach(() => { + for (const root of tempRoots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** Lays out a fake installed package: shipped files plus the `dependencies` the manifest declares. */ +function fixturePackage(files: Record, dependencies: string[] = []): () => void { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-closure-fixture-')); + tempRoots.push(root); + for (const [relative, source] of Object.entries(files)) { + const file = path.join(root, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, source); + } + const manifest: PackedManifest = { + dependencies: Object.fromEntries(dependencies.map((name) => [name, '1.0.0'])), + }; + return () => void auditDependencyClosure(root, manifest); +} + +/** The workspace-private specifier whose published import broke 0.20.4 (#1577). */ +const PRIVATE = '@agent-device/ad-script'; + +/** Every literal spelling of "resolve this specifier at runtime" that a shipped file can use. */ +const LAZY_FORMS: Record = { + 'dynamic import, minified backtick spelling': 'await import(`SPECIFIER`);', + 'dynamic import, quoted spelling': "await import('SPECIFIER');", + 'bare require': 'const mod = require(`SPECIFIER`);', + 'require.resolve': 'const at = require.resolve(`SPECIFIER`);', + 'aliased createRequire result': [ + "import { createRequire as t } from 'node:module';", + 'var a = t(import.meta.url);', + 'const mod = a(`SPECIFIER`);', + ].join('\n'), + 'immediately invoked createRequire': [ + "import { createRequire } from 'node:module';", + 'const mod = createRequire(import.meta.url)(`SPECIFIER`);', + ].join('\n'), + 'createRequire through a namespace import': [ + "import * as M from 'node:module';", + 'var r = M.createRequire(import.meta.url);', + 'const mod = r(`SPECIFIER`);', + ].join('\n'), + 'require alias declared below its use': [ + "import { createRequire as t } from 'node:module';", + 'export function load() { return q(`SPECIFIER`); }', + 'var q = t(import.meta.url);', + ].join('\n'), +}; + +// The reviewable claim of the gate: a shipped file cannot reach a package the manifest does not +// declare. Without every form below, a command could lazily resolve a workspace-private specifier +// and publish green — the 0.20.4 failure class, reintroduced one resolution form at a time. +for (const [form, template] of Object.entries(LAZY_FORMS)) { + test(`an undeclared private specifier fails the closure audit via ${form}`, () => { + const audit = fixturePackage({ 'dist/cli.js': template.replaceAll('SPECIFIER', PRIVATE) }); + assert.throws(audit, (error: Error) => { + assert.match(error.message, /Imported but not declared in "dependencies"/); + assert.match(error.message, /@agent-device\/ad-script in dist\/cli\.js/); + return true; + }); + }); + + test(`a declared dependency satisfies the closure audit via ${form}`, () => { + const audit = fixturePackage({ 'dist/cli.js': template.replaceAll('SPECIFIER', 'yaml') }, [ + 'yaml', + ]); + audit(); + }); +} + +test('a subpath import is attributed to the package that must be declared', () => { + const audit = fixturePackage({ 'dist/cli.js': 'await import(`@limrun/api/client`);' }, [ + '@limrun/api', + ]); + audit(); +}); + +test('the audit reads every shipped extension, not only the bundled .js files', () => { + for (const file of ['bin/agent-device.mjs', 'dist/legacy.cjs', 'dist/index.d.ts']) { + const audit = fixturePackage({ [file]: `const mod = require(\`${PRIVATE}\`);` }); + assert.throws( + audit, + new RegExp(`${PRIVATE.replace('/', '\\/')} in ${file.replace('/', '\\/')}`), + ); + } +}); + +test('builtins and relative specifiers are not dependencies', () => { + const audit = fixturePackage({ + 'dist/cli.js': [ + "import fs from 'node:fs';", + "import { createRequire as t } from 'node:module';", + 'var a = t(import.meta.url);', + 'const z = a(`zlib`);', + 'const u = a(`util`);', + 'await import(`./sibling.js`);', + 'await import(`../parent.js`);', + ].join('\n'), + 'dist/sibling.js': 'export const x = 1;', + }); + audit(); +}); + +// The other direction of the same audit: a dependency every user installs and no shipped file +// reaches. How `pngjs` stayed in `dependencies` after tsdown started inlining it. +test('a declared dependency nothing imports fails the closure audit', () => { + const audit = fixturePackage({ 'dist/cli.js': 'export const x = 1;' }, ['pngjs']); + assert.throws(audit, /Declared in "dependencies" but never imported[\s\S]*- pngjs/); +}); + +// Pinned limitations, so the gate's guarantee stays honest about where it stops. Both are covered +// by the runtime half instead: the gate imports every export and runs the CLI paths that load the +// lazy bundles, which resolves computed specifiers for real. +test('a computed specifier is out of scope for the static audit', () => { + const audit = fixturePackage({ + 'dist/cli.js': [ + "import { createRequire as t } from 'node:module';", + 'var a = t(import.meta.url);', + 'export const load = (name) => a(name);', + 'export const scoped = (v) => require(`@agent-device/ad-` + v);', + 'export const lazy = (name) => import(name);', + ].join('\n'), + }); + audit(); +}); + +test('a minified name collision is not mistaken for a require call', () => { + // The packed png-worker-contract.js really does contain `a(h[t],f,g,l,e,m)` in a scope where `a` + // is not the file's `createRequire` result. Attributing that to a dependency would fail the gate + // on a sound package, so only the one-string-argument shape counts. + const audit = fixturePackage({ + 'dist/cli.js': [ + "import { createRequire as t } from 'node:module';", + 'var a = t(import.meta.url);', + 'export function draw(h, f, g, l, e, m) {', + ' const a = (...parts) => parts.length;', + ' return a(h[0], f, g, l, e, m);', + '}', + ].join('\n'), + }); + audit(); +}); diff --git a/scripts/check-package.ts b/scripts/check-package.ts index eff2cd0d94..19b6f6f98c 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -14,19 +14,20 @@ * * Step 3 is the static half and step 4 the runtime half of the same question: nothing the package * imports may depend on workspace linking. Keep both — a specifier reachable only through a lazy - * dynamic import stays invisible to step 4, and a `require` computed at runtime stays invisible to - * step 3. + * dynamic import stays invisible to step 4, and a specifier computed at runtime stays invisible to + * step 3. Step 3 lives in scripts/lib/shipped-imports.ts, where fixture tests can prove it rejects + * a malformed package; everything here needs a real pack and can only exercise the healthy path. */ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; -import { builtinModules } from 'node:module'; import os from 'node:os'; import path from 'node:path'; -import { parseSync } from 'oxc-parser'; -import { walkFiles } from './lib/walk-files.ts'; +import { + auditDependencyClosure, + type PackedManifest as PackedDependencies, +} from './lib/shipped-imports.ts'; -type PackedManifest = { - dependencies?: Record; +type PackedManifest = PackedDependencies & { exports: Record; bin: Record; }; @@ -36,7 +37,6 @@ const repoRoot = path.resolve(import.meta.dirname, '..'); // `/private/var`; resolving up front keeps the paths this script prints equal to the ones npm uses. const workDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'agent-device-package-')); const consumerDir = path.join(workDir, 'consumer'); -const builtins = new Set(builtinModules.flatMap((name) => [name, `node:${name}`])); /** Stdout is captured for the callers that parse it; stderr passes through so failures are readable. */ function run(command: string, args: string[], cwd: string): string { @@ -102,79 +102,6 @@ function installIntoCleanConsumer(tarball: string): string { return path.join(consumerDir, 'node_modules', 'agent-device'); } -function packageNameOf(specifier: string): string { - const segments = specifier.split('/'); - return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]!; -} - -function moduleSpecifiers(file: string, source: string): string[] { - const record = parseSync(file, source).module; - return [ - ...record.staticImports.map((entry) => entry.moduleRequest.value), - ...record.staticExports.flatMap((entry) => - entry.entries.flatMap((exported) => - exported.moduleRequest?.value ? [exported.moduleRequest.value] : [], - ), - ), - ...record.dynamicImports.flatMap((entry) => { - const raw = source.slice(entry.moduleRequest.start, entry.moduleRequest.end); - const literal = /^(['"])([^'"]*)\1$/.exec(raw); - return literal?.[2] ? [literal[2]] : []; - }), - ]; -} - -function isExternalPackage(specifier: string): boolean { - return !specifier.startsWith('.') && !specifier.startsWith('/') && !builtins.has(specifier); -} - -/** Maps each package the shipped files import to the files importing it. */ -function shippedImports(installedRoot: string): Map { - const importedBy = new Map(); - for (const file of walkFiles(installedRoot, (file) => /\.(?:m?js|d\.ts)$/.test(file))) { - const relative = path.relative(installedRoot, file); - const specifiers = moduleSpecifiers(file, fs.readFileSync(file, 'utf8')); - for (const specifier of specifiers.filter(isExternalPackage)) { - const name = packageNameOf(specifier); - importedBy.set(name, [...(importedBy.get(name) ?? []), `${specifier} in ${relative}`]); - } - } - return importedBy; -} - -function mismatch(heading: string, entries: readonly string[], fix: string): string[] { - return entries.length === 0 ? [] : [heading, ...entries.map((entry) => ` - ${entry}`), fix]; -} - -/** - * Both directions matter. An import the manifest does not declare breaks the install; a declared - * dependency nothing imports is an install every user pays for and no code reaches — how `pngjs` - * stayed in `dependencies` after `tsdown.config.ts` started inlining it. - */ -function auditDependencyClosure(installedRoot: string, manifest: PackedManifest): void { - const declared = new Set(Object.keys(manifest.dependencies ?? {})); - const importedBy = shippedImports(installedRoot); - const problems = [ - ...mismatch( - 'Imported but not declared in "dependencies" (a published install cannot resolve these):', - [...importedBy].filter(([name]) => !declared.has(name)).flatMap(([, sites]) => sites), - 'Declare the package, or add it to `deps.alwaysBundle` in tsdown.config.ts.', - ), - ...mismatch( - 'Declared in "dependencies" but never imported (every user installs these for nothing):', - [...declared].filter((name) => !importedBy.has(name)), - 'Remove the dependency, or stop bundling it in tsdown.config.ts.', - ), - ]; - - if (problems.length > 0) { - throw new Error( - `The published dependency closure does not match what the package imports.\n${problems.join('\n')}`, - ); - } - step(`Verified the dependency closure: ${[...importedBy.keys()].sort().join(', ')}.`); -} - /** Imports every documented entry point in one process so a failure names the subpath that broke. */ function importEveryExport(manifest: PackedManifest): void { const specifiers = Object.keys(manifest.exports).map((subpath) => @@ -235,7 +162,8 @@ try { const manifest = JSON.parse( fs.readFileSync(path.join(installedRoot, 'package.json'), 'utf8'), ) as PackedManifest; - auditDependencyClosure(installedRoot, manifest); + const importedBy = auditDependencyClosure(installedRoot, manifest); + step(`Verified the dependency closure: ${[...importedBy.keys()].sort().join(', ')}.`); importEveryExport(manifest); smokeTestBin(installedRoot, manifest); } catch (error) { diff --git a/scripts/lib/shipped-imports.ts b/scripts/lib/shipped-imports.ts new file mode 100644 index 0000000000..c48bb19922 --- /dev/null +++ b/scripts/lib/shipped-imports.ts @@ -0,0 +1,261 @@ +/** + * The static half of the publishing gate (scripts/check-package.ts step 3): which packages the + * files inside the packed tarball actually resolve at runtime, and whether `dependencies` matches. + * + * Extracted from the gate so this half can be exercised against fixture output. The gate itself + * only runs against a real `npm pack` — minutes of Swift and Android builds — so nothing there can + * prove that a malformed package *fails*; every test against it can only confirm the healthy path. + * + * Two properties of the shipped files decide what a reader here has to understand. Both were + * measured against the built `dist/`, not assumed: + * + * - **The shipped files are minified, and the minifier rewrites every string literal to a + * no-substitution template literal.** `` import(`./cli-help.js`) `` is the normal spelling in + * the bundle; `import('./cli-help.js')` never appears. A reader that accepts only quoted strings + * sees 0 of the 99 dynamic imports the bundle contains — so the lazy-import half of the closure + * audit reads as covered while checking nothing, on exactly the lazy `import()` path that + * published 0.20.4 broke on (#1577). + * + * - **Bundled dependencies resolve through `createRequire`, under minified aliases.** The packed + * `png-worker-contract.js` carries `` var a = t(import.meta.url); a(`zlib`) `` from inlined + * `pngjs`. A `require` call produces no entry in an ESM module record — not a static import, not + * a dynamic import — so a reader built on the record alone cannot see a required package at all, + * and a lazy `createRequire('@agent-device/…')` would slip past this audit the same way. + * + * So specifiers come from the module record (static `import`/`export`) *and* from an AST walk over + * every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, and calls + * through a `createRequire` result. Both spellings of a string literal count everywhere. + * + * Deliberately **not** covered: a specifier computed at runtime — `a(name)`, `require('pkg/' + x)`. + * Resolving those needs constant propagation this gate does not attempt, and rejecting them + * outright is not available either: minifiers reuse short identifiers across scopes, so the packed + * bundle contains unrelated calls like `a(h[t],f,g,l,e,m)` that no name-based match can tell from a + * real `require`. Computed resolution is covered by the runtime half of the gate instead — step 4 + * imports every export and runs the CLI paths that load the lazy bundles. Keep both halves. + */ +import fs from 'node:fs'; +import { builtinModules } from 'node:module'; +import path from 'node:path'; +import { + type CallExpression, + type Expression, + type Node, + parseSync, + type StringLiteral, + type TemplateLiteral, + Visitor, +} from 'oxc-parser'; +import { walkFiles } from './walk-files.ts'; + +export type PackedManifest = { + dependencies?: Record; +}; + +/** Every file extension the package ships that Node can resolve a specifier from. */ +const SHIPPED_SOURCE = /\.(?:m?js|cjs|d\.ts)$/; + +/** The module both spellings of `createRequire` are imported from. */ +const MODULE_BUILTIN = new Set(['module', 'node:module']); + +const builtins = new Set(builtinModules.flatMap((name) => [name, `node:${name}`])); + +/** + * `'pkg'` and `` `pkg` `` are the same specifier. The second is the only spelling the minified + * shipped files use, so treating it as non-literal blinds this reader to the whole bundle. + */ +function literalSpecifier(node: Node | Expression | null | undefined): string | undefined { + if (!node) return undefined; + if (node.type === 'Literal') { + const { value } = node as StringLiteral; + return typeof value === 'string' ? value : undefined; + } + if (node.type === 'TemplateLiteral') { + const template = node as TemplateLiteral; + if (template.expressions.length > 0 || template.quasis.length !== 1) return undefined; + return template.quasis[0]?.value.cooked ?? undefined; + } + return undefined; +} + +/** The local names `createRequire` is reachable through in one file, under any import alias. */ +type CreateRequireBindings = { + /** `import { createRequire } from 'node:module'` — including `as` aliases the minifier invents. */ + locals: Set; + /** `import * as mod from 'node:module'`, reached as `mod.createRequire`. */ + namespaces: Set; +}; + +function isCreateRequire(callee: Expression, bindings: CreateRequireBindings): boolean { + if (callee.type === 'Identifier') return bindings.locals.has(callee.name); + return ( + callee.type === 'MemberExpression' && + !callee.computed && + callee.object.type === 'Identifier' && + bindings.namespaces.has(callee.object.name) && + callee.property.type === 'Identifier' && + callee.property.name === 'createRequire' + ); +} + +/** A resolution call found in one pass, pending the alias set the same pass is still collecting. */ +type PendingCall = + | { form: 'callee'; name: string; specifier: string; single: boolean } + | { form: 'resolve'; name: string; specifier: string } + | { form: 'resolved'; specifier: string }; + +/** The first argument, when it is a literal specifier rather than a computed expression. */ +function firstArgumentSpecifier(call: CallExpression): string | undefined { + const [first] = call.arguments; + if (!first || first.type === 'SpreadElement') return undefined; + return literalSpecifier(first); +} + +/** `.resolve` — the object name, for the `require.resolve('pkg')` spelling. */ +function resolveTarget(callee: Expression): string | undefined { + if (callee.type !== 'MemberExpression' || callee.computed) return undefined; + if (callee.object.type !== 'Identifier') return undefined; + if (callee.property.type !== 'Identifier' || callee.property.name !== 'resolve') return undefined; + return callee.object.name; +} + +function pendingCall( + call: CallExpression, + bindings: CreateRequireBindings, +): PendingCall | undefined { + const specifier = firstArgumentSpecifier(call); + if (specifier === undefined) return undefined; + const { callee } = call; + // `createRequire(import.meta.url)('pkg')` — the alias is inline, so nothing needs resolving. + if (callee.type === 'CallExpression' && isCreateRequire(callee.callee, bindings)) { + return { form: 'resolved', specifier }; + } + if (callee.type === 'Identifier') { + return { form: 'callee', name: callee.name, specifier, single: call.arguments.length === 1 }; + } + // `require.resolve('pkg')`, which also takes an options bag as a second argument. + const target = resolveTarget(callee); + return target === undefined ? undefined : { form: 'resolve', name: target, specifier }; +} + +/** + * Every literal specifier one shipped file resolves, from the module record and from the runtime + * resolution forms the record cannot represent. + */ +function moduleSpecifiers(file: string, source: string): string[] { + const parsed = parseSync(file, source); + const record = parsed.module; + + const bindings: CreateRequireBindings = { locals: new Set(), namespaces: new Set() }; + for (const staticImport of record.staticImports) { + if (!MODULE_BUILTIN.has(staticImport.moduleRequest.value)) continue; + for (const entry of staticImport.entries) { + if (entry.importName.kind === 'Name' && entry.importName.name === 'createRequire') { + bindings.locals.add(entry.localName.value); + } + if (entry.importName.kind === 'NamespaceObject') { + bindings.namespaces.add(entry.localName.value); + } + } + } + + // A single pass: `require` aliases and the calls through them are collected together and matched + // afterwards, so a call does not have to appear below the declaration that names it. + const requireAliases = new Set(['require']); + const pending: PendingCall[] = []; + const dynamic: string[] = []; + new Visitor({ + VariableDeclarator(node) { + if (node.id.type !== 'Identifier') return; + if (node.init?.type !== 'CallExpression') return; + if (isCreateRequire(node.init.callee, bindings)) requireAliases.add(node.id.name); + }, + ImportExpression(node) { + const specifier = literalSpecifier(node.source); + if (specifier !== undefined) dynamic.push(specifier); + }, + CallExpression(node) { + const call = pendingCall(node, bindings); + if (call) pending.push(call); + }, + }).visit(parsed.program); + + const required = pending.flatMap((call) => { + if (call.form === 'resolved') return [call.specifier]; + if (!requireAliases.has(call.name)) return []; + // A bare `name('pkg')` only reads as a require call in the one-string-argument shape. Minified + // scopes reuse short names, and the packed bundle really does contain a six-argument `a(…)` + // whose `a` is not the file's `createRequire` result. + if (call.form === 'callee' && !call.single) return []; + return [call.specifier]; + }); + + return [ + ...record.staticImports.map((entry) => entry.moduleRequest.value), + ...record.staticExports.flatMap((entry) => + entry.entries.flatMap((exported) => + exported.moduleRequest?.value ? [exported.moduleRequest.value] : [], + ), + ), + ...dynamic, + ...required, + ]; +} + +function packageNameOf(specifier: string): string { + const segments = specifier.split('/'); + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]!; +} + +function isExternalPackage(specifier: string): boolean { + return !specifier.startsWith('.') && !specifier.startsWith('/') && !builtins.has(specifier); +} + +/** Maps each package the shipped files import to the files importing it. */ +function shippedImports(installedRoot: string): Map { + const importedBy = new Map(); + for (const file of walkFiles(installedRoot, (file) => SHIPPED_SOURCE.test(file))) { + const relative = path.relative(installedRoot, file); + const specifiers = moduleSpecifiers(file, fs.readFileSync(file, 'utf8')); + for (const specifier of specifiers.filter(isExternalPackage)) { + const name = packageNameOf(specifier); + importedBy.set(name, [...(importedBy.get(name) ?? []), `${specifier} in ${relative}`]); + } + } + return importedBy; +} + +function mismatch(heading: string, entries: readonly string[], fix: string): string[] { + return entries.length === 0 ? [] : [heading, ...entries.map((entry) => ` - ${entry}`), fix]; +} + +/** + * Both directions matter. An import the manifest does not declare breaks the install; a declared + * dependency nothing imports is an install every user pays for and no code reaches — how `pngjs` + * stayed in `dependencies` after `tsdown.config.ts` started inlining it. + */ +export function auditDependencyClosure( + installedRoot: string, + manifest: PackedManifest, +): Map { + const declared = new Set(Object.keys(manifest.dependencies ?? {})); + const importedBy = shippedImports(installedRoot); + const problems = [ + ...mismatch( + 'Imported but not declared in "dependencies" (a published install cannot resolve these):', + [...importedBy].filter(([name]) => !declared.has(name)).flatMap(([, sites]) => sites), + 'Declare the package, or add it to `deps.alwaysBundle` in tsdown.config.ts.', + ), + ...mismatch( + 'Declared in "dependencies" but never imported (every user installs these for nothing):', + [...declared].filter((name) => !importedBy.has(name)), + 'Remove the dependency, or stop bundling it in tsdown.config.ts.', + ), + ]; + + if (problems.length > 0) { + throw new Error( + `The published dependency closure does not match what the package imports.\n${problems.join('\n')}`, + ); + } + return importedBy; +} diff --git a/src/__tests__/npm-package-scripts.test.ts b/src/__tests__/npm-package-scripts.test.ts index a12c1ad192..6843aabf8d 100644 --- a/src/__tests__/npm-package-scripts.test.ts +++ b/src/__tests__/npm-package-scripts.test.ts @@ -80,6 +80,21 @@ test('the npm package build covers every package-owned build output, then verifi ]); }); +// Behavior of the closure audit is pinned by fixtures in +// scripts/__tests__/package-closure-audit.test.ts, which can fail a malformed package the way no +// test of the real gate can. That leaves the wiring: the audit and both runtime probes have to stay +// wired into the gate, or the fixtures would keep passing while the tarball went unchecked. +test('the package gate runs the closure audit and both runtime probes', () => { + const gate = fs.readFileSync(path.join(repoRoot, 'scripts', 'check-package.ts'), 'utf8'); + for (const call of [ + 'auditDependencyClosure(installedRoot, manifest)', + 'importEveryExport(manifest)', + 'smokeTestBin(installedRoot, manifest)', + ]) { + assert.ok(gate.includes(call), `scripts/check-package.ts must still call ${call}`); + } +}); + // The gate reads the packed tarball, so `prepack` is the last point where a broken package can still // be stopped. Publishing runs it; nothing else guarantees the tarball is ever verified. test('publishing cannot skip the package gate', () => { diff --git a/vitest.config.ts b/vitest.config.ts index 851d80722f..3945c806ff 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -58,6 +58,10 @@ export default defineConfig({ 'scripts/__tests__/help-conformance-error-recovery-coverage.test.ts', 'scripts/__tests__/help-conformance-sample-outputs.test.ts', 'scripts/__tests__/help-conformance-topic-coverage.test.ts', + // The publishing gate's closure audit against fixture packages: parse-only, and the + // only place the gate's failure direction is exercised at all (the gate itself needs a + // real `npm pack`, so CI can only watch a healthy package pass). + 'scripts/__tests__/package-closure-audit.test.ts', // Parses CI configuration only, so this action guard needs no device or subprocess lane. 'test/ci/upload-agent-device-artifacts.test.ts', // The frozen replay-compat corpus (#1417): parse-only, no device or