@blzsky/variant 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +177 -0
- package/dist/build-2LO7GD2S.js +63 -0
- package/dist/check-ZTKGGF4M.js +44 -0
- package/dist/chunk-3E5LBU3O.js +180 -0
- package/dist/chunk-4LLK2S4R.js +630 -0
- package/dist/chunk-6J4XDG44.js +14 -0
- package/dist/chunk-CXNXZLDG.js +13 -0
- package/dist/chunk-DRMASVYN.js +25 -0
- package/dist/chunk-GDSZFIHM.js +29 -0
- package/dist/chunk-ISRF5F3Q.js +68 -0
- package/dist/chunk-KSHWC2XN.js +78 -0
- package/dist/chunk-LJDDS4RR.js +91 -0
- package/dist/chunk-LXKLCLL6.js +91 -0
- package/dist/chunk-MZZOE7L4.js +26 -0
- package/dist/chunk-NSNZOH5P.js +31 -0
- package/dist/chunk-O4LZKXNZ.js +301 -0
- package/dist/chunk-RGD422OM.js +108 -0
- package/dist/chunk-RVXUCJYA.js +81 -0
- package/dist/chunk-SWPVR2DF.js +468 -0
- package/dist/chunk-V22IPJ7A.js +64 -0
- package/dist/chunk-V5CTGXMM.js +256 -0
- package/dist/chunk-VTJ67TT5.js +238 -0
- package/dist/chunk-XTYVVUWW.js +279 -0
- package/dist/chunk-XUBWCTJZ.js +100 -0
- package/dist/chunk-YYGCSKIQ.js +29 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +225 -0
- package/dist/diff-GFI6RAEA.js +60 -0
- package/dist/doctor-GH4RZVEH.js +127 -0
- package/dist/env-QORDD6FQ.js +24 -0
- package/dist/impact-AF453PLK.js +702 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +7 -0
- package/dist/init-KJIUNGAN.js +158 -0
- package/dist/insight-DZ5NQY6D.js +29 -0
- package/dist/pr-UFZRZX6S.js +152 -0
- package/dist/run-6PNH4QRA.js +40 -0
- package/dist/symbol-graph-YYUEMZGR.js +22 -0
- package/dist/tsconfig-paths-E3UEDGXZ.js +10 -0
- package/dist/workspace-X5FUBTY3.js +186 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, Bluesky Labs.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense,
|
|
9
|
+
and/or sell copies of the Software, and to permit persons to whom
|
|
10
|
+
the Software is furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included
|
|
13
|
+
in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
16
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# variant
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@blzsky/variant)
|
|
4
|
+
[](https://github.com/saintparish4/variant/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
Change intelligence for TypeScript monorepos. variant reads a diff at the AST level and answers one question: **which tests does this change actually need, and how sure are we?**
|
|
7
|
+
|
|
8
|
+
```console
|
|
9
|
+
$ npx @blzsky/variant impact --base HEAD~1
|
|
10
|
+
|
|
11
|
+
Base ref: HEAD~1
|
|
12
|
+
|
|
13
|
+
You changed 1 file.
|
|
14
|
+
internal src/math.ts
|
|
15
|
+
|
|
16
|
+
Impact: 1 file
|
|
17
|
+
|
|
18
|
+
Run: 1 test file
|
|
19
|
+
Skip: 1 test file (of 2 total)
|
|
20
|
+
|
|
21
|
+
Verdict: build recommended
|
|
22
|
+
Confidence: 100% (report-only — run the full suite; skipping unlocks after shadow-mode validation)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`internal` means the exported signature did not change — only the body — so a dependent that never imports the changed names is not selected. That distinction is the point: test runners and build orchestrators decide "affected" from the file or package graph; variant decides it from the exported surface.
|
|
26
|
+
|
|
27
|
+
No config file is required. Published to npm as **`@blzsky/variant`**; the command it installs is **`variant`**.
|
|
28
|
+
|
|
29
|
+
**`impact` is report-only and stays that way until it is measured.** Every prediction is appended to `.variant/history/impact.jsonl` so a false-skip rate can be reconciled against real test results. The printed confidence is a graph-resolution score, not a safety number. Until that rate is published, run the full suite.
|
|
30
|
+
|
|
31
|
+
A cached task DAG (`build`, `run`, `insight`) sits underneath, and dogfoods the repo.
|
|
32
|
+
|
|
33
|
+
The rest of this README is for working **on** variant. Using it in your own project is documented in [docs/](./docs/getting-started.md).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
| | Version | Notes |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Node | ≥ 20 | Enforced via `engines`; CI tests 20, 22, and 24 |
|
|
42
|
+
| pnpm | ≥ 10 | Pinned by `packageManager` |
|
|
43
|
+
| git | any recent | Needed to exercise `--affected`, `diff`, `impact`, and `pr *` — they no-op without a repo |
|
|
44
|
+
|
|
45
|
+
Optional: **[hyperfine](https://github.com/sharkdp/hyperfine)**, required by `pnpm bench`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/saintparish4/variant.git
|
|
53
|
+
cd variant
|
|
54
|
+
pnpm install
|
|
55
|
+
pnpm build # required before the CLI or E2E tests can run
|
|
56
|
+
node dist/cli.js --help # smoke test
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm build # compile all three entry points to dist/ via tsup
|
|
65
|
+
pnpm clean # delete dist/
|
|
66
|
+
pnpm format # biome format --write .
|
|
67
|
+
pnpm format:check # biome format (read-only)
|
|
68
|
+
pnpm lint # biome check . (static analysis, read-only)
|
|
69
|
+
pnpm typecheck # tsc --noEmit
|
|
70
|
+
pnpm check # biome check --write — local autofix (format + lint + organize imports)
|
|
71
|
+
pnpm bench # benchmark harness (pnpm bench:quick for a fast pass)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The default branch is `master`, and it is expected to be lint-clean — if `pnpm format:check`, `pnpm lint`, or `pnpm typecheck` is red, your change caused it.
|
|
75
|
+
|
|
76
|
+
There is no watch build. The loop is `pnpm build && node dist/cli.js <command>`, run against a scratch project or one of the fixture workspaces in `src/__tests__/fixtures/`.
|
|
77
|
+
|
|
78
|
+
`src/cli/index.ts` registers every command with a dynamic `import()` inside its `action()` callback, deferring execa, jiti, and fast-glob until a command actually runs. That is what keeps `variant --help` fast, and it is the one sanctioned exception to the project's prefer-top-level-imports rule — the benchmark job fails if startup regresses past 200 ms.
|
|
79
|
+
|
|
80
|
+
Benchmark methodology and how to reproduce the published numbers: [benchmarks/README.md](./benchmarks/README.md).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Testing
|
|
85
|
+
|
|
86
|
+
Three tiers, each answering a different question. Write a test at the cheapest tier that can answer yours.
|
|
87
|
+
|
|
88
|
+
| Tier | Tests | Lives in | Runs against |
|
|
89
|
+
|------|-------|----------|--------------|
|
|
90
|
+
| Unit | Isolated behavior — one module, collaborators substituted | `src/**/__tests__/*.test.ts` | Source |
|
|
91
|
+
| Integration | Boundaries — real modules meeting, or a real edge (filesystem, git, config) | `src/__tests__/integration/` | Source |
|
|
92
|
+
| E2E | User workflows through the shipped binary | `src/__tests__/e2e/` | Built `dist/` |
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm test # watch mode
|
|
96
|
+
pnpm test:run # every tier, once
|
|
97
|
+
pnpm test:integration # boundaries only
|
|
98
|
+
pnpm test:e2e # workflows only — run pnpm build first
|
|
99
|
+
pnpm test:all # everything + coverage
|
|
100
|
+
pnpm vitest run src/core/graph/__tests__/dag.test.ts # a single file
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Unit tests never shell out — `runTasksWithDeps` takes a `TaskExecutor`, so tests inject a mock. Fixture workspaces are shared across tiers at `src/__tests__/fixtures/`. Coverage gates in CI: 70% lines/statements/functions, 60% branches.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Environment Variables
|
|
108
|
+
|
|
109
|
+
variant takes no configuration from the environment — that lives in `variant.config.ts`. What it reads are standard terminal and CI signals:
|
|
110
|
+
|
|
111
|
+
| Variable | Read by | Effect |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `NO_COLOR` | `visuals/color.ts` | Disables color everywhere. Highest-priority env signal. |
|
|
114
|
+
| `FORCE_COLOR` | `visuals/color.ts` | Forces color on when output is not a TTY. |
|
|
115
|
+
| `CLICOLOR_FORCE` | `visuals/color.ts` | Same as `FORCE_COLOR`. |
|
|
116
|
+
| `CI` | picocolors, indirectly | Nothing in `src/` reads `CI`. picocolors counts it as color *support*, so CI logs keep color unless `NO_COLOR` is set. Animated progress stops in CI because stderr is not a TTY (`printer.ts:82`), not because of this variable. |
|
|
117
|
+
| `JPY_SESSION_NAME` | `visuals/progress.ts` | Detects a Jupyter session and falls back to line-based output. |
|
|
118
|
+
| `VARIANT_TEST_NO_CLI_PROGRESS` | `visuals/printer.ts`, `visuals/progress.ts` | Test-only. Suppresses progress bars so concurrent output stays assertable. |
|
|
119
|
+
|
|
120
|
+
Color precedence: `--color <when>` → `--no-color` → `NO_COLOR` → `FORCE_COLOR`/`CLICOLOR_FORCE` → TTY detection. All of it resolves once in `visuals/color.ts:resolveColorChoice()`, applied process-wide by `writeGlobalColorChoice()`; renderers call `getColors()` and never consult the environment themselves. Adding a second color path is the mistake this design exists to prevent.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
Layered, dependencies pointing one way: `cli → core → adapters`. Ports are owned by `core`; adapters implement them and import nothing from `core` except types.
|
|
127
|
+
|
|
128
|
+
| Directory | Role |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `src/cli/` | Commander wiring, option parsing, terminal rendering |
|
|
131
|
+
| `src/core/` | Orchestration logic, grouped by capability — `semantic`, `impact`, `pr`, `cache`, `graph`, `execution`, `detection`, `plugins` |
|
|
132
|
+
| `src/adapters/` | The outside world — package managers, runtimes, frameworks |
|
|
133
|
+
| `src/types/` | Contracts shared across layers |
|
|
134
|
+
|
|
135
|
+
`tsup` builds two entry points: `dist/index.js` (library API) and `dist/cli.js` (the `variant` binary).
|
|
136
|
+
|
|
137
|
+
The request path for most commands: `cli/context.ts:createContext()` loads config, detects PM/runtime/framework, builds the task DAG, and computes git-diff scoping — then `core/execution:runTasksWithDeps()` walks DAG levels while `core/cache` hashes inputs against `.variant/cache/cache.json`.
|
|
138
|
+
|
|
139
|
+
Along the way `core/provenance` records why each task was selected to run — cache miss, affected by the diff, or never cached. The runner attaches that record to the `VariantError` a failing task throws, and `cli/render/error.ts` prints it under the failure. It explains *selection*, never *cause*; see [docs/troubleshooting.md](./docs/troubleshooting.md) for the output and that distinction.
|
|
140
|
+
|
|
141
|
+
Two rules to know before your first PR: business logic does not live in command handlers, and `core` never prints or exits — it throws `VariantError` subclasses and lets the CLI top level map them to exit codes (`VariantError` → 1, unexpected → 2). Full detail in [CLAUDE.md](./CLAUDE.md).
|
|
142
|
+
|
|
143
|
+
Note the naming: `core/progress/reporter.ts` is a *port* — a bare `TaskEvent` interface with no output code. Everything that actually draws lives in `cli/visuals/` (progress, spinners, task events) and `cli/render/` (command output, errors). A file under `core/` never prints, whatever its name suggests.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Deployment
|
|
148
|
+
|
|
149
|
+
variant ships as an npm package; there is no server to deploy.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pnpm publish # prepublishOnly runs `pnpm test:all && pnpm build`
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Only `dist/` is published (`files: ["dist"]`). The package exposes `.` through the `exports` map, ESM-only (`"type": "module"`).
|
|
156
|
+
|
|
157
|
+
Releases are **manual**; the only workflows in the repository are `ci.yml` and `benchmark.yml`. Before publishing: bump the version, update [CHANGELOG.md](./CHANGELOG.md), and confirm CI is green on `master`.
|
|
158
|
+
|
|
159
|
+
The package is `0.x` and makes no semver stability promise yet. Breaking changes to the `@blzsky/variant` public API can ship in any minor release; each one is recorded in [CHANGELOG.md](./CHANGELOG.md).
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Read [CONTRIBUTORS.md](./CONTRIBUTORS.md) for the full workflow and [CLAUDE.md](./CLAUDE.md) for architecture and code-style rules. The short version:
|
|
166
|
+
|
|
167
|
+
- All PRs must pass `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, and `pnpm build`.
|
|
168
|
+
- Changed behavior needs a test; a bug fix needs a test that fails before it.
|
|
169
|
+
- Behavior changes that users can observe need a `docs/` update in the same PR.
|
|
170
|
+
- Conventional commits: `<type>: <subject>`, imperative mood, ≤ 72 chars, no trailing period.
|
|
171
|
+
- Bump one dependency at a time — never a blanket `pnpm update` — so lockfile diffs stay reviewable.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeTarget,
|
|
3
|
+
renderDryRunPlan,
|
|
4
|
+
reportRunInsights
|
|
5
|
+
} from "./chunk-4LLK2S4R.js";
|
|
6
|
+
import "./chunk-LXKLCLL6.js";
|
|
7
|
+
import "./chunk-ISRF5F3Q.js";
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
toRunOptions
|
|
11
|
+
} from "./chunk-SWPVR2DF.js";
|
|
12
|
+
import "./chunk-V5CTGXMM.js";
|
|
13
|
+
import "./chunk-RGD422OM.js";
|
|
14
|
+
import "./chunk-YYGCSKIQ.js";
|
|
15
|
+
import "./chunk-GDSZFIHM.js";
|
|
16
|
+
import "./chunk-MZZOE7L4.js";
|
|
17
|
+
import "./chunk-3E5LBU3O.js";
|
|
18
|
+
import "./chunk-RVXUCJYA.js";
|
|
19
|
+
import "./chunk-XUBWCTJZ.js";
|
|
20
|
+
import "./chunk-6J4XDG44.js";
|
|
21
|
+
import "./chunk-V22IPJ7A.js";
|
|
22
|
+
|
|
23
|
+
// src/core/scope/task-filter.ts
|
|
24
|
+
function taskPackage(taskName) {
|
|
25
|
+
const colon = taskName.indexOf(":");
|
|
26
|
+
return colon === -1 ? null : taskName.slice(0, colon);
|
|
27
|
+
}
|
|
28
|
+
function affectedTaskFilter(packages) {
|
|
29
|
+
return (taskName) => {
|
|
30
|
+
const pkg = taskPackage(taskName);
|
|
31
|
+
return pkg === null || packages.has(pkg);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function bothFilters(first, second) {
|
|
35
|
+
if (first === void 0) return second;
|
|
36
|
+
return (taskName) => first(taskName) && second(taskName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/cli/commands/build.ts
|
|
40
|
+
async function registerBuildAction(opts = {}) {
|
|
41
|
+
const ctx = await createContext();
|
|
42
|
+
if (opts.dryRun) {
|
|
43
|
+
renderDryRunPlan("build", ctx.graph.toLevels("build"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const runOptions = toRunOptions(ctx, opts);
|
|
47
|
+
if (opts.affected && ctx.affectedPackages !== void 0) {
|
|
48
|
+
runOptions.taskFilter = bothFilters(
|
|
49
|
+
runOptions.taskFilter,
|
|
50
|
+
affectedTaskFilter(ctx.affectedPackages)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const results = await executeTarget(
|
|
54
|
+
"build",
|
|
55
|
+
ctx,
|
|
56
|
+
runOptions,
|
|
57
|
+
"Running build tasks..."
|
|
58
|
+
);
|
|
59
|
+
await reportRunInsights(ctx, results);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
registerBuildAction
|
|
63
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext
|
|
3
|
+
} from "./chunk-SWPVR2DF.js";
|
|
4
|
+
import "./chunk-V5CTGXMM.js";
|
|
5
|
+
import "./chunk-RGD422OM.js";
|
|
6
|
+
import "./chunk-YYGCSKIQ.js";
|
|
7
|
+
import "./chunk-MZZOE7L4.js";
|
|
8
|
+
import "./chunk-3E5LBU3O.js";
|
|
9
|
+
import {
|
|
10
|
+
ConfigError
|
|
11
|
+
} from "./chunk-RVXUCJYA.js";
|
|
12
|
+
import "./chunk-XUBWCTJZ.js";
|
|
13
|
+
import {
|
|
14
|
+
lines
|
|
15
|
+
} from "./chunk-6J4XDG44.js";
|
|
16
|
+
import {
|
|
17
|
+
getPrinter
|
|
18
|
+
} from "./chunk-V22IPJ7A.js";
|
|
19
|
+
|
|
20
|
+
// src/core/graph/validation.ts
|
|
21
|
+
function validateTaskGraph(tasks, graph) {
|
|
22
|
+
for (const [name, task] of Object.entries(tasks)) {
|
|
23
|
+
for (const dependency of task.dependsOn ?? []) {
|
|
24
|
+
if (!(dependency in tasks)) {
|
|
25
|
+
throw new ConfigError(
|
|
26
|
+
`Task "${name}" depends on unknown task "${dependency}"`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const name of Object.keys(tasks)) {
|
|
32
|
+
graph.toLevels(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/cli/commands/check.ts
|
|
37
|
+
async function registerCheckAction() {
|
|
38
|
+
const ctx = await createContext(process.cwd(), { scope: false });
|
|
39
|
+
validateTaskGraph(ctx.config.tasks, ctx.graph);
|
|
40
|
+
lines(getPrinter(), "Config and graph are valid.");
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
registerCheckAction
|
|
44
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/core/graph/package-graph.ts
|
|
2
|
+
import { readFile, stat } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var WORKSPACE_GLOBS_FALLBACK = ["packages/*", "apps/*", "services/*"];
|
|
5
|
+
async function loadPackageGraph(cwd) {
|
|
6
|
+
const fg = (await import("fast-glob")).default;
|
|
7
|
+
const globs = await readWorkspaceGlobs(cwd);
|
|
8
|
+
const dirs = await fg(globs, {
|
|
9
|
+
cwd,
|
|
10
|
+
onlyDirectories: true,
|
|
11
|
+
absolute: true
|
|
12
|
+
});
|
|
13
|
+
const packages = (await Promise.all(dirs.sort().map(readPackage))).filter(
|
|
14
|
+
(pkg) => pkg !== null
|
|
15
|
+
);
|
|
16
|
+
const byName = new Map(packages.map((p) => [p.manifest.name, p]));
|
|
17
|
+
const edges = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const p of packages) {
|
|
19
|
+
const set = /* @__PURE__ */ new Set();
|
|
20
|
+
const all = {
|
|
21
|
+
...p.manifest.dependencies ?? {},
|
|
22
|
+
...p.manifest.devDependencies ?? {},
|
|
23
|
+
...p.manifest.peerDependencies ?? {}
|
|
24
|
+
};
|
|
25
|
+
for (const dep of Object.keys(all)) {
|
|
26
|
+
if (byName.has(dep)) set.add(dep);
|
|
27
|
+
}
|
|
28
|
+
edges.set(p.manifest.name, set);
|
|
29
|
+
}
|
|
30
|
+
return { packages, edges };
|
|
31
|
+
}
|
|
32
|
+
var ownersByGraph = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
function ownersOf(graph) {
|
|
34
|
+
let owners = ownersByGraph.get(graph);
|
|
35
|
+
if (owners === void 0) {
|
|
36
|
+
owners = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const pkg of graph.packages) {
|
|
38
|
+
const dir = path.resolve(pkg.dir);
|
|
39
|
+
if (!owners.has(dir)) owners.set(dir, pkg);
|
|
40
|
+
}
|
|
41
|
+
ownersByGraph.set(graph, owners);
|
|
42
|
+
}
|
|
43
|
+
return owners;
|
|
44
|
+
}
|
|
45
|
+
function packageForFile(filePath, graph) {
|
|
46
|
+
const owners = ownersOf(graph);
|
|
47
|
+
if (owners.size === 0) return null;
|
|
48
|
+
let current = path.resolve(filePath);
|
|
49
|
+
while (true) {
|
|
50
|
+
const owner = owners.get(current);
|
|
51
|
+
if (owner !== void 0) return owner;
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current) return null;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function tasksFromPackageGraph(graph, existing, scripts = ["build", "test", "lint"], pm = "pnpm") {
|
|
58
|
+
const out = { ...existing };
|
|
59
|
+
for (const pkg of graph.packages) {
|
|
60
|
+
const pkgScripts = pkg.manifest.scripts ?? {};
|
|
61
|
+
for (const script of scripts) {
|
|
62
|
+
if (!pkgScripts[script]) continue;
|
|
63
|
+
const taskName = `${pkg.manifest.name}:${script}`;
|
|
64
|
+
if (out[taskName]) continue;
|
|
65
|
+
const dependsOn = [];
|
|
66
|
+
for (const upstream of graph.edges.get(pkg.manifest.name) ?? []) {
|
|
67
|
+
if (scripts.includes(script)) {
|
|
68
|
+
dependsOn.push(`${upstream}:${script}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out[taskName] = {
|
|
72
|
+
command: `${pm} --filter ${pkg.manifest.name} run ${script}`,
|
|
73
|
+
dependsOn,
|
|
74
|
+
inputs: [
|
|
75
|
+
path.posix.join(
|
|
76
|
+
path.relative(process.cwd(), pkg.dir).replace(/\\/g, "/"),
|
|
77
|
+
"**/*.{ts,tsx,js,jsx,json}"
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const script of scripts) {
|
|
84
|
+
const workspaceTasks = Object.keys(out).filter(
|
|
85
|
+
(name) => name.endsWith(`:${script}`) && !existing[name]
|
|
86
|
+
);
|
|
87
|
+
if (workspaceTasks.length > 0 && out[script] !== void 0) {
|
|
88
|
+
const existingDeps = out[script].dependsOn ?? [];
|
|
89
|
+
const newDeps = workspaceTasks.filter((t) => !existingDeps.includes(t));
|
|
90
|
+
out[script] = {
|
|
91
|
+
...out[script],
|
|
92
|
+
dependsOn: [...existingDeps, ...newDeps]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function computeAffectedPackages(changed, graph) {
|
|
99
|
+
const rdeps = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const [name, deps] of graph.edges) {
|
|
101
|
+
for (const dep of deps) {
|
|
102
|
+
let s = rdeps.get(dep);
|
|
103
|
+
if (!s) {
|
|
104
|
+
s = /* @__PURE__ */ new Set();
|
|
105
|
+
rdeps.set(dep, s);
|
|
106
|
+
}
|
|
107
|
+
s.add(name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const affected = new Set(changed);
|
|
111
|
+
let frontier = new Set(changed);
|
|
112
|
+
while (frontier.size > 0) {
|
|
113
|
+
const next = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const pkg of frontier) {
|
|
115
|
+
for (const dependent of rdeps.get(pkg) ?? []) {
|
|
116
|
+
if (!affected.has(dependent)) {
|
|
117
|
+
affected.add(dependent);
|
|
118
|
+
next.add(dependent);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
frontier = next;
|
|
123
|
+
}
|
|
124
|
+
return affected;
|
|
125
|
+
}
|
|
126
|
+
async function readWorkspaceGlobs(cwd) {
|
|
127
|
+
const pnpm = path.join(cwd, "pnpm-workspace.yaml");
|
|
128
|
+
if (await fileExists(pnpm)) {
|
|
129
|
+
const text = await readFile(pnpm, "utf8");
|
|
130
|
+
const lines = text.split("\n");
|
|
131
|
+
const globs = [];
|
|
132
|
+
let inPackages = false;
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const t = line.trim();
|
|
135
|
+
if (t.startsWith("packages:")) {
|
|
136
|
+
inPackages = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (inPackages) {
|
|
140
|
+
const m = /^-\s*['"]?([^'"\s]+)['"]?/.exec(t);
|
|
141
|
+
const glob = m?.[1];
|
|
142
|
+
if (glob !== void 0) globs.push(glob);
|
|
143
|
+
else if (t && !t.startsWith("#")) inPackages = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (globs.length) return globs;
|
|
147
|
+
}
|
|
148
|
+
const root = path.join(cwd, "package.json");
|
|
149
|
+
if (await fileExists(root)) {
|
|
150
|
+
const json = JSON.parse(await readFile(root, "utf8"));
|
|
151
|
+
const ws = Array.isArray(json.workspaces) ? json.workspaces : json.workspaces?.packages;
|
|
152
|
+
if (ws?.length) return ws;
|
|
153
|
+
}
|
|
154
|
+
return WORKSPACE_GLOBS_FALLBACK;
|
|
155
|
+
}
|
|
156
|
+
async function readPackage(dir) {
|
|
157
|
+
const manifestPath = path.join(dir, "package.json");
|
|
158
|
+
try {
|
|
159
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
160
|
+
if (!manifest?.name) return null;
|
|
161
|
+
return { name: manifest.name, dir, manifest };
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function fileExists(p) {
|
|
167
|
+
try {
|
|
168
|
+
await stat(p);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
loadPackageGraph,
|
|
177
|
+
packageForFile,
|
|
178
|
+
tasksFromPackageGraph,
|
|
179
|
+
computeAffectedPackages
|
|
180
|
+
};
|