From f34b36308c934978ae409607e440d830459efc66 Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Wed, 1 Jul 2026 11:18:40 -0400 Subject: [PATCH 1/2] Add code-health PR check: dead-code delta vs base --- .github/scripts/knip-classify.mjs | 151 ++++ .github/scripts/knip-delta.mjs | 228 ++++++ .github/workflows/health.yml | 120 ++++ knip.json | 6 + package-lock.json | 1107 ++++++++++++++++++++++++++--- package.json | 4 +- 6 files changed, 1534 insertions(+), 82 deletions(-) create mode 100644 .github/scripts/knip-classify.mjs create mode 100644 .github/scripts/knip-delta.mjs create mode 100644 .github/workflows/health.yml create mode 100644 knip.json diff --git a/.github/scripts/knip-classify.mjs b/.github/scripts/knip-classify.mjs new file mode 100644 index 000000000..e3483a15d --- /dev/null +++ b/.github/scripts/knip-classify.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +// Tag each unused exported symbol in a Knip JSON report with whether it's still +// referenced privately within its own file — beyond its own declaration. This +// separates an *unnecessary export* (used privately — just drop `export`, a +// clean diff) from a *dead exported symbol* (used nowhere — the export was only +// hiding it from the unused-var linter, so delete it). +// +// We parse each file with the TypeScript compiler and ask whether any reference +// to the symbol falls outside its own declaration's subtree. A name-count regex +// can't answer that: it reads a recursive call or a self-referencing type as a +// "use" and mislabels genuinely dead code as merely unnecessary. +// +// Must run while the report's ref is checked out, since it reads source files. +// Rewrites the report in place. Usage: node knip-classify.mjs + +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const REPO = process.env.GITHUB_WORKSPACE || process.cwd(); + +// TypeScript ships as CommonJS; import it by absolute path so this script +// resolves it even when run from outside the repo (CI copies it to a temp dir, +// which has no node_modules to walk up into). If TS can't load, leave the report +// unclassified — knip-delta detects that and posts its own alert. +let ts; +try { + const url = pathToFileURL( + path.join(REPO, 'node_modules/typescript/lib/typescript.js') + ).href; + ts = (await import(url)).default; +} catch { + process.exit(0); +} + +const jsonPath = process.argv[2]; +if (!jsonPath) { + console.error('usage: knip-classify.mjs '); + process.exit(1); +} + +const SYM = ['exports', 'types', 'nsExports', 'nsTypes']; + +let data; +try { + data = JSON.parse(readFileSync(jsonPath, 'utf8')); +} catch { + process.exit(0); // nothing to classify; delta script degrades gracefully +} +if (!data || !Array.isArray(data.issues)) process.exit(0); + +// Parse each source file once. Returns the SourceFile, or null if unreadable. +const cache = new Map(); +const parse = (file) => { + if (!cache.has(file)) { + try { + const text = readFileSync(path.join(REPO, file), 'utf8'); + // Pass the real filename so .tsx is parsed as JSX. + cache.set( + file, + ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true) + ); + } catch { + cache.set(file, null); + } + } + return cache.get(file); +}; + +// An identifier that names a member/binding rather than *referencing* the +// symbol: the `b` in `a.b`, the key in `{ b: … }`, a class/interface member +// name, an enum member, an import/export specifier. Shorthand `{ b }` is a real +// reference, so it is intentionally not excluded here. +const isNonReference = (id) => { + const p = id.parent; + if (!p) return false; + if (ts.isPropertyAccessExpression(p)) return p.name === id; + if (ts.isQualifiedName(p)) return p.right === id; + if ( + ts.isPropertyAssignment(p) || + ts.isPropertySignature(p) || + ts.isPropertyDeclaration(p) || + ts.isMethodSignature(p) || + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) || + ts.isEnumMember(p) || + ts.isImportSpecifier(p) || + ts.isExportSpecifier(p) + ) + return p.name === id; + return false; +}; + +// The top-level statement enclosing a node — the declaration's own subtree. +const topLevelStatement = (node, sf) => { + let n = node; + while (n.parent && n.parent !== sf) n = n.parent; + return n; +}; + +// privateUse: is the symbol referenced anywhere outside its own declaration? +// We find the declaration by the identifier nearest knip's reported `pos`, then +// look for any other reference to the same name outside that declaration's range. +const isPrivatelyUsed = (sf, name, pos) => { + const refs = []; + let decl = null; + let bestDist = Infinity; + const visit = (node) => { + if ( + ts.isIdentifier(node) && + node.text === name && + !isNonReference(node) + ) { + const start = node.getStart(sf); + refs.push(start); + if (typeof pos === 'number') { + const dist = Math.abs(start - pos); + if (dist < bestDist) { + bestDist = dist; + decl = node; + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + + // Couldn't anchor to a declaration (missing pos, re-export, generated name): + // fall back to "more than one reference exists" — imperfect, but no worse + // than the old count heuristic. + if (!decl) return refs.length >= 2; + + const stmt = topLevelStatement(decl, sf); + const lo = stmt.getStart(sf); + const hi = stmt.getEnd(); + return refs.some((start) => start < lo || start >= hi); +}; + +for (const issue of data.issues) { + for (const cat of SYM) { + for (const s of issue[cat] ?? []) { + const sf = parse(issue.file); + s.privateUse = sf + ? isPrivatelyUsed(sf, String(s.name), s.pos) + : false; + } + } +} + +writeFileSync(jsonPath, JSON.stringify(data)); diff --git a/.github/scripts/knip-delta.mjs b/.github/scripts/knip-delta.mjs new file mode 100644 index 000000000..82161fcf7 --- /dev/null +++ b/.github/scripts/knip-delta.mjs @@ -0,0 +1,228 @@ +#!/usr/bin/env node +// Diff two Knip JSON reports (base vs head) into a PR "code-health" delta: which +// dead-code issues this PR introduces vs. removes, across the categories Knip +// reports (unused files, exports, dependencies, …). +// +// The comparison is against the PR's base, so the bar ratchets forward on its +// own as main improves — no committed baseline to maintain. +// +// Usage: node knip-delta.mjs +// Always exits 0 (informational). Emits `introduced` / `fixed` as step outputs +// so a future version can gate on `introduced > 0`. +import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; + +const [, , basePath, headPath, outPath] = process.argv; +const MARKER = ''; +const CAP = 10; // max offenders listed per category before "…and N more" + +// Raw Knip issue arrays we read from each file. `files` is the file itself being +// unused; the rest are arrays of symbols/names on a file. (unresolved imports +// and duplicate exports are intentionally omitted for now — add back here.) +const RAW_CATS = [ + 'files', + 'dependencies', + 'devDependencies', + 'optionalPeerDependencies', + 'unlisted', + 'binaries', + 'exports', + 'types', + 'nsExports', + 'nsTypes', + 'enumMembers', + 'classMembers', +]; +const EXPORTED = new Set(['exports', 'types', 'nsExports', 'nsTypes']); + +// Fold raw categories into the display categories used in the report: +// - deps / devDeps / optional peers collapse into one "unused dependencies" row; +// - an unused export splits by whether the symbol is still used privately in its +// own file — an *unnecessary export* (just drop `export`, clean diff) — or +// unused everywhere — a *dead exported symbol* the export was hiding from the +// linter (delete it). `privateUse` is tagged per-symbol by knip-classify.mjs +// while each ref is checked out; if absent, fall back to one "unused exports". +function displayCat(cat, entry) { + if (EXPORTED.has(cat)) return entry.privateUse ? 'unnecessary' : 'dead'; + if (cat === 'devDependencies' || cat === 'optionalPeerDependencies') + return 'dependencies'; + return cat; +} + +// Display order + labels. +const LABELS = { + files: 'Dead files', + dead: 'Dead symbols', + enumMembers: 'Dead enum members', + unnecessary: 'Unnecessary exports', + dependencies: 'Unused dependencies', + unlisted: 'Unlisted dependencies', + classMembers: 'Unused class members', + binaries: 'Unused binaries', +}; + +// One-line explainer per display category, shown when its section is expanded. +const EXPLAIN = { + files: 'File imported nowhere — delete (or import) it.', + dead: 'Symbol used nowhere — `export` hides it from the linter; delete it', + enumMembers: 'An enum member referenced nowhere', + unnecessary: + 'Exported but only used within its own file — just drop `export`', + dependencies: 'In package.json but never imported', + unlisted: 'Imported but missing from package.json', + classMembers: 'A class member referenced nowhere', + binaries: 'A referenced binary that is not installed', +}; + +function load(path) { + try { + const data = JSON.parse(readFileSync(path, 'utf8')); + if (!data || !Array.isArray(data.issues)) return null; + return data; + } catch { + return null; + } +} + +// Map every issue to a stable key so we can diff the *sets* (not just counts): +// a PR that fixes one issue and adds another nets zero but still introduced one. +function keyset(report) { + const map = new Map(); // key -> {cat, file, name} (cat = display category) + for (const issue of report.issues) { + const file = issue.file ?? '(root)'; + for (const cat of RAW_CATS) { + for (const entry of issue[cat] ?? []) { + const name = + typeof entry === 'string' + ? entry + : (entry?.name ?? JSON.stringify(entry)); + const dcat = displayCat(cat, entry || {}); + const key = + dcat === 'files' + ? `files ${name}` + : `${dcat} ${file} ${name}`; + map.set(key, { cat: dcat, file, name }); + } + } + } + return map; +} + +// Inner text for an offender; wrapped in backticks (monospace) by the caller. +function fmt(entry) { + return entry.cat === 'files' ? entry.name : `${entry.file} : ${entry.name}`; +} + +// A report is "classified" if every exported symbol carries a privateUse tag +// (added by knip-classify.mjs); without it the unnecessary/dead split is a lie, +// so we fail loudly rather than mislabel. +function isClassified(report) { + for (const issue of report.issues) + for (const cat of EXPORTED) + for (const entry of issue[cat] ?? []) + if (entry && entry.privateUse === undefined) return false; + return true; +} + +const base = load(basePath); +const head = load(headPath); + +let body; +if (!base || !head) { + body = `${MARKER}\n## ⚠️ Code Health\n\nCouldn't compute the delta — Knip failed to produce a report on ${!base ? 'the base' : 'this branch'}. (Check the job logs.)`; + writeFileSync(outPath, body); + console.log(body); + process.exit(1); +} + +if (!isClassified(head) || !isClassified(base)) { + body = `${MARKER}\n## ⚠️ Code Health\n\nThe classify step (knip-classify) didn't run, so unnecessary vs. dead exports can't be split. Failing the check — see the job logs.`; + writeFileSync(outPath, body); + console.log(body); + process.exit(1); +} + +const baseKeys = keyset(base); +const headKeys = keyset(head); +const introduced = [...headKeys] + .filter(([k]) => !baseKeys.has(k)) + .map(([, v]) => v); +const fixed = [...baseKeys].filter(([k]) => !headKeys.has(k)).map(([, v]) => v); +const carried = [...headKeys] // "old": present in both base and head (total − new) + .filter(([k]) => baseKeys.has(k)) + .map(([, v]) => v); + +const groupByCat = (items) => { + const m = {}; + for (const i of items) (m[i.cat] ??= []).push(i); + return m; +}; +const newG = groupByCat(introduced); +const fixedG = groupByCat(fixed); +const oldG = groupByCat(carried); + +// Offenders go in a blockquote (normal markdown, so both LaTeX math and inline +// code render). A math-colored ± marker gives new = red `+` / gone = green `-`, +// matching the header on BOTH sign and color — which a ```diff block couldn't, +// since its color is tied to the sign. Paths stay monospace via backticks (the +// marker is colored; the path keeps the default code color). +const MARK = { + new: '$\\textcolor{red}{+}$ ', + fixed: '$\\textcolor{green}{-}$ ', + old: ' '.repeat(5), // non-breaking spaces: won't collapse in the HTML +}; +const blocks = []; +for (const cat of Object.keys(LABELS)) { + const nw = newG[cat] ?? []; + const fx = fixedG[cat] ?? []; + const od = oldG[cat] ?? []; + const total = nw.length + od.length; + if (!total && !fx.length) continue; + + const deltas = []; + if (nw.length) deltas.push(`$\\textcolor{red}{+${nw.length}}$`); + if (fx.length) deltas.push(`$\\textcolor{green}{-${fx.length}}$`); + const summary = + `${total} ${LABELS[cat]}` + + (deltas.length ? ` ${deltas.join(' ')}` : ''); + + const items = [ + ...nw.map((i) => `${MARK.new}\`${fmt(i)}\``), + ...fx.map((i) => `${MARK.fixed}\`${fmt(i)}\``), + ...od.map((i) => `${MARK.old}\`${fmt(i)}\``), + ]; + const lines = items.slice(0, CAP); + if (items.length > CAP) lines.push(`…and ${items.length - CAP} more`); + const list = lines.join('\n'); + + blocks.push( + `${summary}\n\n> ${EXPLAIN[cat]}\n\n${list}\n\n` + ); +} + +// ❌ any dead code added or newly orphaned (even if the PR also cleaned some up); +// ✅ only removals; ⚪ no change. (⚠️ is reserved for a failed check, above.) +let emoji, headline; +if (introduced.length > 0) { + emoji = '❌'; + headline = `Introduces $\\textcolor{red}{${introduced.length}}$ dead-code issue${introduced.length > 1 ? 's' : ''}${fixed.length ? ` (removes $\\textcolor{green}{${fixed.length}}$)` : ''}.`; +} else if (fixed.length > 0) { + emoji = '✅'; + headline = `Removes $\\textcolor{green}{${fixed.length}}$ dead-code issue${fixed.length > 1 ? 's' : ''}, introduces none.`; +} else { + emoji = '⚪'; + headline = 'No change to the dead-code surface.'; +} + +body = + `${MARKER}\n## ${emoji} Code Health\n\n${headline}\n\n` + blocks.join('\n'); + +writeFileSync(outPath, body); +console.log(body); + +if (process.env.GITHUB_STEP_SUMMARY) + appendFileSync(process.env.GITHUB_STEP_SUMMARY, body + '\n'); +if (process.env.GITHUB_OUTPUT) + appendFileSync( + process.env.GITHUB_OUTPUT, + `introduced=${introduced.length}\nfixed=${fixed.length}\n` + ); diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml new file mode 100644 index 000000000..329c80600 --- /dev/null +++ b/.github/workflows/health.yml @@ -0,0 +1,120 @@ +name: Code Health + +# Informational dead-code delta for each PR. Runs Knip on the PR head and on the +# base, then comments the difference. The bar is always "the base branch," so it +# ratchets forward on its own as main improves — no committed baseline needed. + +permissions: + contents: read + packages: read # `@estuary/flow-web` install + pull-requests: write # post the delta comment + +on: + pull_request: + +concurrency: + group: health-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + knip-delta: + name: Dead-code delta + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22.22.0 + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + registry-url: 'https://npm.pkg.github.com' + scope: '@estuary' + + - name: Get Deps + run: npm ci + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Both runs use the head branch's installed Knip/toolchain, so only the + # source differs between them. Knip exits non-zero when it finds issues + # (expected on a dirty tree), so tolerate it and let the script read JSON. + - name: Knip on PR head + run: | + git checkout --force --quiet ${{ github.event.pull_request.head.sha }} + # Stash the classifier so it survives the base checkout below + # (base doesn't have it on a PR that introduces this check). + cp .github/scripts/knip-classify.mjs "$RUNNER_TEMP/classify.mjs" + node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/head.json" || true + node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/head.json" + + - name: Knip on base (measured with the head's config) + run: | + git checkout --force --quiet ${{ github.event.pull_request.base.sha }} + # Pin the HEAD's Knip config for the base run too, so a PR that + # only changes config (like the one that introduces this check) + # doesn't manufacture a phantom delta. The ruler is constant; + # only source differences register. Falls back to base's config. + git checkout --quiet ${{ github.event.pull_request.head.sha }} -- knip.json 2>/dev/null || true + node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/base.json" || true + # Classify against the base's checked-out source (files present now). + node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/base.json" + + - name: Restore head + run: git checkout --force --quiet ${{ github.event.pull_request.head.sha }} + + - name: Compute delta + id: delta + run: node .github/scripts/knip-delta.mjs "$RUNNER_TEMP/base.json" "$RUNNER_TEMP/head.json" "$RUNNER_TEMP/health.md" + + # always(): the delta step exits non-zero (fails the check) when the + # tool itself is broken — knip produced no report, or the classify + # step didn't run — and we still want its alert comment posted. + - name: Upsert PR comment + if: always() + uses: actions/github-script@v7 + env: + HEALTH_MD: ${{ runner.temp }}/health.md + with: + script: | + const fs = require('fs'); + // No report file → an earlier step failed before the delta ran; nothing to post. + if (!fs.existsSync(process.env.HEALTH_MD)) return; + const body = fs.readFileSync(process.env.HEALTH_MD, 'utf8'); + const marker = ''; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + const existing = comments.find( + (c) => c.body && c.body.includes(marker) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + + # Once you trust the signal, un-comment to make new dead code block the PR: + # - name: Enforce no new dead code + # if: steps.delta.outputs.introduced != '0' + # run: | + # echo "::error::This PR introduces ${{ steps.delta.outputs.introduced }} new dead-code issue(s). See the Code-health comment." + # exit 1 diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..c8f55f61a --- /dev/null +++ b/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "tags": ["-lintignore"], + "project": ["src/**/*.{ts,tsx}"], + "ignore": ["src/gql-types/**", "playwright-tests/**"] +} diff --git a/package-lock.json b/package-lock.json index 8a2370e4f..61a73db10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,6 +120,7 @@ "eslint-plugin-storybook": "^10.3.4", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^24.0.0", + "knip": "^6.23.0", "licensee": "^11.1.1", "msw": "^2.0.11", "prettier": "^3.5.3", @@ -636,10 +637,33 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -4143,6 +4167,25 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -4873,79 +4916,725 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", - "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz", + "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz", + "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz", + "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz", + "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz", + "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz", + "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz", + "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz", + "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz", + "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz", + "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz", + "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz", + "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz", + "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz", + "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz", + "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz", + "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz", + "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", + "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz", + "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz", + "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=14.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", - "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", - "license": "Apache-2.0", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "tslib": "^2.4.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", + "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", @@ -5971,6 +6660,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6151,12 +6851,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", - "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~8.3.0" } }, "node_modules/@types/normalize-package-data": { @@ -10641,6 +11341,26 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fd-package-json/node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -10824,6 +11544,22 @@ "node": ">= 6" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -11015,6 +11751,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -12472,9 +13221,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -12790,6 +13539,89 @@ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true }, + "node_modules/knip": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.23.0.tgz", + "integrity": "sha512-2DvAOX2pZWiG4SLvRRxOAU0aWGEn1ZoVblI541xIoXFdHqq2THMZXy66/qcY5WGuW3TXhb9T1x1zd/Hd1u+yqg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.3.0", + "get-tsconfig": "4.14.0", + "jiti": "^2.7.0", + "oxc-parser": "^0.137.0", + "oxc-resolver": "11.21.3", + "picomatch": "^4.0.4", + "smol-toml": "^1.6.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.1", + "yaml": "^2.9.0", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -14580,6 +15412,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz", + "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.137.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.137.0", + "@oxc-parser/binding-android-arm64": "0.137.0", + "@oxc-parser/binding-darwin-arm64": "0.137.0", + "@oxc-parser/binding-darwin-x64": "0.137.0", + "@oxc-parser/binding-freebsd-x64": "0.137.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.137.0", + "@oxc-parser/binding-linux-arm64-musl": "0.137.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.137.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-musl": "0.137.0", + "@oxc-parser/binding-openharmony-arm64": "0.137.0", + "@oxc-parser/binding-wasm32-wasi": "0.137.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.137.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.137.0", + "@oxc-parser/binding-win32-x64-msvc": "0.137.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", + "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.3", + "@oxc-resolver/binding-android-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-x64": "11.21.3", + "@oxc-resolver/binding-freebsd-x64": "11.21.3", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-musl": "11.21.3", + "@oxc-resolver/binding-openharmony-arm64": "11.21.3", + "@oxc-resolver/binding-wasm32-wasi": "11.21.3", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + } + }, "node_modules/p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -16181,6 +17082,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -16946,6 +17857,19 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -17833,14 +18757,14 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -18242,6 +19166,16 @@ "node": ">=14.17" } }, + "node_modules/unbash": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.2.tgz", + "integrity": "sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -18272,9 +19206,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unicode-emoji-utils": { @@ -19453,10 +20387,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -19538,6 +20473,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", diff --git a/package.json b/package.json index d14beb502..eb369cc43 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "build-storybook": "storybook build", "codegen": "graphql-codegen", "codegen:local": "LOCAL=true graphql-codegen", - "codegen:check": "graphql-codegen --check" + "codegen:check": "graphql-codegen --check", + "knip": "knip" }, "dependencies": { "@date-fns/utc": "^2.1.1", @@ -157,6 +158,7 @@ "eslint-plugin-storybook": "^10.3.4", "eslint-plugin-unused-imports": "^4.3.0", "jsdom": "^24.0.0", + "knip": "^6.23.0", "licensee": "^11.1.1", "msw": "^2.0.11", "prettier": "^3.5.3", From 598f4d0bd9d850c22098fd0a79fd57134a006b29 Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Wed, 1 Jul 2026 23:11:31 -0400 Subject: [PATCH 2/2] Add an orphaned-props category to the Code Health check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prop can be declared (and even read internally) on a component while no caller anywhere in src/ ever passes it, so it's always its default/undefined value in practice. Knip doesn't catch this — it only tracks module-level exports, not JSX call sites. orphaned-props.mjs resolves each component's Props type syntactically (inline literals, named interfaces/type aliases, extends, intersections, and Pick/Omit/Partial/Required/Readonly) and cross-references it against every JSX invocation in the tree, using this repo's absolute src/-rooted imports to match usage sites back to declarations without needing a full type-checker. A component is skipped rather than guessed at when its Props type doesn't resolve cleanly, when any call site spreads attributes (which could supply any prop), or when it has zero call sites (that's dead-code territory, already covered by Knip). children is treated as satisfied by non-empty JSX content, not just an explicit attribute. Results merge into the existing Knip JSON report as a new orphanedProps category, so knip-delta.mjs's existing base-vs-head ratchet and PR-comment rendering pick it up for free. --- .github/scripts/knip-delta.mjs | 3 + .github/scripts/orphaned-props.mjs | 557 +++++++++++++++++++++++++++++ .github/workflows/health.yml | 9 +- 3 files changed, 566 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/orphaned-props.mjs diff --git a/.github/scripts/knip-delta.mjs b/.github/scripts/knip-delta.mjs index 82161fcf7..15dd2c2c4 100644 --- a/.github/scripts/knip-delta.mjs +++ b/.github/scripts/knip-delta.mjs @@ -31,6 +31,7 @@ const RAW_CATS = [ 'nsTypes', 'enumMembers', 'classMembers', + 'orphanedProps', ]; const EXPORTED = new Set(['exports', 'types', 'nsExports', 'nsTypes']); @@ -58,6 +59,7 @@ const LABELS = { unlisted: 'Unlisted dependencies', classMembers: 'Unused class members', binaries: 'Unused binaries', + orphanedProps: 'Orphaned props', }; // One-line explainer per display category, shown when its section is expanded. @@ -71,6 +73,7 @@ const EXPLAIN = { unlisted: 'Imported but missing from package.json', classMembers: 'A class member referenced nowhere', binaries: 'A referenced binary that is not installed', + orphanedProps: 'Declared on the component but no caller ever passes it', }; function load(path) { diff --git a/.github/scripts/orphaned-props.mjs b/.github/scripts/orphaned-props.mjs new file mode 100644 index 000000000..8ce710c68 --- /dev/null +++ b/.github/scripts/orphaned-props.mjs @@ -0,0 +1,557 @@ +#!/usr/bin/env node +// Find React component props that are declared (and possibly read internally) +// but that no call site anywhere in `src/` ever passes — so the prop is always +// its default/undefined value in practice. Merges results into an existing Knip +// JSON report (adds an `orphanedProps` array to each file's issue entry) so they +// ride the same base-vs-head ratchet in knip-delta.mjs. +// +// Purely syntactic, like knip-classify.mjs: each file is parsed once with the TS +// compiler and never type-checked. Cross-file resolution (a component's Props +// type living in a sibling `types.ts`) relies on this repo's convention of +// absolute `src/`-rooted imports, so a module specifier maps straight to a file +// path — no tsconfig path aliasing or relative-import resolution needed. +// +// Two passes over every file: +// 1. Declarations — record every interface/type alias, every import, and every +// component (function/arrow assigned to a Capitalized name) along with its +// resolved Props member names. +// 2. Usage — walk every JSX element, resolve its tag to a component from pass +// 1, and record which attribute names are actually passed at that site. +// +// A component is skipped entirely (never reported) rather than guessed at when: +// - its Props type can't be resolved syntactically (generics, mapped/ +// conditional types, a type re-exported through a barrel, etc.) +// - any call site spreads attributes (``) — the spread could +// supply any prop, so we can't rule any of them out +// - it has zero call sites — that's a dead component (Knip's job), not an +// orphaned-prop issue +// +// Must run while the report's ref is checked out, since it reads source files. +// Usage: node orphaned-props.mjs + +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const REPO = process.env.GITHUB_WORKSPACE || process.cwd(); + +let ts; +try { + const url = pathToFileURL( + path.join(REPO, 'node_modules/typescript/lib/typescript.js') + ).href; + ts = (await import(url)).default; +} catch { + process.exit(0); // leave the report untouched; knip-delta degrades gracefully +} + +const jsonPath = process.argv[2]; +if (!jsonPath) { + console.error('usage: orphaned-props.mjs '); + process.exit(1); +} + +const INDETERMINATE = Symbol('indeterminate'); + +// ---- File discovery --------------------------------------------------- + +const IGNORE_DIRS = new Set(['gql-types']); + +function walk(dir, out) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (IGNORE_DIRS.has(entry.name)) continue; + walk(full, out); + } else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) { + out.push(path.relative(REPO, full)); + } + } +} + +const files = []; +walk(path.join(REPO, 'src'), files); + +// ---- Parse once, reuse everywhere -------------------------------------- + +const sourceCache = new Map(); +function parse(file) { + if (!sourceCache.has(file)) { + try { + const text = readFileSync(path.join(REPO, file), 'utf8'); + sourceCache.set( + file, + ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true) + ); + } catch { + sourceCache.set(file, null); + } + } + return sourceCache.get(file); +} + +// ---- Module resolution -------------------------------------------------- +// This repo imports almost everything by absolute `src/...` specifier, so a +// module specifier maps directly onto a file path. Anything else (external +// packages, the rare relative import) is left unresolved and simply won't +// match any locally-declared component or type. + +const resolveCache = new Map(); +function resolveModule(spec) { + if (!spec.startsWith('src/')) return null; + if (resolveCache.has(spec)) return resolveCache.get(spec); + const candidates = [ + `${spec}.ts`, + `${spec}.tsx`, + `${spec}/index.ts`, + `${spec}/index.tsx`, + ]; + const hit = candidates.find((c) => existsSync(path.join(REPO, c))) ?? null; + resolveCache.set(spec, hit); + return hit; +} + +// ---- Pass 1: declarations ------------------------------------------------- + +// file -> Map +const importsByFile = new Map(); +// file -> Map +const typesByFile = new Map(); +// componentKey -> { name, file, declFile, declPos, propsNode, usedProps: Set, hasSpread: bool, callSites: number } +const components = new Map(); + +function componentKey(file, name) { + return `${file}#${name}`; +} + +function registerComponent(file, name, propsTypeNode, declPos) { + const key = componentKey(file, name); + if (components.has(key)) return components.get(key); + const entry = { + name, + file, + propsTypeNode, + declPos, + usedProps: new Set(), + hasSpread: false, + callSites: 0, + }; + components.set(key, entry); + return entry; +} + +// Unwrap `memo(...)` / `forwardRef(...)` (possibly nested) to the inner +// function-like node actually receiving props as its first parameter. +function unwrapComponentWrapper(node) { + let n = node; + while ( + ts.isCallExpression(n) && + ts.isIdentifier(n.expression) && + (n.expression.text === 'memo' || n.expression.text === 'forwardRef') && + n.arguments.length > 0 + ) { + n = n.arguments[0]; + } + return n; +} + +function isCapitalized(name) { + return /^[A-Z]/.test(name); +} + +// Find `FC` / `FunctionComponent` / `React.FC` etc. on a +// variable's own type annotation, when the props type isn't on the parameter. +function propsFromFCAnnotation(typeNode) { + if (!typeNode || !ts.isTypeReferenceNode(typeNode)) return null; + const name = ts.isQualifiedName(typeNode.typeName) + ? typeNode.typeName.right.text + : typeNode.typeName.text; + if ( + (name === 'FC' || name === 'FunctionComponent' || name === 'VFC') && + typeNode.typeArguments?.length + ) { + return typeNode.typeArguments[0]; + } + return null; +} + +function declarationsPass(file, sf) { + const imports = new Map(); + const types = new Map(); + importsByFile.set(file, imports); + typesByFile.set(file, types); + + // Track local names, plus which of them (if any) are *also* the file's + // default export — so we register ONE entry per component and alias the + // `#default` key to it, rather than creating a second entry object (which + // would double-count it: `components.values()` would yield the component + // twice, once per key, if two keys pointed at two different objects). + const localComponents = new Map(); // name -> {propsNode, pos} + const defaultAliasNames = new Set(); + let anonymousDefault = null; // {propsNode, pos}, for a nameless default export + + const propsNodeOf = (fnLike) => { + const params = fnLike.parameters; + if (params.length < 1 || params.length > 2) return null; + let propsNode = params[0].type ?? null; + if ( + !propsNode && + fnLike.parent && + ts.isVariableDeclaration(fnLike.parent) + ) { + propsNode = propsFromFCAnnotation(fnLike.parent.type); + } + return propsNode; + }; + + const considerFunctionLike = (name, fnLike, pos) => { + if (!isCapitalized(name)) return; + const propsNode = propsNodeOf(fnLike); + if (!propsNode) return; // no resolvable type info — skip silently + localComponents.set(name, { propsNode, pos }); + }; + + const hasDefaultModifier = (node) => { + const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; + return Boolean( + mods?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) + ); + }; + + const visit = (node) => { + if (ts.isImportDeclaration(node) && node.importClause) { + const spec = node.moduleSpecifier.text; + const clause = node.importClause; + if (clause.name) { + imports.set(clause.name.text, { + spec, + importedName: 'default', + isDefault: true, + }); + } + if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { + for (const el of clause.namedBindings.elements) { + imports.set(el.name.text, { + spec, + importedName: (el.propertyName ?? el.name).text, + isDefault: false, + }); + } + } + } else if (ts.isInterfaceDeclaration(node) && node.name) { + types.set(node.name.text, node); + } else if (ts.isTypeAliasDeclaration(node) && node.name) { + types.set(node.name.text, node); + } else if (ts.isFunctionDeclaration(node)) { + if (node.name) { + considerFunctionLike(node.name.text, node, node.name.getStart(sf)); + if (hasDefaultModifier(node)) defaultAliasNames.add(node.name.text); + } else if (hasDefaultModifier(node)) { + // `export default function(props: Props) {}` — nameless. + const propsNode = propsNodeOf(node); + if (propsNode) anonymousDefault = { propsNode, pos: node.getStart(sf) }; + } + } else if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + if (!ts.isIdentifier(decl.name) || !decl.initializer) continue; + const init = unwrapComponentWrapper(decl.initializer); + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) { + considerFunctionLike(decl.name.text, init, decl.name.getStart(sf)); + } + } + } else if (ts.isExportAssignment(node) && !node.isExportEquals) { + if (ts.isIdentifier(node.expression)) { + // `export default Foo;` referring to a declaration seen above. + defaultAliasNames.add(node.expression.text); + } else { + // `export default (props: Props) => {...}` — nameless. + const init = unwrapComponentWrapper(node.expression); + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) { + const propsNode = propsNodeOf(init); + if (propsNode) + anonymousDefault = { propsNode, pos: node.getStart(sf) }; + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + + for (const [name, { propsNode, pos }] of localComponents) { + const entry = registerComponent(file, name, propsNode, pos); + if (defaultAliasNames.has(name)) { + components.set(componentKey(file, 'default'), entry); + } + } + if (anonymousDefault && !components.has(componentKey(file, 'default'))) { + registerComponent( + file, + 'default', + anonymousDefault.propsNode, + anonymousDefault.pos + ); + } +} + +// ---- Props member resolution --------------------------------------------- + +function stringLiteralUnionToSet(node) { + const out = new Set(); + const collect = (n) => { + if (ts.isLiteralTypeNode(n) && ts.isStringLiteral(n.literal)) { + out.add(n.literal.text); + return true; + } + if (ts.isUnionTypeNode(n)) return n.types.every(collect); + return false; + }; + return collect(node) ? out : null; +} + +function findNamedType(file, name) { + const local = typesByFile.get(file)?.get(name); + if (local) return { file, node: local }; + const imp = importsByFile.get(file)?.get(name); + if (!imp) return null; + const target = resolveModule(imp.spec); + if (!target) return null; + const targetSf = parse(target); + if (!targetSf || !typesByFile.has(target)) { + // Ensure the target has been through the declarations pass. + if (targetSf && !typesByFile.has(target)) declarationsPass(target, targetSf); + } + const node = typesByFile.get(target)?.get(imp.importedName); + return node ? { file: target, node } : null; +} + +function membersOfInterface(file, node, resolving) { + const own = new Set(); + for (const m of node.members) { + if ( + (ts.isPropertySignature(m) || ts.isMethodSignature(m)) && + (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) + ) { + own.add(m.name.text); + } + } + for (const clause of node.heritageClauses ?? []) { + for (const expr of clause.types) { + if (!ts.isIdentifier(expr.expression)) return INDETERMINATE; + const found = findNamedType(file, expr.expression.text); + if (!found) return INDETERMINATE; + const members = resolvePropsMembers(found.file, found.node, resolving); + if (members === INDETERMINATE) return INDETERMINATE; + for (const m of members) own.add(m); + } + } + return own; +} + +// Resolves a declaration (interface or type alias) to its member-name set. +function resolvePropsMembers(file, declNode, resolving) { + const key = `${file}#${declNode.name.text}`; + if (resolving.has(key)) return INDETERMINATE; // cycle guard + resolving.add(key); + try { + if (ts.isInterfaceDeclaration(declNode)) { + return membersOfInterface(file, declNode, resolving); + } + if (ts.isTypeAliasDeclaration(declNode)) { + return resolvePropsFromTypeNode(file, declNode.type, resolving); + } + return INDETERMINATE; + } finally { + resolving.delete(key); + } +} + +// Resolves an arbitrary type node (inline literal, named reference, Pick/Omit/ +// Partial/Required wrapper, or intersection) to a member-name set. +function resolvePropsFromTypeNode(file, node, resolving) { + if (!node) return INDETERMINATE; + + if (ts.isTypeLiteralNode(node)) { + const out = new Set(); + for (const m of node.members) { + if ( + (ts.isPropertySignature(m) || ts.isMethodSignature(m)) && + (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) + ) { + out.add(m.name.text); + } else { + return INDETERMINATE; // index signature, mapped type, etc. + } + } + return out; + } + + if (ts.isIntersectionTypeNode(node)) { + const out = new Set(); + for (const t of node.types) { + const members = resolvePropsFromTypeNode(file, t, resolving); + if (members === INDETERMINATE) return INDETERMINATE; + for (const m of members) out.add(m); + } + return out; + } + + if (ts.isParenthesizedTypeNode(node)) { + return resolvePropsFromTypeNode(file, node.type, resolving); + } + + if (ts.isTypeReferenceNode(node)) { + if (!ts.isIdentifier(node.typeName)) return INDETERMINATE; // qualified name + const name = node.typeName.text; + + if ( + (name === 'Partial' || name === 'Required' || name === 'Readonly') && + node.typeArguments?.length === 1 + ) { + return resolvePropsFromTypeNode(file, node.typeArguments[0], resolving); + } + if ( + (name === 'Pick' || name === 'Omit') && + node.typeArguments?.length === 2 + ) { + const base = resolvePropsFromTypeNode( + file, + node.typeArguments[0], + resolving + ); + if (base === INDETERMINATE) return INDETERMINATE; + const keys = stringLiteralUnionToSet(node.typeArguments[1]); + if (!keys) return INDETERMINATE; + const out = new Set(); + for (const m of base) { + const keep = name === 'Pick' ? keys.has(m) : !keys.has(m); + if (keep) out.add(m); + } + return out; + } + + const found = findNamedType(file, name); + if (!found) return INDETERMINATE; + return resolvePropsMembers(found.file, found.node, resolving); + } + + return INDETERMINATE; // union, mapped, conditional, indexed access, etc. +} + +// ---- Pass 2: usage --------------------------------------------------------- + +function hasNonTrivialChildren(jsxElement) { + return jsxElement.children.some((c) => { + if (ts.isJsxText(c)) return c.text.trim().length > 0; + return true; // JsxElement / JsxSelfClosingElement / JsxExpression / JsxFragment + }); +} + +function usagePass(file, sf) { + const imports = importsByFile.get(file); + + const resolveTag = (tagName) => { + if (!ts.isIdentifier(tagName) || !isCapitalized(tagName.text)) return null; + const name = tagName.text; + if (components.has(componentKey(file, name))) + return componentKey(file, name); + const imp = imports?.get(name); + if (!imp) return null; + const target = resolveModule(imp.spec); + if (!target) return null; + const targetName = imp.isDefault ? 'default' : imp.importedName; + const key = componentKey(target, targetName); + return components.has(key) ? key : null; + }; + + const recordUsage = (openingLike, key, jsxElementForChildren) => { + const entry = components.get(key); + entry.callSites += 1; + let sawChildrenContent = false; + for (const attr of openingLike.attributes.properties) { + if (ts.isJsxSpreadAttribute(attr)) { + entry.hasSpread = true; + continue; + } + if (ts.isJsxAttribute(attr)) { + const attrName = attr.name.getText(sf); + if (attrName === 'children') sawChildrenContent = true; + entry.usedProps.add(attrName); + } + } + if ( + !sawChildrenContent && + jsxElementForChildren && + hasNonTrivialChildren(jsxElementForChildren) + ) { + entry.usedProps.add('children'); + } + }; + + const visit = (node) => { + if (ts.isJsxSelfClosingElement(node)) { + const key = resolveTag(node.tagName); + if (key) recordUsage(node, key, null); + } else if (ts.isJsxElement(node)) { + const key = resolveTag(node.openingElement.tagName); + if (key) recordUsage(node.openingElement, key, node); + } + ts.forEachChild(node, visit); + }; + visit(sf); +} + +// ---- Run -------------------------------------------------------------- + +for (const file of files) { + const sf = parse(file); + if (sf) declarationsPass(file, sf); +} +for (const file of files) { + const sf = parse(file); + if (sf) usagePass(file, sf); +} + +// ---- Resolve Props members lazily, now that all declarations are known ---- + +const orphans = []; // { file, name, pos } +// A component can be reachable under more than one key (its own name, plus a +// `#default` alias) but both point at the same entry object — dedupe on that +// object identity so it's evaluated once. +for (const entry of new Set(components.values())) { + if (entry.callSites === 0 || entry.hasSpread) continue; + const members = resolvePropsFromTypeNode(entry.file, entry.propsTypeNode, new Set()); + if (members === INDETERMINATE) continue; + for (const prop of members) { + if (!entry.usedProps.has(prop)) { + orphans.push({ + file: entry.file, + name: `${entry.name === 'default' ? path.basename(entry.file).replace(/\.tsx?$/, '') : entry.name}.${prop}`, + pos: entry.declPos, + }); + } + } +} + +// ---- Merge into the Knip-shaped report ------------------------------------ + +let data; +try { + data = JSON.parse(readFileSync(jsonPath, 'utf8')); +} catch { + data = { issues: [] }; +} +if (!data || !Array.isArray(data.issues)) data = { issues: [] }; + +const issueByFile = new Map(data.issues.map((i) => [i.file, i])); +for (const o of orphans) { + let issue = issueByFile.get(o.file); + if (!issue) { + issue = { file: o.file }; + issueByFile.set(o.file, issue); + data.issues.push(issue); + } + (issue.orphanedProps ??= []).push({ name: o.name, pos: o.pos }); +} + +writeFileSync(jsonPath, JSON.stringify(data)); diff --git a/.github/workflows/health.yml b/.github/workflows/health.yml index 329c80600..e44dfcaf7 100644 --- a/.github/workflows/health.yml +++ b/.github/workflows/health.yml @@ -46,11 +46,13 @@ jobs: - name: Knip on PR head run: | git checkout --force --quiet ${{ github.event.pull_request.head.sha }} - # Stash the classifier so it survives the base checkout below - # (base doesn't have it on a PR that introduces this check). + # Stash the analysis scripts so they survive the base checkout + # below (base doesn't have them on a PR that introduces one). cp .github/scripts/knip-classify.mjs "$RUNNER_TEMP/classify.mjs" + cp .github/scripts/orphaned-props.mjs "$RUNNER_TEMP/orphaned-props.mjs" node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/head.json" || true node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/head.json" + node "$RUNNER_TEMP/orphaned-props.mjs" "$RUNNER_TEMP/head.json" - name: Knip on base (measured with the head's config) run: | @@ -61,8 +63,9 @@ jobs: # only source differences register. Falls back to base's config. git checkout --quiet ${{ github.event.pull_request.head.sha }} -- knip.json 2>/dev/null || true node_modules/.bin/knip --reporter json > "$RUNNER_TEMP/base.json" || true - # Classify against the base's checked-out source (files present now). + # Classify/scan against the base's checked-out source (files present now). node "$RUNNER_TEMP/classify.mjs" "$RUNNER_TEMP/base.json" + node "$RUNNER_TEMP/orphaned-props.mjs" "$RUNNER_TEMP/base.json" - name: Restore head run: git checkout --force --quiet ${{ github.event.pull_request.head.sha }}