From e665b86d93bf1c93f5925caf73abae1b90609980 Mon Sep 17 00:00:00 2001 From: MCMXC <16797721+mcmxcdev@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:33:22 -0600 Subject: [PATCH] feat: cache registry lookups, add logging, adopt changesets Add an on-disk TTL cache for npm registry lookups so repeat audit runs avoid redundant network requests, and a logging module for diagnostic output. Registry/CLI/check wired to use them. Adopt Changesets for versioning and publishing: - @changesets/cli + .changeset config (access: public) - scripts: changeset, version-packages, release - Release workflow runs changesets/action on push to main, opening a "Version Packages" PR and publishing to npm on merge - Replaces the old release-triggered publish workflow Includes a changeset queuing the cache+logging feature as a minor bump. Co-Authored-By: Claude Opus 4.8 --- .changeset/config.json | 11 + .changeset/early-cache-and-logging.md | 5 + .github/workflows/publish.yml | 44 -- .github/workflows/release.yml | 44 ++ README.md | 28 + package.json | 6 +- pnpm-lock.yaml | 775 ++++++++++++++++++++++++++ src/audit.ts | 5 + src/cache.ts | 97 ++++ src/cli.ts | 5 + src/commands/check.ts | 8 + src/log.ts | 30 + src/registry.ts | 33 +- test/cache.test.ts | 87 +++ test/config.test.ts | 1 - test/init.test.ts | 1 - test/log.test.ts | 66 +++ test/registry.test.ts | 51 +- 18 files changed, 1247 insertions(+), 50 deletions(-) create mode 100644 .changeset/config.json create mode 100644 .changeset/early-cache-and-logging.md delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 src/cache.ts create mode 100644 src/log.ts create mode 100644 test/cache.test.ts create mode 100644 test/log.test.ts diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..ec98e35 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/early-cache-and-logging.md b/.changeset/early-cache-and-logging.md new file mode 100644 index 0000000..1f376af --- /dev/null +++ b/.changeset/early-cache-and-logging.md @@ -0,0 +1,5 @@ +--- +"check-my-engine": minor +--- + +Cache npm registry lookups on disk with a configurable TTL to speed up repeat runs, and add a logging module for diagnostic output. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 9966e1f..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write # required for npm provenance / OIDC Trusted Publishing - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Install pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: ".nvmrc" - - - name: Install dependencies - run: pnpm install - - - name: Lint - run: pnpm lint - - - name: Typecheck - run: pnpm typecheck - - - name: Test - run: pnpm test - - # prepublishOnly rebuilds dist; provenance + access come from publishConfig. - # No NPM_TOKEN needed when the package uses npm Trusted Publishing. - - name: Publish to npm - run: npm publish - env: - NPM_CONFIG_PROVENANCE: "true" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5f79444 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + branches: [main] + +# Cancel in-progress runs on the same branch. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write # create the "Version Packages" PR and git tags + pull-requests: write # open/update the release PR + id-token: write # npm provenance / OIDC Trusted Publishing + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Create release PR or publish to npm + uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 + with: + version: pnpm run version-packages + publish: pnpm run release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: "true" diff --git a/README.md b/README.md index 09176ec..346d58f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,34 @@ Example output: | `--no-suggest` | Skip registry lookups for in-range versions (offline). | | `--allow-prerelease` | Consider prerelease versions when suggesting. | | `--fallback-to-node-modules` | Read engines from `node_modules` when the lockfile omits them (npm only; no-op for pnpm). | +| `--no-cache` | Skip the on-disk registry cache (always hit the network). | + +#### Registry cache + +Suggestion lookups hit the npm registry once per package name. Responses are +cached on disk so repeated runs — `postinstall`, CI, watch loops — stay fast and +avoid registry rate limits. The cache is best-effort: a corrupt or unwritable +cache degrades to a normal network fetch, never an error. + +| Setting | Default | Meaning | +| --------------------------- | --------------------------------------------------------------- | ---------------------------------------- | +| `CHECK_MY_ENGINE_CACHE_DIR` | `$XDG_CACHE_HOME/check-my-engine` or `~/.cache/check-my-engine` | Cache directory. | +| `CHECK_MY_ENGINE_CACHE_TTL` | `3600` (seconds) | How long a cached packument stays fresh. | +| `--no-cache` | cache on | Bypass the cache for one run. | + +#### Debug logging + +For development, set `NODE_DEBUG=check-my-engine` to trace registry traffic on +stderr — each network fetch, registry hit, and cache hit. It is silent unless +the namespace is enabled, so end-user runs (including `postinstall`) print +nothing, and the trace never touches stdout (safe alongside `--json`). + +```shell +$ NODE_DEBUG=check-my-engine npx check-my-engine +check-my-engine registry fetch: https://registry.npmjs.org/needs-22 +check-my-engine registry hit: needs-22 (200, 12 versions) +check-my-engine cache hit: needs-22 +``` ## Enforce on install diff --git a/package.json b/package.json index ae4d5fc..7d24188 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,10 @@ "test:watch": "vitest", "typecheck": "tsc --noEmit", "prepublishOnly": "pnpm build", - "check": "node dist/cli.js check" + "check": "node dist/cli.js check", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "pnpm build && changeset publish" }, "dependencies": { "commander": "^15.0.0", @@ -60,6 +63,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.1", + "@changesets/cli": "^2.31.0", "@types/node": "^24.13.2", "@types/semver": "^7.7.1", "prettier": "^3.9.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cec80f7..987597c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@biomejs/biome': specifier: ^2.5.1 version: 2.5.1 + '@changesets/cli': + specifier: ^2.31.0 + version: 2.31.0(@types/node@24.13.2) '@types/node': specifier: ^24.13.2 version: 24.13.2 @@ -45,6 +48,10 @@ importers: packages: + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@2.5.1': resolution: {integrity: sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==} engines: {node: '>=14.21.3'} @@ -102,6 +109,61 @@ packages: cpu: [x64] os: [win32] + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -267,6 +329,15 @@ packages: cpu: [x64] os: [win32] + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -280,12 +351,30 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} @@ -540,6 +629,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} @@ -580,13 +672,39 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -601,6 +719,9 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -623,6 +744,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -632,10 +757,22 @@ packages: supports-color: optional: true + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + es-module-lexer@2.2.0: resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} @@ -644,6 +781,11 @@ packages: engines: {node: '>=18'} hasBin: true + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -651,6 +793,16 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -660,18 +812,91 @@ packages: picomatch: optional: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + hasBin: true + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -757,12 +982,31 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + 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==} @@ -782,16 +1026,62 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -821,11 +1111,26 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.9.4: resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} engines: {node: '>=14'} hasBin: true + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -834,6 +1139,10 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.1.3: resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -844,14 +1153,36 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -860,17 +1191,35 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -896,6 +1245,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -936,6 +1289,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + vite@8.1.0: resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1020,6 +1377,11 @@ packages: jsdom: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1032,6 +1394,8 @@ packages: snapshots: + '@babel/runtime@7.29.7': {} + '@biomejs/biome@2.5.1': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.5.1 @@ -1067,6 +1431,149 @@ snapshots: '@biomejs/cli-win32-x64@2.5.1': optional: true + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.5 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.0(@types/node@24.13.2)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@24.13.2) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.3.0 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.0 + prettier: 2.8.8 + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -1161,6 +1668,13 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@inquirer/external-editor@1.0.3(@types/node@24.13.2)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.13.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1175,6 +1689,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -1182,6 +1712,18 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@oxc-project/types@0.137.0': {} '@rolldown/binding-android-arm64@1.1.3': @@ -1326,6 +1868,8 @@ snapshots: '@types/estree@1.0.9': {} + '@types/node@12.20.55': {} + '@types/node@24.13.2': dependencies: undici-types: 7.18.2 @@ -1375,10 +1919,30 @@ snapshots: acorn@8.17.0: {} + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + any-promise@1.3.0: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + assertion-error@2.0.1: {} + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -1388,6 +1952,8 @@ snapshots: chai@6.2.2: {} + chardet@2.2.0: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -1402,12 +1968,29 @@ snapshots: convert-source-map@2.0.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.4.3: dependencies: ms: 2.1.3 + detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + es-module-lexer@2.2.0: {} esbuild@0.27.7: @@ -1439,27 +2022,116 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esprima@4.0.1: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 expect-type@1.4.0: {} + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.2 rollup: 4.62.2 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.3: optional: true + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + human-id@4.2.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + joycon@3.1.1: {} + js-yaml@3.15.0: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + lightningcss-android-arm64@1.32.0: optional: true @@ -1515,10 +2187,23 @@ snapshots: load-tsconfig@0.2.5: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mlly@1.8.2: dependencies: acorn: 8.17.0 @@ -1526,6 +2211,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + mri@1.2.0: {} + ms@2.1.3: {} mz@2.7.0: @@ -1540,12 +2227,44 @@ snapshots: obug@2.1.3: {} + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} + pify@4.0.1: {} + pirates@4.0.7: {} pkg-types@1.3.1: @@ -1567,12 +2286,27 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@2.8.8: {} + prettier@3.9.4: {} + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.15.0 + pify: 4.0.1 + strip-bom: 3.0.0 + readdirp@4.1.2: {} resolve-from@5.0.0: {} + reusify@1.1.0: {} + rolldown@1.1.3: dependencies: '@oxc-project/types': 0.137.0 @@ -1625,18 +2359,47 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + semver@7.8.5: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + + slash@3.0.0: {} + source-map-js@1.2.1: {} source-map@0.7.6: {} + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + stackback@0.0.2: {} std-env@4.1.0: {} + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -1647,6 +2410,8 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 + term-size@2.2.1: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -1668,6 +2433,10 @@ snapshots: tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -1709,6 +2478,8 @@ snapshots: undici-types@7.18.2: {} + universalify@0.1.2: {} + vite@8.1.0(@types/node@24.13.2)(esbuild@0.27.7)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -1749,6 +2520,10 @@ snapshots: transitivePeerDependencies: - msw + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/src/audit.ts b/src/audit.ts index 92636de..3a3f383 100644 --- a/src/audit.ts +++ b/src/audit.ts @@ -1,3 +1,4 @@ +import type { JsonCache } from "./cache.js"; import { classifyCoverage } from "./compat.js"; import { loadGraph } from "./lockfile.js"; import { findLatestInRange } from "./registry.js"; @@ -11,6 +12,8 @@ export interface AuditOptions { allowPrerelease?: boolean; registry?: string; fallbackToNodeModules?: boolean; + /** On-disk registry cache reused across suggestion lookups. */ + cache?: JsonCache; } function isIgnored(node: DependencyNode, ignore: string[]): boolean { @@ -66,6 +69,7 @@ export async function audit( allowPrerelease = false, registry, fallbackToNodeModules = false, + cache, } = options; const graph = await loadGraph(cwd, { fallbackToNodeModules }); @@ -116,6 +120,7 @@ export async function audit( const version = await findLatestInRange(name, projectRange, { allowPrerelease, registry, + cache, }); return { name, version, failed: false }; } catch { diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..dac020c --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,97 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour + +export interface CacheConfig { + /** Directory the cache files live in. */ + dir: string; + /** Entries older than this (ms) are treated as a miss. */ + ttlMs: number; +} + +export interface JsonCache { + get(key: string): Promise; + set(key: string, value: T): Promise; +} + +interface Envelope { + fetchedAt: number; + value: T; +} + +/** + * Default cache directory. Honors an explicit `CHECK_MY_ENGINE_CACHE_DIR`, + * then `XDG_CACHE_HOME`, falling back to `~/.cache/check-my-engine`. + */ +export function defaultCacheDir(): string { + const override = process.env.CHECK_MY_ENGINE_CACHE_DIR?.trim(); + if (override) return override; + const xdg = process.env.XDG_CACHE_HOME?.trim(); + if (xdg) return path.join(xdg, "check-my-engine"); + return path.join(os.homedir(), ".cache", "check-my-engine"); +} + +/** + * Cache TTL in milliseconds. `CHECK_MY_ENGINE_CACHE_TTL` is read as a number of + * seconds; anything non-numeric or negative falls back to the default. + */ +export function resolveCacheTtlMs(): number { + const raw = process.env.CHECK_MY_ENGINE_CACHE_TTL?.trim(); + if (raw) { + const seconds = Number(raw); + if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000; + } + return DEFAULT_TTL_MS; +} + +function fileFor(dir: string, key: string): string { + const hash = createHash("sha256").update(key).digest("hex"); + return path.join(dir, `${hash}.json`); +} + +/** + * Create a best-effort on-disk JSON cache. Read and write errors never throw: + * a corrupt or missing file is simply a cache miss, and a failed write is + * silently dropped, so a broken cache degrades to "always fetch". + */ +export function createCache(config: CacheConfig): JsonCache { + return { + async get(key: string): Promise { + let text: string; + try { + text = await readFile(fileFor(config.dir, key), "utf8"); + } catch { + return undefined; + } + try { + const envelope = JSON.parse(text) as Envelope; + if ( + typeof envelope?.fetchedAt !== "number" || + Date.now() - envelope.fetchedAt > config.ttlMs + ) { + return undefined; + } + return envelope.value; + } catch { + return undefined; + } + }, + + async set(key: string, value: T): Promise { + const envelope: Envelope = { fetchedAt: Date.now(), value }; + try { + await mkdir(config.dir, { recursive: true }); + await writeFile( + fileFor(config.dir, key), + JSON.stringify(envelope), + "utf8", + ); + } catch { + // Best effort: a cache write failure must not break the audit. + } + }, + }; +} diff --git a/src/cli.ts b/src/cli.ts index 3726847..00021a7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -47,6 +47,10 @@ async function main(argv: string[]): Promise { "--fallback-to-node-modules", "read engines from node_modules when missing from the lockfile", ) + .option( + "--no-cache", + "do not read or write the on-disk registry cache", + ) .action(async (opts) => { const code = await runCheck({ cwd: opts.cwd, @@ -57,6 +61,7 @@ async function main(argv: string[]): Promise { suggest: opts.suggest, allowPrerelease: opts.allowPrerelease, fallbackToNodeModules: opts.fallbackToNodeModules, + noCache: !opts.cache, }); process.exitCode = code; }); diff --git a/src/commands/check.ts b/src/commands/check.ts index 6e184d5..ee6e6eb 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -1,5 +1,6 @@ import pc from "picocolors"; import { audit } from "../audit.js"; +import { createCache, defaultCacheDir, resolveCacheTtlMs } from "../cache.js"; import { loadConfig } from "../config.js"; import { LockfileError } from "../lockfile.js"; import { ProjectError, resolveProjectRange } from "../project.js"; @@ -15,6 +16,8 @@ export interface CheckCliOptions { suggest: boolean; allowPrerelease?: boolean; fallbackToNodeModules?: boolean; + /** Disable the on-disk registry cache (always hit the network). */ + noCache?: boolean; } /** Run the audit command. Returns the process exit code. */ @@ -37,6 +40,10 @@ export async function runCheck(opts: CheckCliOptions): Promise { throw err; } + const cache = opts.noCache + ? undefined + : createCache({ dir: defaultCacheDir(), ttlMs: resolveCacheTtlMs() }); + let result: AuditResult | undefined; try { result = await audit(opts.cwd, projectRange, { @@ -45,6 +52,7 @@ export async function runCheck(opts: CheckCliOptions): Promise { suggest: opts.suggest, allowPrerelease, fallbackToNodeModules: opts.fallbackToNodeModules, + cache, }); } catch (err) { if (err instanceof LockfileError) { diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..de0b163 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,30 @@ +import { format } from "node:util"; + +/** + * Namespaced debug logger, gated by `NODE_DEBUG` (the Node convention). It stays + * silent unless the namespace is listed — so it never fires for end users, only + * when a developer opts in with e.g. `NODE_DEBUG=check-my-engine`. + * + * Output goes to stderr so it never pollutes `--json` (or any other) stdout. The + * environment is read on every call, which keeps the logger trivially testable + * and costs nothing relative to the network round-trips it traces. + */ +export type Logger = (message: string, ...args: unknown[]) => void; + +const ENV_VAR = "NODE_DEBUG"; + +function isEnabled(namespace: string): boolean { + const raw = process.env[ENV_VAR]; + if (!raw) return false; + return raw + .split(/[\s,]+/) + .filter(Boolean) + .some((token) => token === "*" || token === namespace); +} + +export function createLogger(namespace: string): Logger { + return (message, ...args) => { + if (!isEnabled(namespace)) return; + process.stderr.write(`${namespace} ${format(message, ...args)}\n`); + }; +} diff --git a/src/registry.ts b/src/registry.ts index e6a3fe5..1903046 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -1,8 +1,12 @@ import semver from "semver"; +import type { JsonCache } from "./cache.js"; import { covers } from "./compat.js"; +import { createLogger } from "./log.js"; export class RegistryError extends Error {} +const log = createLogger("check-my-engine"); + const DEFAULT_REGISTRY = "https://registry.npmjs.org/"; interface AbbreviatedVersion { @@ -31,10 +35,22 @@ function packumentUrl(registry: string, name: string): string { export async function fetchPackument( name: string, registry = getRegistry(), + cache?: JsonCache, ): Promise { + const cacheKey = `${registry}\n${name}`; + if (cache) { + const hit = await cache.get(cacheKey); + if (hit) { + log("cache hit: %s", name); + return hit; + } + } + + const url = packumentUrl(registry, name); + log("registry fetch: %s", url); let res: Response; try { - res = await fetch(packumentUrl(registry, name), { + res = await fetch(url, { headers: { accept: "application/vnd.npm.install-v1+json" }, }); } catch (err) { @@ -47,13 +63,23 @@ export async function fetchPackument( `Registry returned ${res.status} ${res.statusText} for "${name}".`, ); } + let data: AbbreviatedPackument; try { - return (await res.json()) as AbbreviatedPackument; + data = (await res.json()) as AbbreviatedPackument; } catch (err) { throw new RegistryError( `Invalid response from registry for "${name}": ${(err as Error).message}`, ); } + + log( + "registry hit: %s (%d, %d versions)", + name, + res.status, + Object.keys(data.versions ?? {}).length, + ); + if (cache) await cache.set(cacheKey, data); + return data; } export interface FindOptions { @@ -63,6 +89,8 @@ export interface FindOptions { registry?: string; /** Skip versions marked deprecated. Default true. */ skipDeprecated?: boolean; + /** On-disk packument cache. Omit to always hit the network. */ + cache?: JsonCache; } /** @@ -77,6 +105,7 @@ export async function findLatestInRange( const packument = await fetchPackument( name, options.registry ?? getRegistry(), + options.cache, ); const versions = packument.versions ?? {}; const skipDeprecated = options.skipDeprecated ?? true; diff --git a/test/cache.test.ts b/test/cache.test.ts new file mode 100644 index 0000000..5ae58fe --- /dev/null +++ b/test/cache.test.ts @@ -0,0 +1,87 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createCache, defaultCacheDir, resolveCacheTtlMs } from "../src/cache"; + +let dir: string; + +beforeEach(async () => { + dir = await mkdtemp(path.join(os.tmpdir(), "cme-cache-")); +}); + +afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + vi.useRealTimers(); + vi.unstubAllEnvs(); +}); + +describe("createCache", () => { + it("returns a previously stored value within the TTL", async () => { + const cache = createCache({ dir, ttlMs: 10_000 }); + await cache.set("demo", { hello: "world" }); + expect(await cache.get<{ hello: string }>("demo")).toEqual({ + hello: "world", + }); + }); + + it("returns undefined for an unknown key", async () => { + const cache = createCache({ dir, ttlMs: 10_000 }); + expect(await cache.get("missing")).toBeUndefined(); + }); + + it("treats entries older than the TTL as a miss", async () => { + vi.useFakeTimers(); + const cache = createCache({ dir, ttlMs: 1_000 }); + await cache.set("demo", { n: 1 }); + vi.advanceTimersByTime(1_500); + expect(await cache.get("demo")).toBeUndefined(); + }); + + it("keys by the full string, so distinct keys do not collide", async () => { + const cache = createCache({ dir, ttlMs: 10_000 }); + await cache.set("https://r/a", { v: "a" }); + await cache.set("https://r/b", { v: "b" }); + expect(await cache.get<{ v: string }>("https://r/a")).toEqual({ v: "a" }); + expect(await cache.get<{ v: string }>("https://r/b")).toEqual({ v: "b" }); + }); + + it("handles scoped package keys with slashes", async () => { + const cache = createCache({ dir, ttlMs: 10_000 }); + await cache.set("https://registry/@scope/pkg", { v: "scoped" }); + expect( + await cache.get<{ v: string }>("https://registry/@scope/pkg"), + ).toEqual({ v: "scoped" }); + }); +}); + +describe("resolveCacheTtlMs", () => { + it("defaults to one hour", () => { + vi.stubEnv("CHECK_MY_ENGINE_CACHE_TTL", ""); + expect(resolveCacheTtlMs()).toBe(60 * 60 * 1000); + }); + + it("reads seconds from the environment", () => { + vi.stubEnv("CHECK_MY_ENGINE_CACHE_TTL", "30"); + expect(resolveCacheTtlMs()).toBe(30_000); + }); + + it("ignores a non-numeric or negative value", () => { + vi.stubEnv("CHECK_MY_ENGINE_CACHE_TTL", "nonsense"); + expect(resolveCacheTtlMs()).toBe(60 * 60 * 1000); + vi.stubEnv("CHECK_MY_ENGINE_CACHE_TTL", "-5"); + expect(resolveCacheTtlMs()).toBe(60 * 60 * 1000); + }); +}); + +describe("defaultCacheDir", () => { + it("honors CHECK_MY_ENGINE_CACHE_DIR", () => { + vi.stubEnv("CHECK_MY_ENGINE_CACHE_DIR", "/tmp/explicit"); + expect(defaultCacheDir()).toBe("/tmp/explicit"); + }); + + it("falls back to XDG_CACHE_HOME", () => { + vi.stubEnv("CHECK_MY_ENGINE_CACHE_DIR", ""); + vi.stubEnv("XDG_CACHE_HOME", "/tmp/xdg"); + expect(defaultCacheDir()).toBe(path.join("/tmp/xdg", "check-my-engine")); + }); +}); diff --git a/test/config.test.ts b/test/config.test.ts index b51aa3c..bcf8064 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,7 +1,6 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; import { loadConfig } from "../src/config"; const dirs: string[] = []; diff --git a/test/init.test.ts b/test/init.test.ts index 00b8ef7..855538b 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -1,7 +1,6 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; import { runInit } from "../src/commands/init"; const dirs: string[] = []; diff --git a/test/log.test.ts b/test/log.test.ts new file mode 100644 index 0000000..3244e4d --- /dev/null +++ b/test/log.test.ts @@ -0,0 +1,66 @@ +import { createLogger } from "../src/log"; + +function captureStderr() { + const writes: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk) => { + writes.push(String(chunk)); + return true; + }); + return writes; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); +}); + +describe("createLogger", () => { + it("is silent when NODE_DEBUG is unset", () => { + vi.stubEnv("NODE_DEBUG", ""); + const writes = captureStderr(); + createLogger("check-my-engine")("hello"); + expect(writes).toHaveLength(0); + }); + + it("writes to stderr when its namespace is enabled", () => { + vi.stubEnv("NODE_DEBUG", "check-my-engine"); + const writes = captureStderr(); + createLogger("check-my-engine")("hello"); + expect(writes).toHaveLength(1); + expect(writes[0]).toContain("check-my-engine"); + expect(writes[0]).toContain("hello"); + expect(writes[0]?.endsWith("\n")).toBe(true); + }); + + it("formats printf-style arguments", () => { + vi.stubEnv("NODE_DEBUG", "check-my-engine"); + const writes = captureStderr(); + createLogger("check-my-engine")("hit %s (%d)", "demo", 200); + expect(writes[0]).toContain("hit demo (200)"); + }); + + it("enables every namespace with a wildcard", () => { + vi.stubEnv("NODE_DEBUG", "*"); + const writes = captureStderr(); + createLogger("check-my-engine")("yo"); + expect(writes).toHaveLength(1); + }); + + it("ignores unrelated namespaces", () => { + vi.stubEnv("NODE_DEBUG", "something-else,another"); + const writes = captureStderr(); + createLogger("check-my-engine")("nope"); + expect(writes).toHaveLength(0); + }); + + it("re-reads the environment on each call", () => { + const writes = captureStderr(); + const log = createLogger("check-my-engine"); + vi.stubEnv("NODE_DEBUG", ""); + log("off"); + vi.stubEnv("NODE_DEBUG", "check-my-engine"); + log("on"); + expect(writes).toHaveLength(1); + expect(writes[0]).toContain("on"); + }); +}); diff --git a/test/registry.test.ts b/test/registry.test.ts index 423883d..35e26ee 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createCache } from "../src/cache"; import { findLatestInRange } from "../src/registry"; function mockPackument(versions: Record) { @@ -68,4 +71,50 @@ describe("findLatestInRange", () => { }); expect(await findLatestInRange("demo", ">=20")).toBe("1.0.0"); }); + + it("serves a cached packument without a second network call", async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), "cme-reg-")); + const cache = createCache({ dir, ttlMs: 60_000 }); + const fetchSpy = vi.fn( + async () => + new Response( + JSON.stringify({ + versions: { + "1.0.0": { version: "1.0.0", engines: { node: ">=18" } }, + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + vi.stubGlobal("fetch", fetchSpy); + + expect(await findLatestInRange("demo", ">=20", { cache })).toBe("1.0.0"); + expect(await findLatestInRange("demo", ">=20", { cache })).toBe("1.0.0"); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + await rm(dir, { recursive: true, force: true }); + }); + + it("logs registry and cache hits when NODE_DEBUG is enabled", async () => { + vi.stubEnv("NODE_DEBUG", "check-my-engine"); + const stderr: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk) => { + stderr.push(String(chunk)); + return true; + }); + const dir = await mkdtemp(path.join(os.tmpdir(), "cme-reg-log-")); + const cache = createCache({ dir, ttlMs: 60_000 }); + mockPackument({ "1.0.0": { version: "1.0.0", engines: { node: ">=18" } } }); + + await findLatestInRange("demo", ">=20", { cache }); + await findLatestInRange("demo", ">=20", { cache }); + + const out = stderr.join(""); + expect(out).toContain("registry hit: demo"); + expect(out).toContain("cache hit: demo"); + + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await rm(dir, { recursive: true, force: true }); + }); });