makersclaw 0.5.2 → 0.7.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.
Files changed (148) hide show
  1. package/README.md +73 -7
  2. package/package.json +3 -2
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +3 -3
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +3 -3
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +121 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +2 -2
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -5
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +65 -9
  117. package/src/install.mjs +360 -21
  118. package/src/lockfile.mjs +468 -0
  119. package/src/pm.mjs +473 -0
  120. package/src/registry.mjs +125 -1
  121. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  122. package/templates/next/browser/app/layout.jsx +94 -0
  123. package/templates/next/browser/app/page.jsx +60 -0
  124. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  125. package/templates/next/browser/lib/actions.js +16 -0
  126. package/templates/next/browser/lib/data.js +43 -0
  127. package/templates/next/files/README.md +818 -82
  128. package/templates/next/files/app/claw-alive/route.js +2 -2
  129. package/templates/next/files/app/frame.js +134 -9
  130. package/templates/next/files/app/layout.jsx +2 -2
  131. package/templates/next/files/check/run.mjs +1491 -53
  132. package/templates/next/files/components/ui/item.jsx +2 -2
  133. package/templates/next/files/components/ui/notice.jsx +1 -1
  134. package/templates/next/files/components/ui/separator.jsx +1 -1
  135. package/templates/next/files/lib/actions.js +171 -52
  136. package/templates/next/files/lib/brain-server.js +96 -0
  137. package/templates/next/files/lib/brain.js +413 -0
  138. package/templates/next/files/lib/claw.js +11 -0
  139. package/templates/next/files/lib/data.js +120 -30
  140. package/templates/next/files/lib/db.js +97 -17
  141. package/templates/next/files/lib/door.js +680 -0
  142. package/templates/next/files/next.config.mjs +85 -9
  143. package/templates/next/files/package.json +3 -3
  144. package/templates/next/files/{middleware.js → proxy.js} +13 -6
  145. package/templates/next/files/surfaces.js +3 -2
  146. package/templates/next/pnpm-lock.yaml +5870 -0
  147. package/templates/next/template.json +13 -6
  148. package/templates/next/files/schema.sql +0 -22
@@ -0,0 +1,468 @@
1
+ // The app's pnpm-lock.yaml, cut from the release's.
2
+ //
3
+ // Each template ships one lock beside its files (templates/next/
4
+ // pnpm-lock.yaml, made by `npm run lock`): the template's dependencies and
5
+ // every free block's, resolved together once, when the release is made.
6
+ // An app's lock is the part of it the app's package.json names. So every
7
+ // app on one version resolves every package the same way, whenever it is
8
+ // laid, and every app's packages are the same files in one pnpm store.
9
+ //
10
+ // Only the release lock is ever resolved. Cutting a part of it needs no
11
+ // registry and no pnpm: the lines of each entry are kept exactly as pnpm
12
+ // wrote them, in pnpm's order (code-unit order of the keys). The one field
13
+ // that depends on the rest of the graph, a snapshot's `optional: true`
14
+ // (reached only through optional edges), is worked out again for the part.
15
+ // Cutting the whole lock for the whole package.json gives it back byte for
16
+ // byte; test/lockfile.test.mjs holds that.
17
+ //
18
+ // What the release lock does not name (a package the app installed itself,
19
+ // a premium block's) is not the release's to lock. Its entries are carried
20
+ // over from the app's current lock unless a peer in them was resolved to a
21
+ // version the release does not hold (a hook library bound to another
22
+ // React); then it is left to the install, which resolves it against the
23
+ // release's versions and writes it in (src/pm.mjs installs without
24
+ // --frozen-lockfile then).
25
+ //
26
+ // No YAML library: the CLI has no dependencies. The reader takes pnpm's
27
+ // lockfile v9 as pnpm writes it and refuses anything else by name.
28
+
29
+ export const LOCKFILE = "pnpm-lock.yaml";
30
+
31
+ /** A lockfile this reader does not take. */
32
+ export class LockError extends Error {}
33
+
34
+ const SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
35
+ const TOP = ["lockfileVersion", "settings", "importers", "packages", "snapshots"];
36
+
37
+ const lex = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
38
+
39
+ /** One YAML scalar as pnpm's writer (js-yaml) prints it: plain, 'single'
40
+ * or "double" quoted. */
41
+ function scalar(raw) {
42
+ const s = raw.trim();
43
+ if (s.startsWith("'")) {
44
+ if (!s.endsWith("'") || s.length < 2) throw new LockError(`unreadable scalar ${s}`);
45
+ return s.slice(1, -1).replace(/''/g, "'");
46
+ }
47
+ if (s.startsWith('"')) {
48
+ try {
49
+ return JSON.parse(s);
50
+ } catch {
51
+ throw new LockError(`unreadable scalar ${s}`);
52
+ }
53
+ }
54
+ return s;
55
+ }
56
+
57
+ /** `key: value`, `key:` or `key: {}` after the indent → [key, value | null]. */
58
+ function keyValue(body) {
59
+ let key;
60
+ let rest;
61
+ if (body.startsWith("'")) {
62
+ let i = 1;
63
+ for (; i < body.length; i++) {
64
+ if (body[i] !== "'") continue;
65
+ if (body[i + 1] === "'") i++;
66
+ else break;
67
+ }
68
+ key = scalar(body.slice(0, i + 1));
69
+ rest = body.slice(i + 1);
70
+ } else if (body.startsWith('"')) {
71
+ const end = body.indexOf('":');
72
+ if (end < 0) throw new LockError(`unreadable key ${body}`);
73
+ key = scalar(body.slice(0, end + 1));
74
+ rest = body.slice(end + 1);
75
+ } else {
76
+ const m = /^(.*?)(:(?: .*)?)$/.exec(body);
77
+ if (!m || m[1].includes(": ")) throw new LockError(`unreadable line ${body}`);
78
+ key = m[1];
79
+ rest = m[2];
80
+ }
81
+ if (rest === ":") return [key, null];
82
+ if (!rest.startsWith(": ")) throw new LockError(`unreadable line ${body}`);
83
+ return [key, rest.slice(2)];
84
+ }
85
+
86
+ const indentOf = (line) => line.length - line.trimStart().length;
87
+
88
+ /** A dependency's reference → the snapshot key it points at (pnpm's
89
+ * refToRelative for lockfile v9): `7.0.0` under `is-number` is
90
+ * `is-number@7.0.0`; an alias's reference is already a key; a link is
91
+ * none. */
92
+ export function refToKey(name, ref) {
93
+ if (ref.startsWith("link:")) return null;
94
+ if (ref.startsWith("@")) return ref;
95
+ const at = ref.indexOf("@");
96
+ if (at === -1) return `${name}@${ref}`;
97
+ const paren = ref.indexOf("(");
98
+ const colon = ref.indexOf(":");
99
+ if ((paren === -1 || at < paren) && (colon === -1 || at < colon)) return ref;
100
+ return `${name}@${ref}`;
101
+ }
102
+
103
+ /** A snapshot key without its peer suffix: the key of its `packages` entry. */
104
+ export const packageKey = (key) => {
105
+ const paren = key.indexOf("(");
106
+ return paren === -1 ? key : key.slice(0, paren);
107
+ };
108
+
109
+ /** name@version → [name, version]; a scoped name keeps its first "@". */
110
+ const splitKey = (key) => {
111
+ const at = key.indexOf("@", 1);
112
+ return at === -1 ? [key, ""] : [key.slice(0, at), key.slice(at + 1)];
113
+ };
114
+
115
+ /** The peers a snapshot was resolved with, from its key's suffix:
116
+ * `a@1.0.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)` →
117
+ * [["react-dom", "19.3.0"], ["react", "19.3.0"]]. A patch hash is not a
118
+ * peer. */
119
+ export function peersOf(key) {
120
+ const out = [];
121
+ let depth = 0;
122
+ let start = -1;
123
+ for (let i = key.indexOf("("); i >= 0 && i < key.length; i++) {
124
+ if (key[i] === "(") {
125
+ if (depth++ === 0) start = i + 1;
126
+ } else if (key[i] === ")" && --depth === 0) {
127
+ const [name, version] = splitKey(packageKey(key.slice(start, i)));
128
+ if (version) out.push([name, version]);
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /** Split a section's lines into entries: a line at indent 2 opens one,
135
+ * blank lines separate them. */
136
+ function entries(lines, section) {
137
+ const out = [];
138
+ let cur = null;
139
+ for (const line of lines) {
140
+ if (line === "") {
141
+ cur = null;
142
+ continue;
143
+ }
144
+ const indent = indentOf(line);
145
+ if (indent === 2) {
146
+ cur = { head: line, lines: [line] };
147
+ out.push(cur);
148
+ } else if (indent >= 4 && cur) {
149
+ cur.lines.push(line);
150
+ } else {
151
+ throw new LockError(`${section}: unexpected line "${line}"`);
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+
157
+ /**
158
+ * Read a pnpm lockfile v9 → {
159
+ * head the lockfileVersion and settings lines, as written
160
+ * importer section → Map(name → { specifier, version, lines }) for "."
161
+ * packages Map(key → lines)
162
+ * snapshots Map(key → { lines, deps: [{ name, key, optional }] })
163
+ * }. Throws a LockError for anything else: another lockfile version, a
164
+ * workspace, overrides, patches, catalogs, a block scalar.
165
+ */
166
+ export function parseLock(text) {
167
+ const lines = String(text).replace(/\r\n/g, "\n").split("\n");
168
+ if (lines.at(-1) === "") lines.pop();
169
+ const tops = new Map();
170
+ let cur = null;
171
+ for (const line of lines) {
172
+ if (line !== "" && indentOf(line) === 0) {
173
+ const [key, value] = keyValue(line);
174
+ if (!TOP.includes(key)) throw new LockError(`the lockfile has "${key}", which makersclaw does not read`);
175
+ if (tops.has(key)) throw new LockError(`the lockfile has "${key}" twice`);
176
+ cur = { value, lines: [] };
177
+ tops.set(key, cur);
178
+ } else if (cur) {
179
+ cur.lines.push(line);
180
+ } else if (line !== "") {
181
+ throw new LockError(`unexpected line "${line}"`);
182
+ }
183
+ }
184
+ if (tops.get("lockfileVersion")?.value !== "'9.0'") {
185
+ throw new LockError(`lockfileVersion is ${tops.get("lockfileVersion")?.value ?? "missing"}; makersclaw reads 9.0 (pnpm 9 and 10)`);
186
+ }
187
+ for (const line of lines) if (/:\s[|>][-+0-9]*$/.test(line)) throw new LockError(`a block scalar: ${line.trim()}`);
188
+
189
+ const settings = tops.get("settings");
190
+ const head = ["lockfileVersion: '9.0'", ""];
191
+ if (settings) {
192
+ if (settings.value !== null) throw new LockError("settings is not a mapping");
193
+ head.push("settings:", ...settings.lines.filter((l) => l !== ""), "");
194
+ }
195
+
196
+ const importer = new Map(SECTIONS.map((s) => [s, new Map()]));
197
+ const imp = tops.get("importers");
198
+ if (imp) {
199
+ const list = entries(imp.lines, "importers");
200
+ for (const e of list) {
201
+ const [id, v] = keyValue(e.head.slice(2));
202
+ if (id !== ".") throw new LockError(`importer "${id}": makersclaw reads one project, not a workspace`);
203
+ if (v === "{}") continue;
204
+ if (v !== null) throw new LockError(`importer "." is not a mapping`);
205
+ let section = null;
206
+ let dep = null;
207
+ for (const line of e.lines.slice(1)) {
208
+ const indent = indentOf(line);
209
+ const [k, val] = keyValue(line.slice(indent));
210
+ if (indent === 4) {
211
+ if (!SECTIONS.includes(k) || val !== null) throw new LockError(`importer ".": "${k}" is not read by makersclaw`);
212
+ section = importer.get(k);
213
+ dep = null;
214
+ } else if (indent === 6 && section && val === null) {
215
+ dep = { specifier: null, version: null, lines: [line] };
216
+ section.set(k, dep);
217
+ } else if (indent === 8 && dep && (k === "specifier" || k === "version") && val !== null) {
218
+ dep[k] = scalar(val);
219
+ dep.lines.push(line);
220
+ } else {
221
+ throw new LockError(`importer ".": unexpected line "${line}"`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ for (const [s, deps] of importer) {
227
+ for (const [name, d] of deps) if (d.specifier === null || d.version === null) throw new LockError(`importer ".": ${s}.${name} lacks a specifier or a version`);
228
+ }
229
+
230
+ const packages = new Map();
231
+ const pk = tops.get("packages");
232
+ if (pk && pk.value !== "{}") {
233
+ for (const e of entries(pk.lines, "packages")) packages.set(keyValue(e.head.slice(2))[0], e.lines);
234
+ }
235
+
236
+ const snapshots = new Map();
237
+ const sn = tops.get("snapshots");
238
+ if (sn && sn.value !== "{}") {
239
+ for (const e of entries(sn.lines, "snapshots")) {
240
+ const [key] = keyValue(e.head.slice(2));
241
+ const deps = [];
242
+ let edge = null;
243
+ for (const line of e.lines.slice(1)) {
244
+ const indent = indentOf(line);
245
+ if (indent === 4) {
246
+ const [k] = keyValue(line.slice(4));
247
+ edge = k === "dependencies" ? false : k === "optionalDependencies" ? true : null;
248
+ } else if (indent === 6 && edge !== null) {
249
+ const [name, ref] = keyValue(line.slice(6));
250
+ const target = refToKey(name, scalar(ref ?? ""));
251
+ if (target) deps.push({ name, key: target, optional: edge });
252
+ }
253
+ }
254
+ snapshots.set(key, { lines: e.lines, deps });
255
+ }
256
+ }
257
+ return { head, importer, packages, snapshots };
258
+ }
259
+
260
+ /** The app's direct dependencies, by section, from its package.json. */
261
+ export function manifestDeps(pkg) {
262
+ return new Map(SECTIONS.map((s) => [s, new Map(Object.entries(pkg?.[s] ?? {}))]));
263
+ }
264
+
265
+ /** Does the lock name exactly the app's dependencies, specifier for
266
+ * specifier? pnpm installs --frozen-lockfile only then. Returns the first
267
+ * difference, or null when it matches. */
268
+ export function lockDifference(lock, pkg) {
269
+ const want = manifestDeps(pkg);
270
+ for (const s of SECTIONS) {
271
+ const have = lock.importer.get(s);
272
+ for (const [name, spec] of want.get(s)) {
273
+ const d = have.get(name);
274
+ if (!d) return `${s}.${name} is not in ${LOCKFILE}`;
275
+ if (d.specifier !== spec) return `${s}.${name} is ${spec} in package.json, ${d.specifier} in ${LOCKFILE}`;
276
+ }
277
+ for (const name of have.keys()) if (!want.get(s).has(name)) return `${s}.${name} is in ${LOCKFILE}, not in package.json`;
278
+ }
279
+ return null;
280
+ }
281
+
282
+ /** Every snapshot reached from `roots` ([{ key, optional }]); `required`
283
+ * holds those reached without crossing an optional edge — every other one
284
+ * is `optional: true`. A key with no snapshot is listed in `missing`. */
285
+ function reach(snapshots, roots) {
286
+ const all = new Set();
287
+ const required = new Set();
288
+ const missing = new Set();
289
+ const walk = (start, set, requiredOnly) => {
290
+ const stack = [...start];
291
+ while (stack.length) {
292
+ const key = stack.pop();
293
+ if (set.has(key)) continue;
294
+ const snap = snapshots.get(key);
295
+ if (!snap) {
296
+ missing.add(key);
297
+ continue;
298
+ }
299
+ set.add(key);
300
+ for (const d of snap.deps) if (!(requiredOnly && d.optional)) stack.push(d.key);
301
+ }
302
+ };
303
+ walk(roots.map((r) => r.key), all, false);
304
+ walk(roots.filter((r) => !r.optional).map((r) => r.key), required, true);
305
+ return { all, required, missing };
306
+ }
307
+
308
+ /** A snapshot's lines with `optional: true` set or cleared. pnpm writes it
309
+ * last; an entry with nothing else is `key: {}`. */
310
+ function withOptional(lines, optional) {
311
+ const [head, ...body] = lines;
312
+ const rest = body.filter((l) => l !== " optional: true");
313
+ const key = head.replace(/: \{\}$/, ":");
314
+ if (optional) return [key, ...rest, " optional: true"];
315
+ return rest.length ? [key, ...rest] : [`${key.replace(/:$/, "")}: {}`];
316
+ }
317
+
318
+ const sectionOf = (s) => (s === "optionalDependencies" ? true : false);
319
+
320
+ /**
321
+ * The app's lock: what the release lock resolves for the app's
322
+ * package.json, plus what the app's current lock resolves for the rest,
323
+ * when that cannot clash.
324
+ *
325
+ * release parseLock() of the template's release lock
326
+ * pkg the app's package.json
327
+ * current parseLock() of the app's own lock, or null
328
+ *
329
+ * Returns { text, covered, carried, left }: covered — names the release
330
+ * locks; carried — names kept from the current lock; left — names neither
331
+ * locks, for the install to resolve (the lock then does not match
332
+ * package.json, and the install is not frozen).
333
+ */
334
+ export function deriveLock(release, pkg, current = null) {
335
+ const want = manifestDeps(pkg);
336
+ const covered = [];
337
+ const carryWanted = [];
338
+ const left = [];
339
+ const importer = new Map(SECTIONS.map((s) => [s, new Map()]));
340
+ const roots = [];
341
+
342
+ const inRelease = (name) => {
343
+ for (const s of SECTIONS) if (release.importer.get(s).has(name)) return release.importer.get(s).get(name);
344
+ return null;
345
+ };
346
+ for (const s of SECTIONS) {
347
+ for (const [name, spec] of want.get(s)) {
348
+ const r = inRelease(name);
349
+ if (r && r.specifier === spec) {
350
+ importer.get(s).set(name, r);
351
+ roots.push({ key: refToKey(name, r.version), optional: sectionOf(s) });
352
+ covered.push(name);
353
+ continue;
354
+ }
355
+ const c = current?.importer.get(s).get(name);
356
+ if (c && c.specifier === spec) carryWanted.push({ s, name, dep: c });
357
+ else left.push(name);
358
+ }
359
+ }
360
+
361
+ const fromRelease = reach(release.snapshots, roots);
362
+ if (fromRelease.missing.size) throw new LockError(`the release lock has no snapshot for ${[...fromRelease.missing].join(", ")}`);
363
+
364
+ // A carried package must not hold a peer the release resolves to
365
+ // another version: a hook library carried with React 19.2 beside the
366
+ // release's 19.3 gets its own React, and two Reacts break every hook.
367
+ // A second version of an ordinary dependency is fine (npm and pnpm both
368
+ // nest one), and what the release does not hold at all is the app's.
369
+ const releaseVersions = new Map();
370
+ for (const key of fromRelease.all) {
371
+ const [name, v] = splitKey(packageKey(key));
372
+ releaseVersions.set(name, (releaseVersions.get(name) ?? new Set()).add(v));
373
+ }
374
+ const carried = [];
375
+ const carriedRoots = [];
376
+ for (const c of carryWanted) {
377
+ const root = { key: refToKey(c.name, c.dep.version), optional: sectionOf(c.s) };
378
+ const r = reach(current.snapshots, [root]);
379
+ const clash =
380
+ r.missing.size > 0 ||
381
+ [...r.all].some((key) => peersOf(key).some(([name, v]) => releaseVersions.has(name) && !releaseVersions.get(name).has(v)));
382
+ if (clash) {
383
+ left.push(c.name);
384
+ continue;
385
+ }
386
+ importer.get(c.s).set(c.name, c.dep);
387
+ carriedRoots.push(root);
388
+ carried.push(c.name);
389
+ }
390
+
391
+ const snapshots = new Map();
392
+ for (const key of fromRelease.all) snapshots.set(key, release.snapshots.get(key));
393
+ for (const key of reach(current?.snapshots ?? new Map(), carriedRoots).all) if (!snapshots.has(key)) snapshots.set(key, current.snapshots.get(key));
394
+ const { required } = reach(snapshots, [...roots, ...carriedRoots]);
395
+ const lines = new Map([...snapshots].map(([key, snap]) => [key, withOptional(snap.lines, !required.has(key))]));
396
+ const text = emit(release.head, importer, lines, (key) => release.packages.get(key) ?? current?.packages.get(key));
397
+ return { text, covered: covered.sort(lex), carried: carried.sort(lex), left: left.sort(lex) };
398
+ }
399
+
400
+ /** A lockfile as pnpm writes it: the head, the importer's sections, then
401
+ * every package and every snapshot in code-unit order, a blank line
402
+ * between entries. `packageLines(key)` gives a packages entry. */
403
+ function emit(head, importer, snapshotLines, packageLines) {
404
+ const out = [...head, "importers:", ""];
405
+ const deps = SECTIONS.filter((s) => importer.get(s).size);
406
+ if (deps.length) {
407
+ out.push(" .:");
408
+ for (const s of deps) {
409
+ out.push(` ${s}:`);
410
+ for (const name of [...importer.get(s).keys()].sort(lex)) out.push(...importer.get(s).get(name).lines);
411
+ }
412
+ } else {
413
+ out.push(" .: {}");
414
+ }
415
+ out.push("");
416
+ const keys = [...snapshotLines.keys()].sort(lex);
417
+ const pkgKeys = [...new Set(keys.map(packageKey))].sort(lex);
418
+ if (pkgKeys.length) {
419
+ out.push("packages:", "");
420
+ for (const key of pkgKeys) {
421
+ const lines = packageLines(key);
422
+ if (!lines) throw new LockError(`no packages entry for ${key}`);
423
+ out.push(...lines, "");
424
+ }
425
+ out.push("snapshots:", "");
426
+ for (const key of keys) out.push(...snapshotLines.get(key), "");
427
+ } else {
428
+ out.push("packages: {}", "", "snapshots: {}", "");
429
+ }
430
+ // pnpm ends the file on the last entry's newline, not a blank line.
431
+ while (out.at(-1) === "") out.pop();
432
+ return `${out.join("\n")}\n`;
433
+ }
434
+
435
+ /**
436
+ * The lock without the snapshots in `drop`, and without every edge to
437
+ * them: what pnpm's current lockfile (node_modules/.pnpm/lock.yaml) would
438
+ * say had those packages never been locked. src/pm.mjs drops the ones
439
+ * pnpm skipped as another platform's; see trimCurrentLock there.
440
+ */
441
+ export function dropSnapshots(lock, drop) {
442
+ const kept = new Map();
443
+ for (const [key, snap] of lock.snapshots) {
444
+ if (drop.has(key)) continue;
445
+ const [head, ...body] = snap.lines;
446
+ const out = [];
447
+ let section = null;
448
+ for (const line of body) {
449
+ const indent = indentOf(line);
450
+ if (indent === 4) {
451
+ section = keyValue(line.slice(4))[0];
452
+ out.push(line);
453
+ } else if (indent === 6 && (section === "dependencies" || section === "optionalDependencies")) {
454
+ const [name, ref] = keyValue(line.slice(6));
455
+ if (!drop.has(refToKey(name, scalar(ref ?? "")))) out.push(line);
456
+ } else {
457
+ out.push(line);
458
+ }
459
+ }
460
+ // A section header left with nothing under it goes too.
461
+ const tidy = out.filter((line, i) => !(indentOf(line) === 4 && line.endsWith(":") && !(out[i + 1] && indentOf(out[i + 1]) > 4)));
462
+ kept.set(key, tidy.length ? [head.replace(/: \{\}$/, ":"), ...tidy] : [`${head.replace(/: \{\}$/, "").replace(/:$/, "")}: {}`]);
463
+ }
464
+ const importer = new Map(
465
+ SECTIONS.map((s) => [s, new Map([...lock.importer.get(s)].filter(([name, d]) => !drop.has(refToKey(name, d.version))))]),
466
+ );
467
+ return emit(lock.head, importer, kept, (key) => lock.packages.get(key));
468
+ }