rolldown-pnpm-config 2.3.0 → 2.4.1

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.
@@ -12,11 +12,11 @@ import { legend } from "../render/legend.js";
12
12
  import { printView } from "../render/print.js";
13
13
  import { failureDoc } from "../render/report.js";
14
14
  import { findConfigFiles, pickConfigCandidate } from "../select-file.js";
15
- import { canonicalize, renderWorkspace } from "../workspace-file.js";
15
+ import { canonicalize, writeWorkspace } from "../workspace-file.js";
16
16
  import { overlayWorkspace } from "../workspace-overlay.js";
17
17
  import { Data, Effect, FileSystem, Option, Predicate } from "effect";
18
18
  import { dirname, relative } from "node:path";
19
- import { CliDoc, CliMessage } from "@effected/cli";
19
+ import { CliDoc, CliMessage } from "@effected/cli-kit";
20
20
  import { existsSync } from "node:fs";
21
21
  import { Argument, Command, Flag } from "effect/cli";
22
22
 
@@ -49,7 +49,7 @@ const WORKSPACE_FIELDS = new Set(Object.entries(DESCRIPTORS).filter(([, d]) => d
49
49
  */
50
50
  function runExport(opts) {
51
51
  return Effect.gen(function* () {
52
- const { config, localCfg, path, parsed } = yield* loadConfigAndWorkspace(opts, (message) => new ExportError({ message }));
52
+ const { config, localCfg, path, parsed, source } = yield* loadConfigAndWorkspace(opts, (message) => new ExportError({ message }));
53
53
  const pluginConfig = config;
54
54
  const owned = discoverOwnedPatches(pluginConfig, dirname(opts.configFile));
55
55
  const resolvedConfig = withResolvedBuildPatches(pluginConfig, dirname(opts.configFile), owned);
@@ -76,7 +76,7 @@ function runExport(opts) {
76
76
  exists: existsSync
77
77
  });
78
78
  const merged = overlayWorkspace(effective, parsed);
79
- const rendered = renderWorkspace(merged);
79
+ const rendered = writeWorkspace(source, merged);
80
80
  const localKeys = new Set(Object.keys(localCfg ?? {}));
81
81
  const tree = buildDiff(canonicalize(parsed), canonicalize(merged), {
82
82
  localKeys,
@@ -11,9 +11,9 @@ import { previewScreen } from "../ui/screens.js";
11
11
  import { WORKSPACE_FIELDS } from "./export.js";
12
12
  import { Data, Effect, Option } from "effect";
13
13
  import { dirname } from "node:path";
14
- import { CliDoc, CliInteractive } from "@effected/cli";
14
+ import { CliDoc, CliInteractive } from "@effected/cli-kit";
15
15
  import { Argument, Command } from "effect/cli";
16
- import { CliUi } from "@effected/cli/ui";
16
+ import { CliUi } from "@effected/cli-kit/ui";
17
17
 
18
18
  // src/cli/commands/preview.ts
19
19
  /** Typed failure for the preview run. @internal */
@@ -20,9 +20,9 @@ import { buildWalkItems } from "../walk-plan.js";
20
20
  import { findWorkspaceRoot, makeWorkspaceResolver } from "../workspace-resolve.js";
21
21
  import { Clock, Data, Effect, FileSystem, Option, Result } from "effect";
22
22
  import { dirname } from "node:path";
23
- import { CliDoc, CliExit, CliFormat, CliInteractive, CliMessage, Doc, Fmt } from "@effected/cli";
23
+ import { CliDoc, CliExit, CliFormat, CliInteractive, CliMessage, Doc, Fmt } from "@effected/cli-kit";
24
24
  import { Argument, Command, Flag } from "effect/cli";
25
- import { CliUi } from "@effected/cli/ui";
25
+ import { CliUi } from "@effected/cli-kit/ui";
26
26
  import { ReleaseAgeGate } from "@effected/npm";
27
27
 
28
28
  // src/cli/commands/upgrade.ts
@@ -1,5 +1,5 @@
1
1
  import { row, tone } from "../render/tone.js";
2
- import { Doc } from "@effected/cli";
2
+ import { Doc } from "@effected/cli-kit";
3
3
 
4
4
  // src/cli/diff/render.ts
5
5
  const CONTEXT = 2;
@@ -1,7 +1,7 @@
1
1
  import { evaluatePluginConfig } from "./evaluate.js";
2
2
  import { parseWorkspace } from "./workspace-file.js";
3
3
  import { Effect, FileSystem, Predicate } from "effect";
4
- import { Fmt } from "@effected/cli";
4
+ import { Fmt } from "@effected/cli-kit";
5
5
 
6
6
  // src/cli/load-config.ts
7
7
  /**
@@ -26,15 +26,18 @@ function loadConfigAndWorkspace(opts, mkError) {
26
26
  if (errors.length > 0) return yield* Effect.fail(mkError(`Non-literal config values: ${errors.join("; ")}`));
27
27
  const path = opts.workspacePath;
28
28
  const cannotRead = (e) => mkError(`Cannot read or parse ${path}: ${Fmt.errorText(e)}`);
29
- const parsed = (yield* fs.exists(path).pipe(Effect.mapError(cannotRead))) ? yield* fs.readFileString(path).pipe(Effect.mapError(cannotRead), Effect.flatMap((text) => Effect.try({
30
- try: () => parseWorkspace(text),
29
+ const present = yield* fs.exists(path).pipe(Effect.mapError(cannotRead));
30
+ const source = present ? yield* fs.readFileString(path).pipe(Effect.mapError(cannotRead)) : "";
31
+ const parsed = present ? yield* Effect.try({
32
+ try: () => parseWorkspace(source),
31
33
  catch: cannotRead
32
- }))) : {};
34
+ }) : {};
33
35
  return {
34
36
  config,
35
37
  localCfg: Predicate.isObject(config.local) ? config.local : void 0,
36
38
  path,
37
- parsed
39
+ parsed,
40
+ source
38
41
  };
39
42
  });
40
43
  }
package/cli/main.js CHANGED
@@ -4,7 +4,7 @@ import { RegistryResolverLive } from "./resolve.js";
4
4
  import { rootCommand } from "./root.js";
5
5
  import { CLI_VERSION } from "./version.js";
6
6
  import { Effect } from "effect";
7
- import { CliAudience, CliRuntime } from "@effected/cli";
7
+ import { CliAudience, CliRuntime } from "@effected/cli-kit";
8
8
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
9
9
 
10
10
  // src/cli/main.ts
@@ -1,5 +1,5 @@
1
1
  import { tone } from "./tone.js";
2
- import { Doc } from "@effected/cli";
2
+ import { Doc } from "@effected/cli-kit";
3
3
 
4
4
  // src/cli/render/legend.ts
5
5
  function swatches(entries) {
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect";
2
- import { CliTheme, Doc } from "@effected/cli";
2
+ import { CliTheme, Doc } from "@effected/cli-kit";
3
3
  import { Audience } from "@effected/env";
4
4
 
5
5
  // src/cli/render/print.ts
@@ -1,4 +1,4 @@
1
- import { Doc, Status } from "@effected/cli";
1
+ import { Doc, Status } from "@effected/cli-kit";
2
2
 
3
3
  // src/cli/render/report.ts
4
4
  function statusDoc(status, message) {
@@ -1,4 +1,4 @@
1
- import { Doc, Token } from "@effected/cli";
1
+ import { Doc, Token } from "@effected/cli-kit";
2
2
 
3
3
  // src/cli/render/tone.ts
4
4
  /**
package/cli/resolve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, Data, Effect, Layer, Predicate } from "effect";
2
- import { Fmt } from "@effected/cli";
2
+ import { Fmt } from "@effected/cli-kit";
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
4
4
 
5
5
  // src/cli/resolve.ts
package/cli/root.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { exportCommand } from "./commands/export.js";
2
2
  import { previewCommand } from "./commands/preview.js";
3
3
  import { upgradeCommand } from "./commands/upgrade.js";
4
- import { CliAudience } from "@effected/cli";
4
+ import { CliAudience } from "@effected/cli-kit";
5
5
  import { Command } from "effect/cli";
6
6
 
7
7
  // src/cli/root.ts
@@ -2,7 +2,7 @@ import { scalarText } from "./diff/render.js";
2
2
  import { row, tone } from "./render/tone.js";
3
3
  import { canonicalize } from "./workspace-file.js";
4
4
  import { Predicate } from "effect";
5
- import { Doc } from "@effected/cli";
5
+ import { Doc } from "@effected/cli-kit";
6
6
 
7
7
  // src/cli/simulated-view.ts
8
8
  /**
package/cli/summary.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { row, tone } from "./render/tone.js";
2
2
  import { cellTone, displayCandidates, peerFor, tableLayout } from "./walk-reducer.js";
3
- import { Doc, Fmt } from "@effected/cli";
3
+ import { Doc, Fmt } from "@effected/cli-kit";
4
4
 
5
5
  // src/cli/summary.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { legend, simulatedLegend } from "../render/legend.js";
2
- import { Doc } from "@effected/cli";
3
- import { DocView, KeyHelp, KeyTable, Styled, Tabs, Viewport, useKeys, useTerminalSize, useTheme } from "@effected/cli/ui";
2
+ import { Doc } from "@effected/cli-kit";
3
+ import { DocView, KeyHelp, KeyTable, Styled, Tabs, Viewport, useKeys, useTerminalSize, useTheme } from "@effected/cli-kit/ui";
4
4
  import { Box, Text } from "ink";
5
5
  import { useEffect, useState } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,5 +1,5 @@
1
- import { Status } from "@effected/cli";
2
- import { Styled, useGlyphs, useTheme } from "@effected/cli/ui";
1
+ import { Status } from "@effected/cli-kit";
2
+ import { Styled, useGlyphs, useTheme } from "@effected/cli-kit/ui";
3
3
  import { Text } from "ink";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
 
package/cli/ui/screens.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { initialProgress, reduceProgress } from "./progress.js";
2
2
  import { Effect, PubSub } from "effect";
3
- import { CliUi } from "@effected/cli/ui";
3
+ import { CliUi } from "@effected/cli-kit/ui";
4
4
 
5
5
  // src/cli/ui/screens.ts
6
6
  /** The interactive upgrade table, loaded on mount. @internal */
@@ -1,8 +1,8 @@
1
1
  import { computeGroupPeers } from "../interop-live.js";
2
2
  import { TONES } from "../render/tone.js";
3
3
  import { cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep } from "../walk-reducer.js";
4
- import { Fmt } from "@effected/cli";
5
- import { KeyHelp, KeyTable, Styled, useKeys, useTerminalSize } from "@effected/cli/ui";
4
+ import { Fmt } from "@effected/cli-kit";
5
+ import { KeyHelp, KeyTable, Styled, useKeys, useTerminalSize } from "@effected/cli-kit/ui";
6
6
  import { Box, Text } from "ink";
7
7
  import { useRef, useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
package/cli/version.js CHANGED
@@ -6,6 +6,6 @@
6
6
  *
7
7
  * @internal
8
8
  */
9
- const CLI_VERSION = "2.3.0";
9
+ const CLI_VERSION = "2.4.1";
10
10
 
11
11
  export { CLI_VERSION };
@@ -1,4 +1,4 @@
1
- import { Fmt } from "@effected/cli";
1
+ import { Fmt } from "@effected/cli-kit";
2
2
 
3
3
  // src/cli/walk-reducer.ts
4
4
  const ORDER = {
@@ -64,7 +64,7 @@ function initTable(items) {
64
64
  done: false
65
65
  };
66
66
  }
67
- const clamp = (n, max) => n < 0 ? 0 : n > max ? max : n;
67
+ const clamp = (n, max) => n < 0 ? 0 : Math.min(n, max);
68
68
  /**
69
69
  * Advance the table by a key. up/down move between rows; left/right move the
70
70
  * radio selection within the row under the cursor; submit applies. Both axes
@@ -1,7 +1,7 @@
1
- import { Order, Predicate, Result } from "effect";
1
+ import { Option, Order, Predicate, Result } from "effect";
2
2
  import { locateWorkspaceRootSync } from "@effected/workspaces";
3
3
  import { nodeSyncOps } from "@effected/workspaces/node-sync";
4
- import { Yaml, YamlStringifyOptions } from "@effected/yaml";
4
+ import { Yaml, YamlDocument, YamlEdit, YamlFormat, YamlLint, YamlLintConfig, YamlStringifyOptions } from "@effected/yaml";
5
5
 
6
6
  // src/cli/workspace-file.ts
7
7
  const FILENAME = "pnpm-workspace.yaml";
@@ -9,6 +9,20 @@ const STRINGIFY_OPTIONS = YamlStringifyOptions.make({
9
9
  indent: 2,
10
10
  lineWidth: 0
11
11
  });
12
+ /**
13
+ * The lint config style inference resolves over: the renderer's historical
14
+ * defaults (2-space, unindented sequences, single quotes), so a dimension the
15
+ * source gives no votes to falls back to today's output rather than the lint
16
+ * preset's double-quote default.
17
+ */
18
+ const STYLE_BASE = YamlLintConfig.make({ rules: {
19
+ ...YamlLintConfig.default.rules,
20
+ indentation: {
21
+ spaces: 2,
22
+ indentSequences: false
23
+ },
24
+ "quoted-strings": { quoteType: "single" }
25
+ } });
12
26
  /** True when every element is a string/number/boolean (safe to sort). */
13
27
  function allPrimitive(arr) {
14
28
  return arr.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
@@ -46,9 +60,298 @@ function parseWorkspace(source) {
46
60
  const parsed = Result.getOrThrow(Yaml.parseResult(source));
47
61
  return Predicate.isObject(parsed) ? parsed : {};
48
62
  }
49
- /** Render a workspace object: deterministic key sort + yaml.stringify. @internal */
50
- function renderWorkspace(obj) {
51
- return Result.getOrThrow(Yaml.stringifyResult(canonicalize(obj), STRINGIFY_OPTIONS));
63
+ /**
64
+ * Render a workspace object from scratch: deterministic key sort +
65
+ * stringify. `options` defaults to the historical 2-space, unindented,
66
+ * single-quoted style.
67
+ *
68
+ * @internal
69
+ */
70
+ function renderWorkspace(obj, options = STRINGIFY_OPTIONS) {
71
+ return Result.getOrThrow(Yaml.stringifyResult(canonicalize(obj), options));
72
+ }
73
+ /**
74
+ * The stringify options matching the style `source` already follows: indent
75
+ * width, whether block sequences sit indented under their key, and quote
76
+ * character. Lenient — a mixed file takes the dominant spelling — and a
77
+ * dimension with no votes keeps the historical default.
78
+ *
79
+ * @internal
80
+ */
81
+ function inferWorkspaceStyle(source) {
82
+ const { rules } = YamlLint.resolveLenient(YamlLint.observe(source, YamlLint.builtins), STYLE_BASE);
83
+ const indentation = rules.indentation;
84
+ const quoted = rules["quoted-strings"];
85
+ return YamlStringifyOptions.make({
86
+ indent: typeof indentation.spaces === "number" ? indentation.spaces : 2,
87
+ indentSequences: indentation.indentSequences === true,
88
+ quoteStyle: quoted.quoteType === "double" ? "double" : "single",
89
+ lineWidth: 0
90
+ });
91
+ }
92
+ /** Order-insensitive structural equality over canonical forms. */
93
+ function sameValue(a, b) {
94
+ return JSON.stringify(canonicalize(a)) === JSON.stringify(canonicalize(b));
95
+ }
96
+ /** A mapping (not a sequence, not null) — the only node the diff descends into. */
97
+ function isMapping(v) {
98
+ return Predicate.isObject(v) && !Array.isArray(v);
99
+ }
100
+ /**
101
+ * Per-item edits for a primitive sequence present on both sides: remove the
102
+ * items `next` no longer holds (highest index first, so earlier indices stay
103
+ * valid) and append the ones it adds, in canonical order. Every other item's
104
+ * line is untouched. Matching is by multiset, so a reorder is not a change.
105
+ */
106
+ function sequenceChanges(path, prev, next) {
107
+ const key = (v) => JSON.stringify(v);
108
+ const pending = /* @__PURE__ */ new Map();
109
+ for (const v of next) pending.set(key(v), (pending.get(key(v)) ?? 0) + 1);
110
+ const removed = [];
111
+ prev.forEach((v, i) => {
112
+ const n = pending.get(key(v)) ?? 0;
113
+ if (n > 0) pending.set(key(v), n - 1);
114
+ else removed.push(i);
115
+ });
116
+ const added = [];
117
+ for (const v of next) {
118
+ const n = pending.get(key(v)) ?? 0;
119
+ if (n > 0) {
120
+ added.push(v);
121
+ pending.set(key(v), n - 1);
122
+ }
123
+ }
124
+ added.sort((a, b) => Order.String(String(a), String(b)));
125
+ const changes = added.map((v, i) => ({
126
+ path: [...path, prev.length + i],
127
+ value: v
128
+ }));
129
+ for (const i of removed.reverse()) changes.push({
130
+ path: [...path, i],
131
+ value: void 0
132
+ });
133
+ return changes;
134
+ }
135
+ /**
136
+ * The minimal path-targeted changes taking `before` to `after`: descend
137
+ * through mappings present on both sides, edit primitive sequences item by
138
+ * item, and replace (or remove) any other value whose canonical form differs.
139
+ */
140
+ function workspaceChanges(before, after, prefix = []) {
141
+ const changes = [];
142
+ for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) changes.push({
143
+ path: [...prefix, key],
144
+ value: void 0
145
+ });
146
+ for (const [key, next] of Object.entries(after)) {
147
+ const path = [...prefix, key];
148
+ if (!Object.hasOwn(before, key)) {
149
+ changes.push({
150
+ path,
151
+ value: canonicalize(next),
152
+ newKey: true
153
+ });
154
+ continue;
155
+ }
156
+ const prev = before[key];
157
+ if (sameValue(prev, next)) continue;
158
+ if (isMapping(prev) && isMapping(next)) changes.push(...workspaceChanges(prev, next, path));
159
+ else if (Array.isArray(prev) && Array.isArray(next) && next.length > 0 && allPrimitive(prev) && allPrimitive(next)) changes.push(...sequenceChanges(path, prev, next));
160
+ else changes.push({
161
+ path,
162
+ value: canonicalize(next)
163
+ });
164
+ }
165
+ return changes;
166
+ }
167
+ /** Every scalar under `node`, with its role. */
168
+ function* scalarsOf(node, role) {
169
+ if (node === null) return;
170
+ if (node._tag === "YamlScalar") yield [node, role];
171
+ else if (node._tag === "YamlMap") for (const pair of node.items) {
172
+ yield* scalarsOf(pair.key, "key");
173
+ yield* scalarsOf(pair.value, "value");
174
+ }
175
+ else if (node._tag === "YamlSeq") for (const item of node.items) yield* scalarsOf(item, "item");
176
+ }
177
+ /** True when `s` would render plain, i.e. quoting it is a style choice rather than a requirement. */
178
+ function couldBePlain(s) {
179
+ const out = Yaml.stringifyResult(s, STRINGIFY_OPTIONS);
180
+ return Result.isSuccess(out) && !/^['"]/.test(out.success);
181
+ }
182
+ /**
183
+ * The quoting vote over `scalars`: positive when, among strings that need no
184
+ * quotes, more are quoted than plain. Strings that must be quoted (`@scope/*`)
185
+ * cast no vote, so scalars quoted only where required read as plain.
186
+ */
187
+ function quoteVote(scalars) {
188
+ let vote = 0;
189
+ for (const [scalar] of scalars) {
190
+ if (typeof scalar.value !== "string" || !couldBePlain(scalar.value)) continue;
191
+ vote += scalar.style === "plain" ? -1 : 1;
192
+ }
193
+ return vote;
194
+ }
195
+ /** The roles in which the whole document quotes strings by choice: the fallback when siblings give no vote. */
196
+ function quotedRoles(doc) {
197
+ const all = [...scalarsOf(doc, "value")];
198
+ return new Set([
199
+ "key",
200
+ "value",
201
+ "item"
202
+ ].filter((r) => quoteVote(all.filter(([, role]) => role === r)) > 0));
203
+ }
204
+ /** `s` as a quoted scalar in the given quote character. */
205
+ function quote(s, style) {
206
+ return style === "double" ? JSON.stringify(s) : `'${s.replaceAll("'", "''")}'`;
207
+ }
208
+ /** The scalar directly at `node` (not nested), with its role. */
209
+ function directScalar(node, role) {
210
+ return node?._tag === "YamlScalar" ? [[node, role]] : [];
211
+ }
212
+ /**
213
+ * Quote the plain string scalars a change just wrote, where the file quotes
214
+ * by choice. The stringifier only quotes what it must, so without this a
215
+ * catalog that quotes every key gains a bare `vitest:` beside `"effect":`.
216
+ * The written node's own key, value or item follows its siblings' vote, which
217
+ * keeps the choice local (a file can quote catalog keys yet leave top-level
218
+ * keys plain); scalars nested deeper in a new subtree, or a node whose
219
+ * siblings cast no vote, follow the whole document's roles. Only the written
220
+ * subtree is touched (plus its key when the key is new).
221
+ */
222
+ function requoteWritten(text, change, docRoles, style) {
223
+ if (change.value === void 0) return text;
224
+ const doc = YamlDocument.parseResult(text);
225
+ if (Result.isFailure(doc) || doc.success.contents === null) return text;
226
+ const root = doc.success.contents;
227
+ const last = change.path[change.path.length - 1];
228
+ const parent = change.path.length > 1 ? root.find(change.path.slice(0, -1)) : Option.some(root);
229
+ if (Option.isNone(parent)) return text;
230
+ const container = parent.value;
231
+ const decide = (vote, role) => vote !== 0 ? vote > 0 : docRoles.has(role);
232
+ const targets = [];
233
+ const deep = (node) => {
234
+ if (node === null || node._tag === "YamlScalar") return;
235
+ for (const [scalar, role] of scalarsOf(node, "value")) targets.push([scalar, docRoles.has(role)]);
236
+ };
237
+ if (container._tag === "YamlMap") {
238
+ const index = container.items.findIndex((p) => p.key._tag === "YamlScalar" && String(p.key.value) === String(last));
239
+ const pair = container.items[index];
240
+ if (pair === void 0) return text;
241
+ const siblings = container.items.filter((_, i) => i !== index);
242
+ if (change.newKey) {
243
+ const vote = quoteVote(siblings.flatMap((p) => directScalar(p.key, "key")));
244
+ for (const [scalar] of directScalar(pair.key, "key")) targets.push([scalar, decide(vote, "key")]);
245
+ }
246
+ const vote = quoteVote(siblings.flatMap((p) => directScalar(p.value, "value")));
247
+ for (const [scalar] of directScalar(pair.value, "value")) targets.push([scalar, decide(vote, "value")]);
248
+ deep(pair.value);
249
+ } else if (container._tag === "YamlSeq" && typeof last === "number") {
250
+ const item = container.items[last] ?? null;
251
+ const vote = quoteVote(container.items.filter((_, i) => i !== last).flatMap((n) => directScalar(n, "item")));
252
+ for (const [scalar] of directScalar(item, "item")) targets.push([scalar, decide(vote, "item")]);
253
+ deep(item);
254
+ }
255
+ const edits = targets.filter(([s, quoted]) => quoted && s.style === "plain" && typeof s.value === "string" && s.tag === void 0).map(([s]) => ({
256
+ offset: s.offset,
257
+ length: s.length,
258
+ content: quote(s.value, style)
259
+ })).sort((a, b) => b.offset - a.offset);
260
+ let out = text;
261
+ for (const e of edits) out = out.slice(0, e.offset) + e.content + out.slice(e.offset + e.length);
262
+ return out;
263
+ }
264
+ /** True when some mapping in `node` holds two keys that read as the same string (`1:` and `'1':`). */
265
+ function hasCollidingKeys(node) {
266
+ if (node === null) return false;
267
+ if (node._tag === "YamlMap") {
268
+ const seen = /* @__PURE__ */ new Set();
269
+ for (const { key, value } of node.items) {
270
+ const k = key._tag === "YamlScalar" ? String(key.value) : JSON.stringify(key);
271
+ if (seen.has(k)) return true;
272
+ seen.add(k);
273
+ if (hasCollidingKeys(value)) return true;
274
+ }
275
+ } else if (node._tag === "YamlSeq") return node.items.some(hasCollidingKeys);
276
+ return false;
277
+ }
278
+ /**
279
+ * True when `text` parses to exactly `next` (canonically) with no colliding
280
+ * keys. The edit model and the parsed object can disagree — anchors and
281
+ * aliases (a removed `&b` leaves a dangling `*b`; a splice under an anchor
282
+ * changes every alias), merge keys, non-string keys — and this is the one
283
+ * check that catches all of them.
284
+ */
285
+ function faithful(text, next) {
286
+ const doc = YamlDocument.parseResult(text);
287
+ if (Result.isFailure(doc) || hasCollidingKeys(doc.success.contents)) return false;
288
+ const value = Yaml.parseResult(text);
289
+ return Result.isSuccess(value) && sameValue(isMapping(value.success) ? value.success : {}, next);
290
+ }
291
+ /**
292
+ * Splice a primitive into an existing scalar's own source span, keeping its
293
+ * quotes and trailing comment. `modify` only splices when given no
294
+ * document-shaping options (any `indent`/`indentSequences` sends it down the
295
+ * whole-document path, which re-styles the replaced node and drops its
296
+ * comment), so the splice is attempted unstyled and accepted only when every
297
+ * edit stays inside the target scalar. `None` means "not a splice" — the
298
+ * caller takes the styled path. Workaround for spencerbeggs/effected#1219.
299
+ */
300
+ function spliceScalar(text, path, value) {
301
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return Option.none();
302
+ const doc = YamlDocument.parseResult(text);
303
+ if (Result.isFailure(doc) || doc.success.contents === null) return Option.none();
304
+ const target = doc.success.contents.find(path);
305
+ if (Option.isNone(target) || target.value._tag !== "YamlScalar") return Option.none();
306
+ const { offset, length } = target.value;
307
+ const edits = YamlFormat.modifyResult(text, path, value);
308
+ if (Result.isFailure(edits)) return Option.none();
309
+ return edits.success.every((e) => e.offset >= offset && e.offset + e.length <= offset + length) ? Option.some(YamlEdit.applyAll(text, edits.success)) : Option.none();
310
+ }
311
+ /** Apply every change as an edit; `None` when the editor refuses one. */
312
+ function applyChanges(source, changes, style) {
313
+ const docRoot = YamlDocument.parseResult(source);
314
+ const roles = Result.isSuccess(docRoot) ? quotedRoles(docRoot.success.contents) : /* @__PURE__ */ new Set();
315
+ const quoteChar = style.quoteStyle ?? "single";
316
+ let text = source;
317
+ for (const change of changes) {
318
+ const spliced = change.newKey ? Option.none() : spliceScalar(text, change.path, change.value);
319
+ if (Option.isSome(spliced)) {
320
+ text = spliced.value;
321
+ continue;
322
+ }
323
+ const result = YamlFormat.modifyToStringResult(text, change.path, change.value, style);
324
+ if (Result.isFailure(result)) return Option.none();
325
+ text = requoteWritten(result.success, change, roles, quoteChar);
326
+ }
327
+ return Option.some(text);
328
+ }
329
+ /**
330
+ * Write `next` over an existing pnpm-workspace.yaml `source` as targeted
331
+ * edits rather than a re-render, so comments, key order and unchanged lines
332
+ * survive. A changed scalar is spliced in place keeping its quotes and
333
+ * comment; a primitive list gains or loses only the items that changed; and
334
+ * inserted content takes the style the file already follows (see
335
+ * {@link inferWorkspaceStyle}), including quoting keys, values or list items
336
+ * where the file quotes them by choice. New keys append to their mapping, and
337
+ * a CRLF file stays CRLF.
338
+ *
339
+ * An empty source renders from scratch exactly as {@link renderWorkspace}
340
+ * does. A source with no mapping to edit, one the editor refuses
341
+ * (multi-document, directives), or an edit result that does not parse back to
342
+ * exactly `next` (anchors, merge keys, colliding keys) is re-rendered whole
343
+ * in its inferred style instead.
344
+ *
345
+ * @internal
346
+ */
347
+ function writeWorkspace(source, next) {
348
+ if (source.trim() === "") return renderWorkspace(next);
349
+ const parsed = Result.getOrThrow(Yaml.parseResult(source));
350
+ const style = inferWorkspaceStyle(source);
351
+ const crlf = source.includes("\r\n");
352
+ const edited = isMapping(parsed) ? applyChanges(source, workspaceChanges(parsed, next), style) : Option.none();
353
+ const text = Option.isSome(edited) && faithful(edited.value, next) ? edited.value : renderWorkspace(next, style);
354
+ return crlf ? text.replace(/\r?\n/g, "\r\n") : text;
52
355
  }
53
356
 
54
- export { canonicalize, findWorkspaceFile, parseWorkspace, renderWorkspace };
357
+ export { canonicalize, findWorkspaceFile, inferWorkspaceStyle, parseWorkspace, renderWorkspace, writeWorkspace };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rolldown-pnpm-config",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "private": false,
5
5
  "description": "A dogfooding example of our plugin",
6
6
  "repository": {
@@ -35,16 +35,16 @@
35
35
  "rolldown-pnpm-config": "bin/rolldown-pnpm-config.js"
36
36
  },
37
37
  "dependencies": {
38
- "@effect/platform-node": "~4.1.1",
39
- "@effected/cli": "^0.25.0",
40
- "@effected/env": "^0.9.0",
41
- "@effected/glob": "^0.17.0",
42
- "@effected/npm": "^0.29.0",
43
- "@effected/semver": "^0.20.0",
44
- "@effected/walker": "^0.24.0",
45
- "@effected/workspaces": "^0.41.0",
46
- "@effected/yaml": "^0.27.0",
47
- "effect": "~4.1.1",
38
+ "@effect/platform-node": "~4.1.2",
39
+ "@effected/cli-kit": "^0.26.0",
40
+ "@effected/env": "^0.10.0",
41
+ "@effected/glob": "^0.18.0",
42
+ "@effected/npm": "^0.30.0",
43
+ "@effected/semver": "^0.21.0",
44
+ "@effected/walker": "^0.25.0",
45
+ "@effected/workspaces": "^0.42.0",
46
+ "@effected/yaml": "^0.28.0",
47
+ "effect": "~4.1.2",
48
48
  "ink": "^8.0.0",
49
49
  "oxc-parser": "^0.156.0",
50
50
  "react": "^19.3.1"
package/plugin/freeze.js CHANGED
@@ -4,7 +4,7 @@ import { FIELD_REGISTRY } from "../registry.js";
4
4
  import { resolvePeerDependencyRules } from "./allowed-versions.js";
5
5
  import { resolveOverrides } from "./overrides-from-catalogs.js";
6
6
  import { Data, Effect, Predicate, Schema } from "effect";
7
- import { Fmt } from "@effected/cli";
7
+ import { Fmt } from "@effected/cli-kit";
8
8
 
9
9
  // src/plugin/freeze.ts
10
10
  /**
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.60.0"
8
+ "packageVersion": "7.60.1"
9
9
  }
10
10
  ]
11
11
  }