diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5c9a664 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Repository Instructions + +- Write repo-local Node tooling as TypeScript (`.ts`) and run it with + `node --experimental-transform-types`. +- Do not add `.mjs` tooling scripts. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c3dff8e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,171 @@ +# Contributing + +This document covers the repository details that matter when working on +Codescythe itself: the package layout, Rust boundaries, Bazel release graph, and +validation commands. + +## Architecture + +Codescythe is split into a small Rust analysis core, two runtime adapters, and +distribution packages. Source files are intentionally flattened at each crate or +package root instead of hidden under `src/` folders. + +```text +. +|-- codescythe.schema.json +|-- crates/ +| |-- codescythe/ # Rust core analysis, config loading, fix logic +| |-- codescythe_cli/ # Standalone CLI binary +| `-- codescythe_napi/ # Node-API shared library adapter +|-- packages/ +| |-- codescythe/ # Public npm package and TypeScript loader +| |-- codescythe-darwin-arm64/ +| |-- codescythe-linux-amd64/ +| `-- codescythe-linux-arm64/ +|-- tests/ +| `-- fixtures/ # Knip-style conformance fixtures +`-- tools/ + `-- ts.bzl # Minimal Gazelle TS mapping +``` + +### Core Crate + +`crates/codescythe` owns the analyzer. It loads `codescythe.json` or the +`codescythe` key in `package.json`, validates that config with the bundled JSON +Schema, walks the configured project globs, parses TypeScript/JavaScript with +Oxc, builds the import/export graph, and reports unused files, unused exports, +and unresolved imports. + +The public Rust API is intentionally narrow: + +- `codescythe::run(cwd, config_path)` returns an analysis report. +- `codescythe::run_and_fix(cwd, config_path)` applies supported removals and + returns a fix report. + +The core crate has no npm or CLI concerns. That keeps conformance tests and +future analysis work centered on one library boundary. + +### Runtime Adapters + +`crates/codescythe_cli` is a thin `clap` wrapper around the core crate. It +supports text and JSON output, exits with `1` when issues are found, and exits +with `2` for runtime/config errors. + +`crates/codescythe_napi` exposes the same core behavior to Node through N-API. +It returns JSON strings from Rust, while the public TypeScript loader parses +those strings into JavaScript objects. + +### Npm Package Boundary + +The pnpm workspace treats `packages/*` as public distribution boundaries. The +root `package.json` owns workspace imports and scripts; public packages own +their own `package.json` files. + +`@perplexity/codescythe` is the public npm package. Its TypeScript loader chooses +one optional native package from `process.platform` and `process.arch`: + +- `@perplexity/codescythe-darwin-arm64` +- `@perplexity/codescythe-linux-amd64` +- `@perplexity/codescythe-linux-arm64` + +The package entrypoints are TypeScript files and are executed with Node's +`--experimental-transform-types` support. The package CLI shim is also +TypeScript. + +## Build Graph + +Bazel is the source of truth for release artifacts. + +```text +//crates/codescythe + |-- used by //crates/codescythe_cli:codescythe + `-- used by //crates/codescythe_napi:codescythe_napi + +//crates/codescythe_cli:release_binaries + |-- codescythe-darwin-arm64 + |-- codescythe-linux-amd64 # musl static/static-pie + `-- codescythe-linux-arm64 # musl static + +//crates/codescythe_napi:release_nodes + |-- codescythe.darwin-arm64.node + |-- codescythe.linux-amd64.node # musl shared object + `-- codescythe.linux-arm64.node # musl shared object + +//packages/...:package + `-- copies the matching TypeScript loader/package files plus native output +``` + +The release transitions in `crates/codescythe_cli/release_binary.bzl` and +`crates/codescythe_napi/release_node.bzl` use `with_cfg` to force optimized +platform builds and select the LLVM cross toolchains. Linux CLI binaries use +musl targets for static artifacts. Linux N-API packages also use musl targets, +with `crt-static` disabled for the shared-library build so Rust emits a `.node` +shared object instead of dropping the `cdylib` output. + +## Development + +```sh +bazel test //... +bazel run //:gazelle +cargo test +``` + +Repo-local Node tooling should be written as TypeScript and run with Node's +`--experimental-transform-types` support. Do not add `.mjs` tooling scripts. + +Build release artifacts: + +```sh +bazel build //crates/codescythe_cli:release_binaries +bazel build //crates/codescythe_napi:release_nodes +bazel build //packages/codescythe:package //packages/codescythe-darwin-arm64:package //packages/codescythe-linux-amd64:package //packages/codescythe-linux-arm64:package +``` + +Run the colocated npm smoke test against unpacked package artifacts by setting +`CODESCYTHE_PACKAGE_DIR` and `CODESCYTHE_NATIVE_PACKAGE_DIR`, then running: + +```sh +pnpm test:npm +``` + +## Benchmarks + +The benchmark harness in `benchmarks/` uses pinned real-world source snapshots +from `microsoft/vscode`, `grafana/grafana`, and `elastic/kibana`. The fixtures +are declared as Bazel external repositories in `MODULE.bazel`, and the harness +uses the `benchmark` npm package to time a release Codescythe CLI build and, +when available, Knip with issue reporting limited to files and exports. + +```sh +pnpm benchmark +``` + +By default the benchmark fetches both fixtures through Bazel, builds +`codescythe` with Cargo before measuring, and compares against the workspace's +Knip dev dependency. Use `--fixture vscode`, `--fixture grafana`, or +`--fixture kibana` to run one fixture. Set `CODESCYTHE_BIN=/path/to/codescythe` +or `KNIP_BIN=/path/to/knip` to benchmark a specific binary. Pass `--skip-build` +when `target/release/codescythe` already exists, or `--skip-knip` to run only +Codescythe. + +Codescythe parses project files in parallel by default, capped at the machine's +available parallelism. Set `CODESCYTHE_PARSE_THREADS` to tune benchmark runs for +local filesystem behavior; `RAYON_NUM_THREADS` is also respected when the +Codescythe-specific variable is unset. + +## Tests And CI + +The Rust conformance test lives in `crates/codescythe` and uses the +`tests/fixtures/knip-export-basics` fixture. Npm smoke coverage is colocated with +the public package in `packages/codescythe/npm_smoke.ts` and runs through Mocha. + +GitHub Actions builds all release targets on macOS, uploads the package and +binary artifacts, then smoke-tests each triple on its native runner: + +- Darwin arm64 on `macos-15`. +- Linux amd64 on `ubuntu-24.04`. +- Linux arm64 on `ubuntu-24.04-arm`. + +The smoke jobs verify the npm package loader, direct native package loading, the +package CLI shim, the standalone static binary, fixture output, and that Linux +artifacts do not reference `GLIBC_` symbols. diff --git a/Cargo.lock b/Cargo.lock index 0816be4..1a4b1b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,7 @@ dependencies = [ "oxc_ast", "oxc_parser", "oxc_span", + "rayon", "serde", "serde_json", "tempfile", @@ -271,6 +272,31 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "ctor" version = "1.0.6" @@ -300,6 +326,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "email_address" version = "0.2.9" @@ -1307,6 +1339,26 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" diff --git a/Cargo.toml b/Cargo.toml index c4a4a2e..f83e684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ oxc_allocator = "0.126" oxc_ast = "0.126" oxc_parser = "0.126" oxc_span = "0.126" +rayon = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } tempfile = "3.23" diff --git a/MODULE.bazel b/MODULE.bazel index 40a7127..7df81e5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,6 +21,47 @@ bazel_dep(name = "rules_rs", version = "0.0.82") bazel_dep(name = "llvm", version = "0.8.3") bazel_dep(name = "with_cfg.bzl", version = "0.14.6") +benchmark_fixture_build_file = """ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "package_json", + srcs = ["package.json"], +) +""" + +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "benchmark_vscode", + build_file_content = benchmark_fixture_build_file, + sha256 = "2a13c215a62ef6a6858ce5d144e8956e847123151d0440185a5a1191c407d6bb", + strip_prefix = "vscode-9b7643f90393b9ad2c5d5cbbdad70fa928090009", + urls = [ + "https://github.com/microsoft/vscode/archive/9b7643f90393b9ad2c5d5cbbdad70fa928090009.tar.gz", + ], +) + +http_archive( + name = "benchmark_grafana", + build_file_content = benchmark_fixture_build_file, + sha256 = "1f8c2e414dc367cd687dc6e9edb6697753000a2a9b0065e345bc88e21b591e7c", + strip_prefix = "grafana-7709dc39cf8ee2de85c38b8943b208adf8a3c47c", + urls = [ + "https://github.com/grafana/grafana/archive/7709dc39cf8ee2de85c38b8943b208adf8a3c47c.tar.gz", + ], +) + +http_archive( + name = "benchmark_kibana", + build_file_content = benchmark_fixture_build_file, + sha256 = "e620e9505e6c035a24bd16e1b6ee4c6e7abd6a26d1d74ef665684927f3d6b2c8", + strip_prefix = "kibana-d706f62a04af1112db6b4dfef3c94955bdb98250", + urls = [ + "https://github.com/elastic/kibana/archive/d706f62a04af1112db6b4dfef3c94955bdb98250.tar.gz", + ], +) + node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node", dev_dependency = True) node.toolchain(node_version = "24.14.0") diff --git a/README.md b/README.md index d7be0f4..3a261db 100644 --- a/README.md +++ b/README.md @@ -17,152 +17,44 @@ for the common package and monorepo maintenance job where the project boundary i already known and the useful answer is deterministic: which TypeScript files and exports are unused, and which of those removals can be applied safely. -## Architecture - -Codescythe is split into a small Rust analysis core, two runtime adapters, and -distribution packages. Source files are intentionally flattened at each crate or -package root instead of hidden under `src/` folders. - -```text -. -|-- codescythe.schema.json -|-- crates/ -| |-- codescythe/ # Rust core analysis, config loading, fix logic -| |-- codescythe_cli/ # Standalone CLI binary -| `-- codescythe_napi/ # Node-API shared library adapter -|-- packages/ -| |-- codescythe/ # Public npm package and TypeScript loader -| |-- codescythe-darwin-arm64/ -| |-- codescythe-linux-amd64/ -| `-- codescythe-linux-arm64/ -|-- tests/ -| `-- fixtures/ # Knip-style conformance fixtures -`-- tools/ - `-- ts.bzl # Minimal Gazelle TS mapping -``` - -### Core Crate - -`crates/codescythe` owns the analyzer. It loads `codescythe.json` or the -`codescythe` key in `package.json`, validates that config with the bundled JSON -Schema, walks the configured project globs, parses TypeScript/JavaScript with -Oxc, builds the import/export graph, and reports unused files, unused exports, -and unresolved imports. - -The public Rust API is intentionally narrow: - -- `codescythe::run(cwd, config_path)` returns an analysis report. -- `codescythe::run_and_fix(cwd, config_path)` applies supported removals and - returns a fix report. - -The core crate has no npm or CLI concerns. That keeps conformance tests and -future analysis work centered on one library boundary. - -### Runtime Adapters - -`crates/codescythe_cli` is a thin `clap` wrapper around the core crate. It -supports text and JSON output, exits with `1` when issues are found, and exits -with `2` for runtime/config errors. - -`crates/codescythe_napi` exposes the same core behavior to Node through N-API. -It returns JSON strings from Rust, while the public TypeScript loader parses -those strings into JavaScript objects. - -### Npm Package Boundary - -The pnpm workspace treats `packages/*` as public distribution boundaries. The -root `package.json` owns workspace imports and scripts; public packages own -their own `package.json` files. - -`@perplexity/codescythe` is the public npm package. Its TypeScript loader chooses -one optional native package from `process.platform` and `process.arch`: - -- `@perplexity/codescythe-darwin-arm64` -- `@perplexity/codescythe-linux-amd64` -- `@perplexity/codescythe-linux-arm64` - -The package entrypoints are TypeScript files and are executed with Node's -`--experimental-transform-types` support. The package CLI shim is also -TypeScript. - -### Build Graph - -Bazel is the source of truth for release artifacts. - -```text -//crates/codescythe - |-- used by //crates/codescythe_cli:codescythe - `-- used by //crates/codescythe_napi:codescythe_napi - -//crates/codescythe_cli:release_binaries - |-- codescythe-darwin-arm64 - |-- codescythe-linux-amd64 # musl static/static-pie - `-- codescythe-linux-arm64 # musl static - -//crates/codescythe_napi:release_nodes - |-- codescythe.darwin-arm64.node - |-- codescythe.linux-amd64.node # musl shared object - `-- codescythe.linux-arm64.node # musl shared object - -//packages/...:package - `-- copies the matching TypeScript loader/package files plus native output -``` - -The release transitions in `crates/codescythe_cli/release_binary.bzl` and -`crates/codescythe_napi/release_node.bzl` use `with_cfg` to force optimized -platform builds and select the LLVM cross toolchains. Linux CLI binaries use -musl targets for static artifacts. Linux N-API packages also use musl targets, -with `crt-static` disabled for the shared-library build so Rust emits a `.node` -shared object instead of dropping the `cdylib` output. - -### Config +## Codescythe And Knip -The config schema lives at `codescythe.schema.json` and is compiled into the -core crate. Config can be provided as: - -- `codescythe.json` in the project root. -- A `codescythe` object in `package.json`. -- An explicit path passed with `--config`. - -Supported config fields are `entry`, `project`, `ignore`, -`includeEntryExports`, and `ignoreExportsUsedInFile`. +Codescythe takes a deliberately smaller slice of Knip's problem space. -### Tests And CI +| | Knip | Codescythe | +| --- | --- | --- | +| Primary scope | Broad JavaScript and TypeScript project hygiene: unused files, exports, dependencies, binaries, unresolved imports, and related issue types. | Focused TypeScript dead-code analysis: unused project files, unused exports, unresolved imports, and supported export removals. | +| Project discovery | Infers more from package metadata, workspaces, scripts, framework config, and built-in plugins. | Starts from explicit `entry` and `project` config, then follows the import/export graph. | +| Framework awareness | Designed for framework and tool integrations through plugins and compilers. | Intentionally avoids a framework plugin surface. | +| Best fit | Comprehensive audits where framework config, dependency hygiene, and workspace conventions matter. | Deterministic cleanup jobs where the TypeScript boundary is already known and repeatable graph behavior matters more than integration breadth. | -The Rust conformance test lives in `crates/codescythe` and uses the -`tests/fixtures/knip-export-basics` fixture. Npm smoke coverage is colocated with -the public package in `packages/codescythe/npm_smoke.ts` and runs through Mocha. +## Benchmarks -GitHub Actions builds all release targets on macOS, uploads the package and -binary artifacts, then smoke-tests each triple on its native runner: +The benchmark suite runs Codescythe and Knip against pinned real-world +TypeScript-heavy repositories fetched through Bazel. A local smoke run with +`--samples 1 --warmups 0` produced: -- Darwin arm64 on `macos-15`. -- Linux amd64 on `ubuntu-24.04`. -- Linux arm64 on `ubuntu-24.04-arm`. +| Fixture | TypeScript corpus | Codescythe | Knip | +| --- | ---: | ---: | ---: | +| `microsoft/vscode` | 10,213 TS/TSX files | 738.0ms | 5.76s | +| `grafana/grafana` | 8,733 TS/TSX files | 771.0ms | 9.29s | +| `elastic/kibana` | at least 29,280 TS/TSX files | 5.41s | 68.06s | -The smoke jobs verify the npm package loader, direct native package loading, the -package CLI shim, the standalone static binary, fixture output, and that Linux -artifacts do not reference `GLIBC_` symbols. +Run `pnpm benchmark` to measure the same fixtures locally. -## Development +## Config -```sh -bazel test //... -bazel run //:gazelle -cargo test -``` +The config schema lives at `codescythe.schema.json` and is compiled into the +core crate. Config can be provided as: -Build release artifacts: +- `codescythe.json` in the project root. +- A `codescythe` object in `package.json`. +- An explicit path passed with `--config`. -```sh -bazel build //crates/codescythe_cli:release_binaries -bazel build //crates/codescythe_napi:release_nodes -bazel build //packages/codescythe:package //packages/codescythe-darwin-arm64:package //packages/codescythe-linux-amd64:package //packages/codescythe-linux-arm64:package -``` +Supported config fields are `entry`, `project`, `ignore`, +`includeEntryExports`, and `ignoreExportsUsedInFile`. -Run the colocated npm smoke test against unpacked package artifacts by setting -`CODESCYTHE_PACKAGE_DIR` and `CODESCYTHE_NATIVE_PACKAGE_DIR`, then running: +## Contributing -```sh -pnpm test:npm -``` +See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository layout, architecture, +build graph, benchmarks, release artifacts, and local validation commands. diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..838a516 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,41 @@ +# Benchmarks + +`run.ts` uses Benchmark.js to time analyzer CLI runs against pinned real-world +source snapshots fetched through Bazel: + +- `microsoft/vscode` at `9b7643f90393b9ad2c5d5cbbdad70fa928090009`. +- `grafana/grafana` at `7709dc39cf8ee2de85c38b8943b208adf8a3c47c`. +- `elastic/kibana` at `d706f62a04af1112db6b4dfef3c94955bdb98250`. + +Run the default benchmark. It builds Codescythe, fetches all fixtures through +`MODULE.bazel`, and compares against the workspace's Knip dev dependency when +available. + +```sh +pnpm benchmark +``` + +Useful options: + +```sh +node --experimental-transform-types benchmarks/run.ts --fixture vscode --samples 7 +node --experimental-transform-types benchmarks/run.ts --fixture grafana --samples 7 +node --experimental-transform-types benchmarks/run.ts --fixture kibana --samples 7 +node --experimental-transform-types benchmarks/run.ts --skip-build --skip-knip +CODESCYTHE_BIN=/tmp/codescythe KNIP_BIN=/tmp/knip pnpm benchmark +CODESCYTHE_PARSE_THREADS=4 pnpm benchmark +``` + +Codescythe is measured with `--json --compact-json --directory +--config `. Knip is measured only when available, with +reporting limited to file, value-export, and type-export issues so the +comparison stays close to Codescythe's scope. The shared benchmark config treats +all TypeScript-family source files as both `entry` and `project`, which makes +this a stable whole-corpus parse/import/export benchmark instead of a +project-specific unused-file audit. The repo installs Knip as a dev +dependency; set `KNIP_BIN` to compare against a different Knip binary. + +Codescythe parses files in parallel by default, capped at the machine's available +parallelism. Set `CODESCYTHE_PARSE_THREADS` to tune this for local filesystem +behavior; `RAYON_NUM_THREADS` is also respected when the Codescythe-specific +variable is unset. diff --git a/benchmarks/run.ts b/benchmarks/run.ts new file mode 100644 index 0000000..2ea6673 --- /dev/null +++ b/benchmarks/run.ts @@ -0,0 +1,537 @@ +#!/usr/bin/env -S node --experimental-transform-types + +const Benchmark = require('benchmark'); +const { spawnSync } = require('node:child_process'); +const { + existsSync, + mkdtempSync, + realpathSync, + rmSync, + writeFileSync, +} = require('node:fs'); +const { tmpdir } = require('node:os'); +const path = require('node:path'); + +type FixtureName = 'vscode' | 'grafana' | 'kibana'; +type FixtureSelection = FixtureName | 'all'; + +type Fixture = { + name: FixtureName; + label: string; + repo: string; + commit: string; + markerTarget: string; + trackedFiles: number; + tsFiles: number; + extraFiles?: string; + treeTruncated?: boolean; +}; + +type Options = { + fixture: FixtureSelection; + samples: number; + warmups: number; + skipBuild: boolean; + skipKnip: boolean; + codescytheBin?: string; + knipBin?: string; + help: boolean; +}; + +type Tool = { + label: string; + command: string; + args: string[]; + okStatuses: Set; +}; + +type ResultRow = { + label: string; + meanMs: number; + rme: number; + samples: number; + hz: number; +}; + +const scriptDir = __dirname; +const repoRoot = path.resolve(scriptDir, '..'); +const defaultCodescytheBin = path.join( + repoRoot, + 'target', + 'release', + process.platform === 'win32' ? 'codescythe.exe' : 'codescythe', +); + +const sourcePatterns = ['**/*.{ts,tsx,mts,cts}']; +const ignorePatterns = [ + '**/*.d.ts', + '**/__fixtures__/**', + '**/*fixture*/**', + '**/*fixtures*/**', + '**/fixtures/**', + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/coverage/**', + '**/vendor/**', + '**/.yarn/**', + '**/.git/**', +]; + +const fixtures: Fixture[] = [ + { + name: 'vscode', + label: 'VS Code', + repo: 'microsoft/vscode', + commit: '9b7643f90393b9ad2c5d5cbbdad70fa928090009', + markerTarget: '@benchmark_vscode//:package_json', + trackedFiles: 14689, + tsFiles: 10213, + }, + { + name: 'grafana', + label: 'Grafana', + repo: 'grafana/grafana', + commit: '7709dc39cf8ee2de85c38b8943b208adf8a3c47c', + markerTarget: '@benchmark_grafana//:package_json', + trackedFiles: 21680, + tsFiles: 8733, + extraFiles: '5,955 Go files', + }, + { + name: 'kibana', + label: 'Kibana', + repo: 'elastic/kibana', + commit: 'd706f62a04af1112db6b4dfef3c94955bdb98250', + markerTarget: '@benchmark_kibana//:package_json', + trackedFiles: 43905, + tsFiles: 29280, + treeTruncated: true, + }, +]; + +let executionRoot: string | undefined; +let outputBase: string | undefined; + +const options = parseArgs(process.argv.slice(2)); + +if (options.help) { + printHelp(); + process.exit(0); +} + +const selectedFixtures = selectFixtures(options.fixture); +const configRoot = mkdtempSync(path.join(tmpdir(), 'codescythe-benchmark-config-')); + +try { + const codescytheBin = resolveCodescytheBin(options); + const knipBin = options.skipKnip ? undefined : resolveKnipBin(options); + + for (const fixture of selectedFixtures) { + const fixtureRoot = resolveFixtureRoot(fixture); + const configPath = writeFixtureConfig(configRoot, fixture); + const tools = createTools(fixtureRoot, configPath, codescytheBin, knipBin); + const rows = measureTools(tools, options); + printSummary(fixture, fixtureRoot, options, rows, knipBin); + } +} finally { + rmSync(configRoot, { recursive: true, force: true }); +} + +function parseArgs(args: string[]): Options { + const parsed: Options = { + fixture: 'all', + samples: 5, + warmups: 1, + skipBuild: false, + skipKnip: false, + codescytheBin: process.env.CODESCYTHE_BIN, + knipBin: process.env.KNIP_BIN, + help: false, + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--') { + continue; + } else if (arg === '--fixture') { + parsed.fixture = parseFixtureSelection(args[++index]); + } else if (arg === '--samples' || arg === '--runs') { + parsed.samples = parsePositiveInt(args[++index], arg); + } else if (arg === '--warmups') { + parsed.warmups = parseNonNegativeInt(args[++index], '--warmups'); + } else if (arg === '--skip-build') { + parsed.skipBuild = true; + } else if (arg === '--skip-knip') { + parsed.skipKnip = true; + } else if (arg === '--codescythe-bin') { + parsed.codescytheBin = path.resolve(requireValue(args[++index], '--codescythe-bin')); + } else if (arg === '--knip-bin') { + parsed.knipBin = path.resolve(requireValue(args[++index], '--knip-bin')); + } else if (arg === '--help' || arg === '-h') { + parsed.help = true; + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + return parsed; +} + +function parseFixtureSelection(value: string | undefined): FixtureSelection { + const fixture = requireValue(value, '--fixture'); + if ( + fixture === 'all' || + fixture === 'vscode' || + fixture === 'grafana' || + fixture === 'kibana' + ) { + return fixture; + } + throw new Error(`--fixture must be one of: all, vscode, grafana, kibana`); +} + +function requireValue(value: string | undefined, flag: string): string { + if (!value) { + throw new Error(`${flag} requires a value`); + } + return value; +} + +function parsePositiveInt(value: string | undefined, flag: string): number { + return parseMinInt(value, flag, 1); +} + +function parseNonNegativeInt(value: string | undefined, flag: string): number { + return parseMinInt(value, flag, 0); +} + +function parseMinInt(value: string | undefined, flag: string, min: number): number { + const parsed = Number.parseInt(requireValue(value, flag), 10); + if (!Number.isSafeInteger(parsed) || parsed < min) { + throw new Error(`${flag} must be an integer greater than or equal to ${min}`); + } + return parsed; +} + +function selectFixtures(selection: FixtureSelection): Fixture[] { + if (selection === 'all') { + return fixtures; + } + return fixtures.filter(fixture => fixture.name === selection); +} + +function writeFixtureConfig(directory: string, fixture: Fixture): string { + const configPath = path.join(directory, `${fixture.name}.json`); + writeJson(configPath, { + entry: sourcePatterns, + project: sourcePatterns, + ignore: ignorePatterns, + includeEntryExports: true, + ignoreExportsUsedInFile: false, + }); + return configPath; +} + +function writeJson(filePath: string, value: unknown) { + writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function createTools( + fixtureRoot: string, + configPath: string, + codescytheBin: string, + knipBin: string | undefined, +): Tool[] { + const tools: Tool[] = [ + { + label: 'codescythe', + command: codescytheBin, + args: [ + '--json', + '--compact-json', + '--directory', + fixtureRoot, + '--config', + configPath, + ], + okStatuses: new Set([0, 1]), + }, + ]; + + if (knipBin) { + tools.push({ + label: 'knip', + command: knipBin, + args: [ + '--no-progress', + '--no-config-hints', + '--no-exit-code', + '--reporter', + 'json', + '--include', + 'files,exports,types', + '--directory', + fixtureRoot, + '--config', + configPath, + ], + okStatuses: new Set([0]), + }); + } + + return tools; +} + +function resolveCodescytheBin(parsed: Options): string { + if (parsed.codescytheBin) { + assertExecutable(parsed.codescytheBin, 'Codescythe binary'); + return parsed.codescytheBin; + } + + if (!parsed.skipBuild) { + const build = spawnSync('cargo', ['build', '--release', '-p', 'codescythe_cli'], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'inherit', 'pipe'], + }); + if (build.status !== 0) { + throw new Error(`cargo build failed:\n${build.stderr}`); + } + } + + assertExecutable(defaultCodescytheBin, 'Codescythe release binary'); + return defaultCodescytheBin; +} + +function resolveKnipBin(parsed: Options): string | undefined { + if (parsed.knipBin) { + assertExecutable(parsed.knipBin, 'Knip binary'); + return parsed.knipBin; + } + + const localBin = path.join( + repoRoot, + 'node_modules', + '.bin', + process.platform === 'win32' ? 'knip.cmd' : 'knip', + ); + if (canRun(localBin, ['--version'])) { + return localBin; + } + + if (canRun('knip', ['--version'])) { + return 'knip'; + } + + return undefined; +} + +function assertExecutable(command: string, label: string) { + if (!existsSync(command)) { + throw new Error(`${label} not found at ${command}`); + } +} + +function canRun(command: string, args: string[]) { + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + stdio: 'ignore', + }); + return result.status === 0; +} + +function resolveFixtureRoot(fixture: Fixture): string { + const markerPath = bazelStdout([ + 'cquery', + fixture.markerTarget, + '--output=files', + '--noshow_progress', + ]) + .split(/\r?\n/) + .map(line => line.trim()) + .filter(Boolean) + .find(line => line.endsWith('package.json')); + + if (!markerPath) { + throw new Error(`Bazel did not return package.json for ${fixture.markerTarget}`); + } + + const absoluteMarkerPath = path.isAbsolute(markerPath) + ? markerPath + : markerPath.startsWith('external/') + ? path.resolve(getOutputBase(), markerPath) + : path.resolve(getExecutionRoot(), markerPath); + if (!existsSync(absoluteMarkerPath)) { + throw new Error(`Bazel fixture marker does not exist: ${absoluteMarkerPath}`); + } + return realpathSync(path.dirname(absoluteMarkerPath)); +} + +function getExecutionRoot(): string { + if (!executionRoot) { + executionRoot = bazelStdout(['info', 'execution_root', '--noshow_progress']); + } + return executionRoot; +} + +function getOutputBase(): string { + if (!outputBase) { + outputBase = bazelStdout(['info', 'output_base', '--noshow_progress']); + } + return outputBase; +} + +function bazelStdout(args: string[]): string { + const result = spawnSync('bazel', args, { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + if (result.status !== 0) { + throw new Error( + `bazel ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}:\n` + + result.stderr, + ); + } + return result.stdout.trim(); +} + +function measureTools(tools: Tool[], parsed: Options): ResultRow[] { + for (const tool of tools) { + for (let index = 0; index < parsed.warmups; index += 1) { + runTool(tool); + } + } + + const suite = new Benchmark.Suite(); + for (const tool of tools) { + suite.add(tool.label, { + minSamples: parsed.samples, + fn: () => runTool(tool), + }); + } + + suite.run({ async: false }); + + const rows: ResultRow[] = []; + suite.forEach(bench => { + if (bench.error) { + throw bench.error; + } + rows.push({ + label: bench.name, + meanMs: bench.stats.mean * 1000, + rme: bench.stats.rme, + samples: bench.stats.sample.length, + hz: bench.hz, + }); + }); + return rows; +} + +function runTool(tool: Tool) { + const result = spawnSync(tool.command, tool.args, { + cwd: repoRoot, + encoding: 'utf8', + env: { + ...process.env, + CI: '1', + NO_COLOR: '1', + }, + stdio: ['ignore', 'ignore', 'pipe'], + }); + + if (!tool.okStatuses.has(result.status ?? -1)) { + throw new Error( + `${tool.label} failed with exit code ${result.status ?? 'unknown'}:\n${result.stderr}`, + ); + } +} + +function printSummary( + fixture: Fixture, + fixtureRoot: string, + parsed: Options, + rows: ResultRow[], + knipBin: string | undefined, +) { + console.log(`Fixture: ${fixture.label} (${fixture.repo} @ ${fixture.commit.slice(0, 12)})`); + console.log(`Root: ${fixtureRoot}`); + console.log(`Corpus: ${formatCorpus(fixture)}`); + console.log(`Config: entry/project ${sourcePatterns.join(', ')}`); + console.log(`Runs: ${parsed.samples} minimum samples, ${parsed.warmups} warmup runs`); + console.log(''); + console.log(formatTable(rows)); + + if (parsed.skipKnip) { + console.log('\nKnip: skipped by --skip-knip'); + } else if (!knipBin) { + console.log('\nKnip: skipped; run pnpm install, set KNIP_BIN, or put knip on PATH to compare.'); + } + + console.log(''); +} + +function formatCorpus(fixture: Fixture) { + const prefix = fixture.treeTruncated ? 'at least ' : ''; + const parts = [ + `${prefix}${formatCount(fixture.trackedFiles)} tracked files`, + `${prefix}${formatCount(fixture.tsFiles)} TS/TSX files benchmarked`, + ]; + if (fixture.extraFiles) { + parts.push(fixture.extraFiles); + } + return parts.join(', '); +} + +function formatCount(value: number) { + return value.toLocaleString('en-US'); +} + +function formatTable(rows: ResultRow[]) { + const table = [ + ['tool', 'mean', 'rme', 'samples', 'ops/sec'], + ...rows.map(row => [ + row.label, + formatMs(row.meanMs), + `+/-${row.rme.toFixed(2)}%`, + row.samples.toString(), + row.hz.toFixed(row.hz >= 100 ? 1 : 2), + ]), + ]; + const widths = table[0].map((_, column) => + Math.max(...table.map(row => row[column].length)), + ); + + return table + .map((row, index) => { + const line = row + .map((cell, column) => cell.padEnd(widths[column])) + .join(' '); + return index === 0 + ? `${line}\n${widths.map(width => '-'.repeat(width)).join(' ')}` + : line; + }) + .join('\n'); +} + +function formatMs(value: number) { + return `${value.toFixed(1)}ms`; +} + +function printHelp() { + console.log(`Usage: node --experimental-transform-types benchmarks/run.ts [options] + +Options: + --fixture Fixture to benchmark: all, vscode, grafana, kibana (default: all) + --samples Minimum Benchmark.js samples per tool (default: 5) + --warmups Warmup runs per tool (default: 1) + --skip-build Use target/release/codescythe without rebuilding + --skip-knip Measure Codescythe only + --codescythe-bin Use a specific Codescythe binary + --knip-bin Use a specific Knip binary + -h, --help Show this help text +`); +} diff --git a/crates/codescythe/BUILD.bazel b/crates/codescythe/BUILD.bazel index f7356fc..d073471 100644 --- a/crates/codescythe/BUILD.bazel +++ b/crates/codescythe/BUILD.bazel @@ -10,6 +10,7 @@ COMMON_DEPS = [ "@crates//:oxc_ast", "@crates//:oxc_parser", "@crates//:oxc_span", + "@crates//:rayon", "@crates//:serde", "@crates//:serde_json", "@crates//:thiserror", diff --git a/crates/codescythe/Cargo.toml b/crates/codescythe/Cargo.toml index 28e4450..58ce667 100644 --- a/crates/codescythe/Cargo.toml +++ b/crates/codescythe/Cargo.toml @@ -19,6 +19,7 @@ oxc_allocator.workspace = true oxc_ast.workspace = true oxc_parser.workspace = true oxc_span.workspace = true +rayon.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/crates/codescythe/analyze.rs b/crates/codescythe/analyze.rs index 8e3ac1a..c91278c 100644 --- a/crates/codescythe/analyze.rs +++ b/crates/codescythe/analyze.rs @@ -1,7 +1,8 @@ use std::{ - collections::{BTreeMap, BTreeSet, VecDeque}, - fs, - path::{Path, PathBuf}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, + env, fs, + path::{Component, Path, PathBuf}, + thread, }; use anyhow::{Context, Result}; @@ -11,11 +12,20 @@ use oxc_allocator::Allocator; use oxc_ast::ast::*; use oxc_parser::{Parser, ParserReturn}; use oxc_span::{GetSpan, SourceType, Span}; +use rayon::prelude::*; use serde::{Deserialize, Serialize}; use walkdir::{DirEntry, WalkDir}; use crate::CodescytheConfig; +const PARSE_THREADS_ENV: &str = "CODESCYTHE_PARSE_THREADS"; +const RAYON_THREADS_ENV: &str = "RAYON_NUM_THREADS"; + +type PathIndex = HashMap; +type UsedFiles = HashSet; +type UsedExports = HashMap>; +type UnresolvedImports = HashMap>; + #[derive(Debug, Clone, Copy, Default)] pub struct AnalysisOptions { pub include_unreachable_exports: bool, @@ -81,26 +91,30 @@ pub fn analyze_path( .with_context(|| format!("failed to resolve {}", cwd.display()))?; let project_files = discover_project_files(&cwd, config)?; let entry_files = discover_entry_files(&cwd, config, &project_files)?; - let entry_set = entry_files.iter().cloned().collect::>(); + let entry_set = entry_files.iter().cloned().collect::>(); - let mut files = Vec::with_capacity(project_files.len()); - for path in &project_files { - files.push(parse_file(&cwd, path)?); - } + let files = parse_project_files(&cwd, &project_files)?; let index_by_path = files .iter() .enumerate() .map(|(index, file)| (file.path.clone(), index)) - .collect::>(); + .collect::>(); + let index_by_relative = files + .iter() + .enumerate() + .map(|(index, file)| (file.relative.clone(), index)) + .collect::(); - let mut used_files = BTreeSet::::new(); - let mut used_exports = BTreeMap::>::new(); - let mut unresolved = BTreeMap::>::new(); + let mut entry_indexes = HashSet::::new(); + let mut used_files = UsedFiles::new(); + let mut used_exports = UsedExports::new(); + let mut unresolved = UnresolvedImports::new(); let mut queue = VecDeque::::new(); for entry in &entry_set { if let Some(index) = index_by_path.get(entry) { + entry_indexes.insert(*index); if used_files.insert(*index) { queue.push_back(*index); } @@ -109,10 +123,10 @@ pub fn analyze_path( while let Some(index) = queue.pop_front() { let file = &files[index]; - let public_entry = entry_set.contains(&file.path) && !config.include_entry_exports; + let public_entry = entry_indexes.contains(&index) && !config.include_entry_exports; for import in &file.imports { - match resolve_import(&cwd, &file.path, &import.source, &index_by_path) { + match resolve_import(&file.relative, &import.source, &index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -131,7 +145,7 @@ pub fn analyze_path( } for source in &file.side_effect_imports { - match resolve_import(&cwd, &file.path, source, &index_by_path) { + match resolve_import(&file.relative, source, &index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -149,12 +163,11 @@ pub fn analyze_path( for (local, member) in &file.member_uses { if let Some(source) = file.namespace_imports.get(local) { mark_member_import( - &cwd, - &file.path, + &file.relative, source, member, &files, - &index_by_path, + &index_by_relative, &mut used_files, &mut used_exports, &mut queue, @@ -165,17 +178,16 @@ pub fn analyze_path( if let Some(named) = file.named_imports.get(local) { if let Some(target) = - resolve_import(&cwd, &file.path, &named.source, &index_by_path) + resolve_import(&file.relative, &named.source, &index_by_relative) { if let Some(export) = files[target].exports.get(&named.imported) { if let Some(namespace_source) = &export.namespace_source { mark_member_import( - &cwd, - &files[target].path, + &files[target].relative, namespace_source, member, &files, - &index_by_path, + &index_by_relative, &mut used_files, &mut used_exports, &mut queue, @@ -191,10 +203,9 @@ pub fn analyze_path( if public_entry { for export in file.exports.values() { mark_reexport( - &cwd, file, export, - &index_by_path, + &index_by_relative, &mut used_files, &mut used_exports, &mut queue, @@ -203,11 +214,10 @@ pub fn analyze_path( } for source in &file.reexport_all { mark_all_exports( - &cwd, file, source, &files, - &index_by_path, + &index_by_relative, &mut used_files, &mut used_exports, &mut queue, @@ -220,10 +230,9 @@ pub fn analyze_path( for export_name in current_used_exports { if let Some(export) = file.exports.get(&export_name) { mark_reexport( - &cwd, file, export, - &index_by_path, + &index_by_relative, &mut used_files, &mut used_exports, &mut queue, @@ -236,7 +245,7 @@ pub fn analyze_path( let mut issues = Issues::default(); for (index, file) in files.iter().enumerate() { - let is_entry = entry_set.contains(&file.path); + let is_entry = entry_indexes.contains(&index); let is_used = used_files.contains(&index); if !is_used && !is_entry { @@ -264,7 +273,6 @@ pub fn analyze_path( .as_ref() .is_some_and(|local| file.local_references.contains(local)); if !used_by_import && !used_in_file { - let (line, col) = line_col(&file.source, export.name_span.start); issues .exports .entry(file.relative.clone()) @@ -274,8 +282,8 @@ pub fn analyze_path( SymbolIssue { symbol: name.clone(), kind: export.kind, - line, - col, + line: export.line, + col: export.col, span: (export.remove_span.start, export.remove_span.end), }, ); @@ -285,7 +293,11 @@ pub fn analyze_path( issues.unresolved = unresolved .into_iter() - .map(|(file, imports)| (file, imports.into_iter().collect())) + .map(|(file, imports)| { + let mut imports = imports.into_iter().collect::>(); + imports.sort(); + (file, imports) + }) .collect(); let counters = Counters { @@ -445,64 +457,134 @@ fn parse_file(cwd: &Path, path: &Path) -> Result { anyhow::bail!("failed to parse {}:\n{}", path.display(), rendered); } - let mut visitor = FileVisitor::new(cwd, path, source.clone()); + let mut visitor = FileVisitor::new(cwd, path); visitor.visit_program(&program); - Ok(visitor.finish()) + let mut file = visitor.finish(); + for export in file.exports.values_mut() { + (export.line, export.col) = line_col(&source, export.name_span.start); + } + Ok(file) +} + +fn parse_project_files(cwd: &Path, project_files: &[PathBuf]) -> Result> { + let threads = parse_thread_count(); + if threads <= 1 { + return project_files + .iter() + .map(|path| parse_file(cwd, path)) + .collect(); + } + + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .context("failed to build parse thread pool")? + .install(|| { + project_files + .par_iter() + .map(|path| parse_file(cwd, path)) + .collect() + }) +} + +fn parse_thread_count() -> usize { + if let Some(threads) = env_thread_count(PARSE_THREADS_ENV) { + return threads; + } + if let Some(threads) = env_thread_count(RAYON_THREADS_ENV) { + return threads; + } + + thread::available_parallelism() + .map(usize::from) + .unwrap_or(1) +} + +fn env_thread_count(name: &str) -> Option { + env::var(name) + .ok()? + .parse::() + .ok() + .map(|count| count.max(1)) } fn resolve_import( - cwd: &Path, - from: &Path, + from_relative: &str, specifier: &str, - index_by_path: &BTreeMap, + index_by_relative: &PathIndex, ) -> Option { if !specifier.starts_with('.') { return None; } - let base = from.parent()?.join(specifier); - let mut candidates = Vec::new(); - candidates.push(base.clone()); + let base = normalize_import_path(from_relative, specifier)?; + if let Some(index) = index_by_relative.get(&base) { + return Some(*index); + } + for ext in ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs"] { - candidates.push(base.with_extension(ext)); + let candidate = with_extension(&base, ext); + if let Some(index) = index_by_relative.get(&candidate) { + return Some(*index); + } } for ext in ["ts", "tsx", "js", "jsx"] { - candidates.push(base.join(format!("index.{ext}"))); + let candidate = format!("{base}/index.{ext}"); + if let Some(index) = index_by_relative.get(&candidate) { + return Some(*index); + } } - candidates - .into_iter() - .filter_map(|candidate| normalize_existing(cwd, &candidate)) - .find_map(|candidate| index_by_path.get(&candidate).copied()) + None } -fn normalize_existing(cwd: &Path, path: &Path) -> Option { - if path.exists() { - path.canonicalize().ok() +fn normalize_import_path(from_relative: &str, specifier: &str) -> Option { + let parent = from_relative + .rsplit_once('/') + .map_or("", |(parent, _)| parent); + let combined = if parent.is_empty() { + specifier.to_string() } else { - let normalized = cwd.join(path.strip_prefix(cwd).ok()?); - if normalized.exists() { - normalized.canonicalize().ok() - } else { - None + format!("{parent}/{specifier}") + }; + normalize_relative_components(&combined) +} + +fn normalize_relative_components(path: &str) -> Option { + let mut parts = Vec::new(); + for component in Path::new(path).components() { + match component { + Component::Normal(part) => parts.push(part.to_string_lossy().to_string()), + Component::CurDir => {} + Component::ParentDir => { + parts.pop()?; + } + Component::Prefix(_) | Component::RootDir => return None, } } + Some(parts.join("/")) +} + +fn with_extension(path: &str, extension: &str) -> String { + Path::new(path) + .with_extension(extension) + .to_string_lossy() + .replace('\\', "/") } fn mark_member_import( - cwd: &Path, - from: &Path, + from_relative: &str, source: &str, member: &str, files: &[FileData], - index_by_path: &BTreeMap, - used_files: &mut BTreeSet, - used_exports: &mut BTreeMap>, + index_by_relative: &PathIndex, + used_files: &mut UsedFiles, + used_exports: &mut UsedExports, queue: &mut VecDeque, - unresolved: &mut BTreeMap>, + unresolved: &mut UnresolvedImports, importer_relative: &str, ) { - match resolve_import(cwd, from, source, index_by_path) { + match resolve_import(from_relative, source, index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -514,12 +596,11 @@ fn mark_member_import( if let Some(export) = files[target].exports.get(member) { if let Some(namespace_source) = &export.namespace_source { mark_member_import( - cwd, - &files[target].path, + &files[target].relative, namespace_source, member, files, - index_by_path, + index_by_relative, used_files, used_exports, queue, @@ -539,17 +620,16 @@ fn mark_member_import( } fn mark_reexport( - cwd: &Path, file: &FileData, export: &ExportInfo, - index_by_path: &BTreeMap, - used_files: &mut BTreeSet, - used_exports: &mut BTreeMap>, + index_by_relative: &PathIndex, + used_files: &mut UsedFiles, + used_exports: &mut UsedExports, queue: &mut VecDeque, - unresolved: &mut BTreeMap>, + unresolved: &mut UnresolvedImports, ) { if let (Some(source), Some(name)) = (&export.reexport_source, &export.reexport_name) { - match resolve_import(cwd, &file.path, source, index_by_path) { + match resolve_import(&file.relative, source, index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -566,7 +646,7 @@ fn mark_reexport( } if let Some(source) = &export.namespace_source { - match resolve_import(cwd, &file.path, source, index_by_path) { + match resolve_import(&file.relative, source, index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -583,17 +663,16 @@ fn mark_reexport( } fn mark_all_exports( - cwd: &Path, file: &FileData, source: &str, files: &[FileData], - index_by_path: &BTreeMap, - used_files: &mut BTreeSet, - used_exports: &mut BTreeMap>, + index_by_relative: &PathIndex, + used_files: &mut UsedFiles, + used_exports: &mut UsedExports, queue: &mut VecDeque, - unresolved: &mut BTreeMap>, + unresolved: &mut UnresolvedImports, ) { - match resolve_import(cwd, &file.path, source, index_by_path) { + match resolve_import(&file.relative, source, index_by_relative) { Some(target) => { if used_files.insert(target) { queue.push_back(target); @@ -615,7 +694,6 @@ fn mark_all_exports( struct FileData { path: PathBuf, relative: String, - source: String, exports: BTreeMap, imports: Vec, side_effect_imports: Vec, @@ -631,6 +709,8 @@ struct ExportInfo { kind: ExportKind, local_name: Option, name_span: Span, + line: usize, + col: usize, remove_span: Span, reexport_source: Option, reexport_name: Option, @@ -652,7 +732,6 @@ struct NamedImport { struct FileVisitor { path: PathBuf, relative: String, - source: String, exports: BTreeMap, imports: Vec, side_effect_imports: Vec, @@ -664,11 +743,10 @@ struct FileVisitor { } impl FileVisitor { - fn new(cwd: &Path, path: &Path, source: String) -> Self { + fn new(cwd: &Path, path: &Path) -> Self { Self { path: path.to_path_buf(), relative: relative_path(cwd, path), - source, exports: BTreeMap::new(), imports: Vec::new(), side_effect_imports: Vec::new(), @@ -684,7 +762,6 @@ impl FileVisitor { FileData { path: self.path, relative: self.relative, - source: self.source, exports: self.exports, imports: self.imports, side_effect_imports: self.side_effect_imports, @@ -710,6 +787,8 @@ impl FileVisitor { kind, local_name, name_span, + line: 0, + col: 0, remove_span, reexport_source: None, reexport_name: None, @@ -733,6 +812,8 @@ impl FileVisitor { kind, local_name: None, name_span, + line: 0, + col: 0, remove_span, reexport_source: Some(source), reexport_name: Some(local), @@ -851,6 +932,8 @@ impl<'a> Visit<'a> for FileVisitor { kind: export_kind(declaration.export_kind), local_name: None, name_span: exported.span(), + line: 0, + col: 0, remove_span: declaration.span, reexport_source: None, reexport_name: None, diff --git a/crates/codescythe_cli/main.rs b/crates/codescythe_cli/main.rs index c688b8c..a24b5c3 100644 --- a/crates/codescythe_cli/main.rs +++ b/crates/codescythe_cli/main.rs @@ -17,6 +17,9 @@ struct Args { #[arg(long)] json: bool, + + #[arg(long, requires = "json")] + compact_json: bool, } fn main() -> ExitCode { @@ -43,7 +46,11 @@ fn run() -> Result { if args.fix { let result = codescythe::run_and_fix(&cwd, config)?; if args.json { - println!("{}", serde_json::to_string_pretty(&result)?); + if args.compact_json { + println!("{}", serde_json::to_string(&result)?); + } else { + println!("{}", serde_json::to_string_pretty(&result)?); + } } else { println!( "Removed {} unused exports from {} files", @@ -58,7 +65,11 @@ fn run() -> Result { let analysis = codescythe::run(&cwd, config)?; if args.json { - println!("{}", serde_json::to_string_pretty(&analysis)?); + if args.compact_json { + println!("{}", serde_json::to_string(&analysis)?); + } else { + println!("{}", serde_json::to_string_pretty(&analysis)?); + } } else { print_text_report(&analysis); } diff --git a/package.json b/package.json index 4486ba9..38c187e 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,15 @@ "build:binary": "bazel build //crates/codescythe_cli:release_binaries", "build:napi": "bazel build //crates/codescythe_napi:release_nodes", "build:packages": "bazel build //packages/codescythe:package //packages/codescythe-darwin-arm64:package //packages/codescythe-linux-amd64:package //packages/codescythe-linux-arm64:package", - "gazelle": "bazel run //:gazelle" + "gazelle": "bazel run //:gazelle", + "benchmark": "node --experimental-transform-types benchmarks/run.ts", + "benchmark:grafana": "node --experimental-transform-types benchmarks/run.ts --fixture grafana", + "benchmark:kibana": "node --experimental-transform-types benchmarks/run.ts --fixture kibana", + "benchmark:vscode": "node --experimental-transform-types benchmarks/run.ts --fixture vscode" }, "devDependencies": { + "benchmark": "^2.1.4", + "knip": "^6.12.2", "mocha": "^11.0.0" }, "workspaces": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c263395..c4e6c57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: devDependencies: + benchmark: + specifier: ^2.1.4 + version: 2.1.4 + knip: + specifier: ^6.12.2 + version: 6.12.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) mocha: specifier: ^11.0.0 version: 11.7.5 @@ -32,14 +38,270 @@ importers: packages: + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-parser/binding-android-arm-eabi@0.128.0': + resolution: {integrity: sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.128.0': + resolution: {integrity: sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.128.0': + resolution: {integrity: sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.128.0': + resolution: {integrity: sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.128.0': + resolution: {integrity: sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0': + resolution: {integrity: sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.128.0': + resolution: {integrity: sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.128.0': + resolution: {integrity: sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.128.0': + resolution: {integrity: sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.128.0': + resolution: {integrity: sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.128.0': + resolution: {integrity: sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.128.0': + resolution: {integrity: sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.128.0': + resolution: {integrity: sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.128.0': + resolution: {integrity: sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.128.0': + resolution: {integrity: sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.128.0': + resolution: {integrity: sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.128.0': + resolution: {integrity: sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.128.0': + resolution: {integrity: sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.128.0': + resolution: {integrity: sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.128.0': + resolution: {integrity: sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -62,6 +324,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + benchmark@2.1.4: + resolution: {integrity: sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==} + brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} @@ -129,6 +394,18 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -141,10 +418,18 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -180,14 +465,26 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + knip@6.12.2: + resolution: {integrity: sha512-RcZpT1sVziKZgDk1F0hAcp+bq71VJAF8vg1Y9ZLXc1+UXQaMm1rjiUqpJQTIj+lqwmiBQT19/u7ikgazs23cvA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -199,6 +496,9 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -211,6 +511,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + oxc-parser@0.128.0: + resolution: {integrity: sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -237,6 +544,13 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -248,6 +562,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -266,6 +583,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -286,6 +607,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -294,6 +619,21 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -314,6 +654,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -330,8 +675,27 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -341,9 +705,152 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.128.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.128.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.128.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.128.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.128.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.128.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.128.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.128.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.128.0': + optional: true + + '@oxc-project/types@0.128.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -358,6 +865,11 @@ snapshots: balanced-match@1.0.2: {} + benchmark@2.1.4: + dependencies: + lodash: 4.18.1 + platform: 1.3.6 + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 @@ -413,6 +925,14 @@ snapshots: escape-string-regexp@4.0.0: {} + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -425,8 +945,16 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + get-caller-file@2.0.5: {} + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -456,14 +984,38 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + knip@6.12.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + minimist: 1.2.8 + oxc-parser: 0.128.0 + oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.4 + zod: 4.4.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash@4.18.1: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -475,6 +1027,8 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minimist@1.2.8: {} + minipass@7.1.3: {} mocha@11.7.5: @@ -503,6 +1057,57 @@ snapshots: ms@2.1.3: {} + oxc-parser@0.128.0: + dependencies: + '@oxc-project/types': 0.128.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.128.0 + '@oxc-parser/binding-android-arm64': 0.128.0 + '@oxc-parser/binding-darwin-arm64': 0.128.0 + '@oxc-parser/binding-darwin-x64': 0.128.0 + '@oxc-parser/binding-freebsd-x64': 0.128.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.128.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.128.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.128.0 + '@oxc-parser/binding-linux-arm64-musl': 0.128.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.128.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.128.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.128.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.128.0 + '@oxc-parser/binding-linux-x64-gnu': 0.128.0 + '@oxc-parser/binding-linux-x64-musl': 0.128.0 + '@oxc-parser/binding-openharmony-arm64': 0.128.0 + '@oxc-parser/binding-wasm32-wasi': 0.128.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.128.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.128.0 + '@oxc-parser/binding-win32-x64-msvc': 0.128.0 + + oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -524,6 +1129,10 @@ snapshots: picocolors@1.1.1: {} + picomatch@4.0.4: {} + + platform@1.3.6: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -532,6 +1141,8 @@ snapshots: require-directory@2.1.1: {} + resolve-pkg-maps@1.0.0: {} + safe-buffer@5.2.1: {} serialize-javascript@6.0.2: @@ -546,6 +1157,8 @@ snapshots: signal-exit@4.1.0: {} + smol-toml@1.6.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -568,6 +1181,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -576,6 +1191,18 @@ snapshots: dependencies: has-flag: 4.0.0 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: + optional: true + + unbash@3.0.0: {} + + walk-up-path@4.0.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -596,6 +1223,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.8.4: {} + yargs-parser@21.1.1: {} yargs-unparser@2.0.0: @@ -616,3 +1245,5 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zod@4.4.3: {}