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.
- package/cli/commands/export.js +4 -4
- package/cli/commands/preview.js +2 -2
- package/cli/commands/upgrade.js +2 -2
- package/cli/diff/render.js +1 -1
- package/cli/load-config.js +8 -5
- package/cli/main.js +1 -1
- package/cli/render/legend.js +1 -1
- package/cli/render/print.js +1 -1
- package/cli/render/report.js +1 -1
- package/cli/render/tone.js +1 -1
- package/cli/resolve.js +1 -1
- package/cli/root.js +1 -1
- package/cli/simulated-view.js +1 -1
- package/cli/summary.js +1 -1
- package/cli/ui/preview-screen.js +2 -2
- package/cli/ui/progress-view.js +2 -2
- package/cli/ui/screens.js +1 -1
- package/cli/ui/walk-screen.js +2 -2
- package/cli/version.js +1 -1
- package/cli/walk-reducer.js +2 -2
- package/cli/workspace-file.js +309 -6
- package/package.json +11 -11
- package/plugin/freeze.js +1 -1
- package/tsdoc-metadata.json +1 -1
package/cli/commands/export.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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,
|
package/cli/commands/preview.js
CHANGED
|
@@ -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 */
|
package/cli/commands/upgrade.js
CHANGED
|
@@ -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
|
package/cli/diff/render.js
CHANGED
package/cli/load-config.js
CHANGED
|
@@ -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
|
|
30
|
-
|
|
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
|
package/cli/render/legend.js
CHANGED
package/cli/render/print.js
CHANGED
package/cli/render/report.js
CHANGED
package/cli/render/tone.js
CHANGED
package/cli/resolve.js
CHANGED
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
|
package/cli/simulated-view.js
CHANGED
|
@@ -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
package/cli/ui/preview-screen.js
CHANGED
|
@@ -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";
|
package/cli/ui/progress-view.js
CHANGED
|
@@ -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 */
|
package/cli/ui/walk-screen.js
CHANGED
|
@@ -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
package/cli/walk-reducer.js
CHANGED
|
@@ -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
|
|
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
|
package/cli/workspace-file.js
CHANGED
|
@@ -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
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
"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.
|
|
39
|
-
"@effected/cli": "^0.
|
|
40
|
-
"@effected/env": "^0.
|
|
41
|
-
"@effected/glob": "^0.
|
|
42
|
-
"@effected/npm": "^0.
|
|
43
|
-
"@effected/semver": "^0.
|
|
44
|
-
"@effected/walker": "^0.
|
|
45
|
-
"@effected/workspaces": "^0.
|
|
46
|
-
"@effected/yaml": "^0.
|
|
47
|
-
"effect": "~4.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
|
/**
|