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..15dd2c2c4 --- /dev/null +++ b/.github/scripts/knip-delta.mjs @@ -0,0 +1,231 @@ +#!/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', + 'orphanedProps', +]; +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', + orphanedProps: 'Orphaned props', +}; + +// 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', + orphanedProps: 'Declared on the component but no caller ever passes it', +}; + +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/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 new file mode 100644 index 000000000..e44dfcaf7 --- /dev/null +++ b/.github/workflows/health.yml @@ -0,0 +1,123 @@ +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 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: | + 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/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 }} + + - 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",