Skip to content

Commit 929497e

Browse files
committed
no node
1 parent 8f46780 commit 929497e

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Eliminating the Node engine
2+
3+
> Status: **design / deferred** (2026-06-11). Do the work on a feature branch off `jcamp`
4+
> (see memory `branch-off-jcamp`). No commits without explicit approval.
5+
6+
## Goal
7+
8+
Remove every place this tool needs **Node as its engine**, so a .NET-only or zero-Node
9+
machine produces byte-identical release output. Keep — and lean harder on — **Node as
10+
plumbing** (the package manager, the publisher), which is the legitimate "call out to
11+
Node-based tools" and is only present when you're actually in a JS repo.
12+
13+
This is the no-regret shape for the eventual zero-dependency Go port: the engine
14+
(parse changesets → decide bumps + cascade → render changelog → stamp manifest) runs
15+
natively and identically across ecosystems; ecosystem-specific work sits behind a thin
16+
adapter that delegates to the native package manager. See memory `project-north-star`.
17+
18+
## Two kinds of "Node", opposite treatment
19+
20+
| Kind | Examples | Treatment |
21+
|---|---|---|
22+
| **Engine** — computes *what the release is* | `npx changeset version`, prettier-as-required-formatter | **Eliminate** → native C# |
23+
| **Plumbing** — ecosystem mechanics | `pnpm list`/`npm query`/`yarn workspaces`, `npm publish` | **Keep**, and use *more* (graph source) |
24+
25+
The pivot: Workstream **B** *adds* an acceptable package-manager call that *enables removing*
26+
the `npx changeset` engine call in Workstream **C**.
27+
28+
## Where the engine already is native
29+
30+
The planner core has **zero `.csproj` coupling** already:
31+
32+
- `ReleasePlanner.Assemble(ReleaseInputs) → List<ModuleChangelog>` — pure transform.
33+
- `ChangelogGenerator` — cascade + linked/fixed/lockstep grouping, generic over the package model.
34+
- `Semver` — node-semver bump rules, fully ecosystem-agnostic.
35+
- `ChangelogFileWriter` — markdown generation mirroring `@changesets` `getReleaseLine`, no ecosystem details.
36+
37+
Node survives the engine in exactly three spots, one per workstream below.
38+
39+
---
40+
41+
## Workstream A — Native changelog formatter ("native prettier")
42+
43+
**Today:** `src/Changesets/Commands/Version/Helpers/ChangelogFormatter.cs` shells out to
44+
`pnpm exec prettier` / `dprint` / `deno` / etc. Default `format:false`, so most .NET repos
45+
get no formatting.
46+
47+
**Target:** a hand-rolled, dependency-free, line-based formatter, **default-on**, matching the
48+
prettier / dprint / deno trio byte-for-byte. Full rule spec already captured in memory
49+
`changelog-formatter-native-design` (prettier 3.8.4 source rules: heading `#`+space; block
50+
spacing via loose/tight `node.spread` detection; `-` markers + 2-space child indent; verbatim
51+
code fences; trailing-whitespace trim at every line break; single trailing newline; table
52+
column alignment).
53+
54+
**Steps**
55+
1. Implement `NativeChangelogFormatter` — line-based with code-fence open/close state tracking.
56+
Failure mode is "table not column-aligned" (cosmetic), never Markdig-style data corruption
57+
(Markdig rejected — corrupts pipe tables; see the memory).
58+
2. Table column-alignment is the one genuinely hard rule — ship it last as a documented edge;
59+
changeset summaries rarely contain tables.
60+
3. Config wiring:
61+
- `format:"native"` → native formatter (**new default**)
62+
- `format:"prettier"|"dprint"|"deno"|...` → keep the existing shell-out escape hatch
63+
- `format:false` → explicit opt-out
64+
4. **Tests:** dual golden sets regenerated from pinned `@changesets@3.0.0-next.5` (node must be
65+
present to regenerate):
66+
- `format:false` (raw assembly) — current set; `Normalize` keeps trimming trailing ws.
67+
- `format:<prettier>` (formatted) — **new set**; `Normalize` must **not** trim, or it won't
68+
actually exercise the formatter.
69+
- Add a gnarly fixture: code fence + nested list + blockquote + table to lock the known edges.
70+
71+
**Independence:** lands first, standalone. Becomes load-bearing once C makes our engine write
72+
JS changelogs too (they must come out matching prettier without shelling out).
73+
74+
---
75+
76+
## Workstream B — Package manager as graph source ("pm as graph generator")
77+
78+
**Today:** `src/Changesets/Shared/NodePackagesRepository.cs` hand-parses `pnpm-workspace.yaml`
79+
and `package.json` `workspaces` globs, and extracts only package **names** — no internal
80+
dependency graph, no version ranges, no dep types. That's exactly what C needs.
81+
82+
**Target:** when a JS workspace is present, get the graph as **data** from the package manager
83+
(per north star: delegate to the pm, *never* to `@changesets`). Each pm can emit the workspace
84+
graph as JSON:
85+
86+
- **yarn** — emits the *internal edges* directly:
87+
- Berry: `yarn workspaces list -v --json` → each workspace plus a `workspaceDependencies` array.
88+
- Classic: `yarn workspaces info --json` → keyed by package, each with `workspaceDependencies`
89+
+ `mismatchedWorkspaceDependencies`.
90+
- **pnpm**: `pnpm list -r --depth -1 --json` → every workspace package with its
91+
`dependencies` / `devDependencies` / `peerDependencies` and resolved versions.
92+
- **npm** (8.16+): `npm query ".workspace"` → JSON of each workspace with full dep metadata;
93+
`npm ls --json --all` for the resolved tree.
94+
95+
**Nuance:** only **yarn** literally names the *internal* edges. For **npm/pnpm** the pm gives
96+
(workspace nodes + declared deps + resolved versions), and you form the internal edges by
97+
intersecting declared-dep names against the set of workspace names. That intersection is exactly
98+
what `@manypkg/get-packages` (what `@changesets` itself uses) does — cheap and deterministic, not
99+
resolution guesswork. So "the pm gives us the graph" holds for all three; for npm/pnpm it's
100+
"pm gives nodes + declared edges, we keep the ones pointing at other workspace members."
101+
102+
**Steps**
103+
1. New `JsPackagesRepository` returning rich `JsPackage` records: `Name`, `Version`,
104+
`PackageJsonPath`, and **internal dependencies with dep type (deps/dev/peer/optional) and
105+
version range**. (Strictly more than today's `NodePackagesRepository`, which extracts only
106+
names and sees no edges, ranges, or dep types.)
107+
2. Detect the pm from the lockfile — reuse the logic already in
108+
`ChangelogFormatter.ResolvePackageManagerExecute`.
109+
3. Keep the existing native glob walker as a **fallback** when no pm is installed, so name-only
110+
operations (e.g. `status`) still degrade gracefully. Graph-dependent operations require the pm.
111+
112+
**Why this isn't a regression:** only the pm authoritatively resolves workspaces (`workspace:*`
113+
protocols, overrides, hoisting). Reimplementing that natively is the maintenance tar pit the
114+
north star warns against. The pm is an allowed call.
115+
116+
---
117+
118+
## Workstream C — Port JS versioning from `@changesets` (the hard one)
119+
120+
**Today:** JS versioning is **entirely delegated**:
121+
- no `.csproj` found → `npx changeset version`
122+
(`VersionChangesetCommand.cs:98-106`)
123+
- interop mode → native .NET + `npx changeset version` for the JS side
124+
(`VersionChangesetCommand.cs:195-198`)
125+
- all via `NodeChangesetService.cs` / `NodeDelegation.cs`.
126+
127+
**Target:** route `JsPackage` (from B) through the **same** `ReleasePlanner` the .NET path uses,
128+
stamp `package.json` natively, generate changelogs natively. Drop the `npx changeset` engine call.
129+
130+
**Steps**
131+
132+
1. **Abstraction seam.** Introduce `IPackage` (`Name`, `Version`, `ReferencedDependencies`,
133+
`ManifestPath`, `PackageTitle`). `CsProject` and `JsPackage` both implement it. Make
134+
`ReleaseInputs.Projects` an `IReadOnlyList<IPackage>`. Generalize the single coupled field
135+
`ModuleChangelog.ModuleCsProjFilePath``ModuleManifestPath`. Planner, grouping, and changelog
136+
writer need **no logic changes** — they're already agnostic.
137+
138+
2. **Range-aware cascade — the genuinely new code.** Today the cascade hardcodes "dependents
139+
always patch-bump" (`ChangelogGenerator.cs:283`), which is correct for `.csproj`'s rangeless
140+
`ProjectReference` but **wrong for npm**.
141+
142+
**Decision (2026-06-11): match `@changesets` exactly.** Port the real rules:
143+
- a dependent bumps only when the dependency's new version falls **out of its range**;
144+
- `updateInternalDependencies` (patch/minor) threshold;
145+
- `onlyUpdatePeerDependentsWhenOutOfRange` for peer deps.
146+
147+
Framed for "one process" (north star): this is **one** range-aware algorithm parameterized by
148+
"does this dependency edge carry a range?". The current .NET "always patch" is its degenerate
149+
*rangeless* special case — `.csproj` `ProjectReference` is always out-of-range, so it always
150+
patch-bumps. So matching `@changesets` and staying uniform converge: implement the range-aware
151+
algorithm once; `.csproj` falls out of it as the no-range case. Make the cascade strategy a
152+
property of the adapter (range model), not a fork in the planner.
153+
154+
3. **Write-back.** `JsPackage` version write — stamp `version` in `package.json`, **and rewrite
155+
internal dependency ranges** (`"^1.0.0"``"^1.1.0"`). .NET has no analog (`UpdateInternalDependencies`
156+
only controls dependent *bump size*, never a constraint). New but mechanical: System.Text.Json
157+
read + minimal-diff write that preserves existing formatting.
158+
159+
> **Go-port note.** This is the one step where Go's stdlib is *worse* than .NET's for this job.
160+
> Go's `encoding/json` **reorders keys and reformats on marshal**, so a naive round-trip nukes the
161+
> user's formatting and key order — unacceptable for editing someone's `package.json`. Use a
162+
> surgical, format-preserving editor (e.g. `tidwall/sjson`) to set only the `version` and
163+
> dep-range fields **in place**, leaving the rest byte-identical. Same caution for other manifests
164+
> the adapter writes: `Cargo.toml` / `pyproject.toml` need a format-preserving TOML editor
165+
> (`toml-edit`-equivalent), **not** marshal-and-rewrite. .NET's `System.Text.Json` gives controlled
166+
> edits today; don't assume the Go equivalent round-trips cleanly — it doesn't.
167+
168+
4. **Retire engine delegation.** Remove `AutoRunNode` / `NodeChangesetService` / `NodeDelegation`
169+
from the version path (and status / publish). Keep `npm publish` for the publish step (plumbing,
170+
allowed). Optionally keep `nodeChangesetCommand` as a **deprecated** v0 escape hatch — no longer
171+
the destination.
172+
173+
5. **Parity net (flip it).** Parity tests already run against `@changesets@3.0.0-next.5`. Today they
174+
assert *we delegate to* `@changesets`; flip them to assert *our native output matches*
175+
`@changesets`' `package.json` + `CHANGELOG.md` byte-for-byte. Strongest possible oracle for this
176+
port — and the oracle that drives step 2.
177+
178+
**Ordering:** C depends on B (real graph + ranges) and pairs with A (our engine now writes JS
179+
changelogs, so the native formatter must format them for parity).
180+
181+
---
182+
183+
## Sequencing & risk
184+
185+
| Phase | Work | Unblocks | Risk |
186+
|---|---|---|---|
187+
| 1 | **A** native formatter + dual goldens | parity once C lands | Low — spec fully extracted; table alignment deferrable |
188+
| 2 | **B** pm graph source + `JsPackage` | C | Med — per-pm CLI quirks, JSON shape drift |
189+
| 3 | **C.1** `IPackage` seam + generalize `ModuleChangelog` | C.2–C.5 | Low — planner already agnostic, mechanical |
190+
| 4 | **C.2** range-aware cascade (match `@changesets`) || **High** — real `@changesets` semantics; parity goldens are the oracle |
191+
| 5 | **C.3** `package.json` write-back + range rewrite || Med — formatting-preserving JSON edits |
192+
| 6 | **C.4** retire engine delegation; flip parity tests | done | Low |
193+
194+
## End state
195+
196+
Node is required **only** to:
197+
- run the package manager when you're in a JS repo (graph resolution + `npm publish`);
198+
- optionally run a named external formatter if you opt out of the native one.
199+
200+
The engine — parse changesets, decide bumps + cascade, render changelog, stamp manifest — runs in
201+
native C# identically across `.NET` and JS. That is the "one process across all ecosystems" target,
202+
and the no-regret shape for the Go port.
203+
204+
## Related memory
205+
206+
`project-north-star`, `changelog-formatter-native-design`, `branch-off-jcamp`,
207+
`no-commit-without-approval`, `local-first-release-philosophy`.

0 commit comments

Comments
 (0)