@getrefino/onboarding 0.1.0-rc.3 → 0.1.0-rc.5
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/dist/constants.d.ts +12 -0
- package/dist/constants.js +19 -0
- package/dist/generated.d.ts +8 -0
- package/dist/generated.js +25 -5
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +14 -0
- package/dist/integration-update.d.ts +83 -0
- package/dist/integration-update.js +281 -0
- package/dist/lockfiles.d.ts +73 -0
- package/dist/lockfiles.js +443 -0
- package/dist/plan.d.ts +1 -12
- package/dist/plan.js +2 -13
- package/dist/registry.d.ts +42 -0
- package/dist/registry.js +111 -0
- package/dist/semver.d.ts +14 -0
- package/dist/semver.js +64 -0
- package/dist/template-file.js +1 -1
- package/dist/templates-hosted.d.ts +2 -2
- package/dist/templates-hosted.js +35 -6
- package/dist/templates-legacy.d.ts +11 -1
- package/dist/templates-legacy.js +256 -1
- package/dist/templates.js +1 -1
- package/dist/verify.js +75 -8
- package/package.json +2 -2
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving `@getrefino/*` from one release to another inside an existing
|
|
3
|
+
* lockfile, without running a package manager.
|
|
4
|
+
*
|
|
5
|
+
* This is only sound because of a property Refino's runtime packages keep on
|
|
6
|
+
* purpose: they have no third-party dependencies, so the dependency graph of
|
|
7
|
+
* a site does not change shape when they move. Under that condition a
|
|
8
|
+
* package manager's own upgrade rewrites nothing but the Refino entries'
|
|
9
|
+
* version, tarball and integrity (verified against real npm 11, pnpm 11 and
|
|
10
|
+
* yarn 1 output in lockfiles.test.ts), and so can this. The caller proves the
|
|
11
|
+
* condition from the registry manifests (`releaseShapeChange`) before asking.
|
|
12
|
+
*
|
|
13
|
+
* Everything else refuses rather than guesses:
|
|
14
|
+
*
|
|
15
|
+
* - only lockfile formats whose layout is pinned here (npm v3, pnpm v9,
|
|
16
|
+
* yarn classic v1);
|
|
17
|
+
* - every line (or JSON value) that mentions a package being moved must be
|
|
18
|
+
* one this code consumed, so a second workspace, an override, a patch or
|
|
19
|
+
* a peer that names Refino stops the update instead of being left behind;
|
|
20
|
+
* - the old entries must be the registry's (same integrity, same tarball),
|
|
21
|
+
* so a mirror or a local build is never silently swapped for npmjs;
|
|
22
|
+
* - npm's JSON must re-serialize to the exact bytes it was read from before
|
|
23
|
+
* anything changes, so formatting is never rewritten.
|
|
24
|
+
*/
|
|
25
|
+
/** The lockfile names this module understands, and the ones it recognises only to refuse. */
|
|
26
|
+
export const LOCKFILES = {
|
|
27
|
+
"package-lock.json": "npm",
|
|
28
|
+
"pnpm-lock.yaml": "pnpm",
|
|
29
|
+
"yarn.lock": "yarn",
|
|
30
|
+
"npm-shrinkwrap.json": "unsupported",
|
|
31
|
+
"bun.lockb": "unsupported",
|
|
32
|
+
"bun.lock": "unsupported",
|
|
33
|
+
};
|
|
34
|
+
class Refusal extends Error {
|
|
35
|
+
}
|
|
36
|
+
function refuse(reason) {
|
|
37
|
+
throw new Refusal(reason);
|
|
38
|
+
}
|
|
39
|
+
function attempt(run) {
|
|
40
|
+
try {
|
|
41
|
+
return { ok: true, text: run() };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Refusal)
|
|
45
|
+
return { ok: false, reason: error.message };
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** `0.1.0-rc.3` or `0.1.0-rc.3(react-dom@19.1.0(react@19.1.0))`: the version followed by a peer suffix or nothing. */
|
|
50
|
+
function withVersion(value, from, to, what) {
|
|
51
|
+
if (value === from || value.startsWith(`${from}(`))
|
|
52
|
+
return `${to}${value.slice(from.length)}`;
|
|
53
|
+
return refuse(`${what} is locked at ${value}, not ${from}.`);
|
|
54
|
+
}
|
|
55
|
+
/** Refuse when a line mentions a moved package and nothing above consumed it. */
|
|
56
|
+
function assertAccounted(lines, handled, names) {
|
|
57
|
+
lines.forEach((line, index) => {
|
|
58
|
+
if (handled.has(index))
|
|
59
|
+
return;
|
|
60
|
+
const name = names.find((candidate) => line.includes(`${candidate}@`) || line.includes(`'${candidate}'`) || line.includes(`"${candidate}"`));
|
|
61
|
+
if (name)
|
|
62
|
+
refuse(`The lockfile refers to ${name} somewhere Refino does not update (line ${index + 1}), for example another workspace, an override or a patch.`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function assertSeen(seen, expected, where) {
|
|
66
|
+
const missing = expected.filter((name) => !seen.has(name));
|
|
67
|
+
if (missing.length > 0)
|
|
68
|
+
refuse(`${missing.join(", ")} not found in the lockfile's ${where}.`);
|
|
69
|
+
}
|
|
70
|
+
function unquote(key) {
|
|
71
|
+
return key.replace(/^(['"])(.*)\1$/, "$2");
|
|
72
|
+
}
|
|
73
|
+
function isObject(value) {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
function detectIndent(text) {
|
|
77
|
+
return /^\{\n( +|\t+)"/.exec(text)?.[1] ?? " ";
|
|
78
|
+
}
|
|
79
|
+
const DEPENDENCY_GROUPS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
80
|
+
function rewriteNpm(text, request) {
|
|
81
|
+
let lock;
|
|
82
|
+
try {
|
|
83
|
+
lock = JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return refuse("package-lock.json is not valid JSON.");
|
|
87
|
+
}
|
|
88
|
+
if (!isObject(lock) || lock.lockfileVersion !== 3 || !isObject(lock.packages))
|
|
89
|
+
return refuse("Only package-lock.json version 3 (npm 9 and later) is supported.");
|
|
90
|
+
const indent = detectIndent(text);
|
|
91
|
+
const serialize = (value) => `${JSON.stringify(value, null, indent)}\n`;
|
|
92
|
+
if (serialize(lock) !== text)
|
|
93
|
+
return refuse("package-lock.json is not formatted the way npm writes it.");
|
|
94
|
+
const names = request.packages;
|
|
95
|
+
const appKey = request.importer === "." ? "" : request.importer;
|
|
96
|
+
const seenInstalled = new Set();
|
|
97
|
+
const seenSpecs = new Set();
|
|
98
|
+
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
99
|
+
if (!isObject(entry))
|
|
100
|
+
continue;
|
|
101
|
+
const installed = names.find((name) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`));
|
|
102
|
+
if (installed) {
|
|
103
|
+
const prefix = key.slice(0, key.length - `node_modules/${installed}`.length);
|
|
104
|
+
if (prefix !== "" && prefix !== `${appKey}/`)
|
|
105
|
+
refuse(`${installed} is also installed for ${prefix.replace(/\/$/, "")}.`);
|
|
106
|
+
const from = request.fromReleases[installed];
|
|
107
|
+
const to = request.toReleases[installed];
|
|
108
|
+
if (entry.link === true || entry.version !== request.from)
|
|
109
|
+
refuse(`${installed} is locked at ${String(entry.version)}, not ${request.from}.`);
|
|
110
|
+
if (entry.resolved !== from.tarball || entry.integrity !== from.integrity)
|
|
111
|
+
refuse(`${installed} ${request.from} in the lockfile did not come from the public npm registry.`);
|
|
112
|
+
entry.version = request.to;
|
|
113
|
+
entry.resolved = to.tarball;
|
|
114
|
+
entry.integrity = to.integrity;
|
|
115
|
+
for (const group of DEPENDENCY_GROUPS) {
|
|
116
|
+
const deps = entry[group];
|
|
117
|
+
if (!isObject(deps))
|
|
118
|
+
continue;
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
if (deps[name] === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
if (deps[name] !== request.from)
|
|
123
|
+
refuse(`${installed} ${request.from} depends on ${name} ${String(deps[name])}.`);
|
|
124
|
+
deps[name] = request.to;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
seenInstalled.add(installed);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const isWorkspace = !key.includes("node_modules/");
|
|
131
|
+
for (const group of DEPENDENCY_GROUPS) {
|
|
132
|
+
const deps = entry[group];
|
|
133
|
+
if (!isObject(deps))
|
|
134
|
+
continue;
|
|
135
|
+
for (const name of names) {
|
|
136
|
+
if (deps[name] === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
if (!isWorkspace || key !== appKey)
|
|
139
|
+
refuse(`${key || "the repository root"} also depends on ${name}.`);
|
|
140
|
+
const spec = request.specs[name];
|
|
141
|
+
if (!spec || deps[name] !== spec.from)
|
|
142
|
+
refuse(`The lockfile records ${name} as ${String(deps[name])}, but package.json says ${spec?.from ?? "nothing"}.`);
|
|
143
|
+
deps[name] = spec.to;
|
|
144
|
+
seenSpecs.add(name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
assertSeen(seenInstalled, names, "installed packages");
|
|
149
|
+
assertSeen(seenSpecs, Object.keys(request.specs), "dependencies of the app");
|
|
150
|
+
const next = serialize(lock);
|
|
151
|
+
// The safety net for JSON: every mention of a moved package must be one of the values replaced above.
|
|
152
|
+
const residue = names.filter((name) => next.includes(`${name}/-/${name.split("/")[1]}-${request.from}.tgz`) && request.from !== request.to);
|
|
153
|
+
if (residue.length > 0)
|
|
154
|
+
refuse(`The lockfile still refers to ${residue.join(", ")} ${request.from}.`);
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
function readNpm(text, importer, names) {
|
|
158
|
+
let lock;
|
|
159
|
+
try {
|
|
160
|
+
lock = JSON.parse(text);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return refuse("package-lock.json is not valid JSON.");
|
|
164
|
+
}
|
|
165
|
+
if (!isObject(lock) || !isObject(lock.packages))
|
|
166
|
+
return refuse("package-lock.json has no packages section.");
|
|
167
|
+
const packages = lock.packages;
|
|
168
|
+
const appKey = importer === "." ? "" : importer;
|
|
169
|
+
const versions = {};
|
|
170
|
+
for (const name of names) {
|
|
171
|
+
// Node resolves from the app outwards: a copy nested under the app wins over a hoisted one.
|
|
172
|
+
const entry = packages[appKey ? `${appKey}/node_modules/${name}` : `node_modules/${name}`] ?? packages[`node_modules/${name}`];
|
|
173
|
+
if (isObject(entry) && typeof entry.version === "string")
|
|
174
|
+
versions[name] = entry.version;
|
|
175
|
+
}
|
|
176
|
+
return versions;
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// pnpm (lockfileVersion 9)
|
|
180
|
+
function indentOf(line) {
|
|
181
|
+
return line.length - line.trimStart().length;
|
|
182
|
+
}
|
|
183
|
+
function keyOf(line) {
|
|
184
|
+
return unquote(line.trim().replace(/:(\s.*)?$/, ""));
|
|
185
|
+
}
|
|
186
|
+
function checkPnpmVersion(first) {
|
|
187
|
+
if (!first || !/^lockfileVersion: '9\.\d+'$/.test(first))
|
|
188
|
+
refuse("Only pnpm lockfile version 9 (pnpm 9 and later) is supported.");
|
|
189
|
+
}
|
|
190
|
+
function rewritePnpm(text, request) {
|
|
191
|
+
const lines = text.split("\n");
|
|
192
|
+
checkPnpmVersion(lines[0]);
|
|
193
|
+
const names = request.packages;
|
|
194
|
+
const handled = new Set([0]);
|
|
195
|
+
const seen = { importer: new Set(), packages: new Set(), snapshots: new Set() };
|
|
196
|
+
let section = "";
|
|
197
|
+
let importer = null;
|
|
198
|
+
let dependency = null;
|
|
199
|
+
let block = null;
|
|
200
|
+
const entryKey = /^ {2}'(@[^@']+\/[^@']+)@([^'(]+)(\(.*\))?'(:.*)$/;
|
|
201
|
+
lines.forEach((line, index) => {
|
|
202
|
+
if (line.trim() === "")
|
|
203
|
+
return;
|
|
204
|
+
const indent = indentOf(line);
|
|
205
|
+
if (indent === 0) {
|
|
206
|
+
section = keyOf(line);
|
|
207
|
+
importer = dependency = null;
|
|
208
|
+
block = null;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (section === "importers") {
|
|
212
|
+
if (indent === 2) {
|
|
213
|
+
importer = keyOf(line);
|
|
214
|
+
dependency = null;
|
|
215
|
+
}
|
|
216
|
+
else if (indent === 6) {
|
|
217
|
+
dependency = keyOf(line);
|
|
218
|
+
if (names.includes(dependency)) {
|
|
219
|
+
if (importer !== request.importer)
|
|
220
|
+
refuse(`Workspace ${importer} also depends on ${dependency}.`);
|
|
221
|
+
handled.add(index);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (indent === 8 && dependency && names.includes(dependency)) {
|
|
225
|
+
const match = /^(\s+)(specifier|version): (.+)$/.exec(line);
|
|
226
|
+
if (!match)
|
|
227
|
+
return;
|
|
228
|
+
const [, pad, field, value] = match;
|
|
229
|
+
if (field === "specifier") {
|
|
230
|
+
const spec = request.specs[dependency];
|
|
231
|
+
if (!spec || value !== spec.from)
|
|
232
|
+
refuse(`The lockfile records ${dependency} as ${value}, but package.json says ${spec?.from ?? "nothing"}.`);
|
|
233
|
+
lines[index] = `${pad}specifier: ${spec.to}`;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
lines[index] = `${pad}version: ${withVersion(value, request.from, request.to, dependency)}`;
|
|
237
|
+
seen.importer.add(dependency);
|
|
238
|
+
}
|
|
239
|
+
handled.add(index);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (section !== "packages" && section !== "snapshots")
|
|
244
|
+
return;
|
|
245
|
+
if (indent === 2) {
|
|
246
|
+
const match = entryKey.exec(line);
|
|
247
|
+
block = null;
|
|
248
|
+
if (!match || !names.includes(match[1]))
|
|
249
|
+
return;
|
|
250
|
+
const [, name, version, peers = "", rest] = match;
|
|
251
|
+
if (version !== request.from)
|
|
252
|
+
refuse(`The lockfile also holds ${name} ${version}.`);
|
|
253
|
+
lines[index] = ` '${name}@${request.to}${peers}'${rest}`;
|
|
254
|
+
handled.add(index);
|
|
255
|
+
seen[section].add(name);
|
|
256
|
+
block = { name };
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (section === "packages" && block && indent === 4 && line.trimStart().startsWith("resolution:")) {
|
|
260
|
+
const from = request.fromReleases[block.name];
|
|
261
|
+
if (line !== ` resolution: {integrity: ${from.integrity}}`)
|
|
262
|
+
refuse(`${block.name} ${request.from} in the lockfile did not come from the public npm registry.`);
|
|
263
|
+
lines[index] = ` resolution: {integrity: ${request.toReleases[block.name].integrity}}`;
|
|
264
|
+
handled.add(index);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (section === "snapshots" && block && indent === 6) {
|
|
268
|
+
const match = /^(\s+)'(@[^']+)': (.+)$/.exec(line);
|
|
269
|
+
if (!match || !names.includes(match[2]))
|
|
270
|
+
return;
|
|
271
|
+
lines[index] = `${match[1]}'${match[2]}': ${withVersion(match[3], request.from, request.to, match[2])}`;
|
|
272
|
+
handled.add(index);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
assertAccounted(lines, handled, names);
|
|
276
|
+
assertSeen(seen.importer, Object.keys(request.specs), "importer");
|
|
277
|
+
assertSeen(seen.packages, names, "packages");
|
|
278
|
+
assertSeen(seen.snapshots, names, "snapshots");
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
281
|
+
function readPnpm(text, importer, names) {
|
|
282
|
+
const lines = text.split("\n");
|
|
283
|
+
checkPnpmVersion(lines[0]);
|
|
284
|
+
const versions = {};
|
|
285
|
+
let section = "";
|
|
286
|
+
let current = null;
|
|
287
|
+
let dependency = null;
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
if (line.trim() === "")
|
|
290
|
+
continue;
|
|
291
|
+
const indent = indentOf(line);
|
|
292
|
+
if (indent === 0)
|
|
293
|
+
section = keyOf(line);
|
|
294
|
+
if (section !== "importers")
|
|
295
|
+
continue;
|
|
296
|
+
if (indent === 2)
|
|
297
|
+
current = keyOf(line);
|
|
298
|
+
else if (indent === 6)
|
|
299
|
+
dependency = keyOf(line);
|
|
300
|
+
else if (indent === 8 && current === importer && dependency && names.includes(dependency)) {
|
|
301
|
+
const version = /^\s+version: ([^(\s]+)/.exec(line)?.[1];
|
|
302
|
+
if (version)
|
|
303
|
+
versions[dependency] = version;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return versions;
|
|
307
|
+
}
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// yarn classic (lockfile v1)
|
|
310
|
+
function checkYarnVersion(text) {
|
|
311
|
+
if (!text.includes("# yarn lockfile v1") || /^__metadata:/m.test(text))
|
|
312
|
+
refuse("Only yarn classic (yarn 1) lockfiles are supported.");
|
|
313
|
+
}
|
|
314
|
+
function parseDescriptor(raw) {
|
|
315
|
+
const descriptor = unquote(raw.trim());
|
|
316
|
+
const at = descriptor.lastIndexOf("@");
|
|
317
|
+
if (at <= 0)
|
|
318
|
+
return null;
|
|
319
|
+
return { name: descriptor.slice(0, at), range: descriptor.slice(at + 1) };
|
|
320
|
+
}
|
|
321
|
+
function rewriteYarn(text, request) {
|
|
322
|
+
checkYarnVersion(text);
|
|
323
|
+
const lines = text.split("\n");
|
|
324
|
+
const names = request.packages;
|
|
325
|
+
const handled = new Set();
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
let block = null;
|
|
328
|
+
lines.forEach((line, index) => {
|
|
329
|
+
if (line.trim() === "" || line.startsWith("#"))
|
|
330
|
+
return;
|
|
331
|
+
if (indentOf(line) === 0) {
|
|
332
|
+
block = null;
|
|
333
|
+
const descriptors = line.replace(/:$/, "").split(/,\s*/).map(parseDescriptor);
|
|
334
|
+
const name = descriptors[0]?.name;
|
|
335
|
+
if (!name || !names.includes(name))
|
|
336
|
+
return;
|
|
337
|
+
const spec = request.specs[name];
|
|
338
|
+
const next = descriptors.map((descriptor) => {
|
|
339
|
+
if (!descriptor || descriptor.name !== name)
|
|
340
|
+
return refuse(`Unexpected lockfile entry: ${line}`);
|
|
341
|
+
if (spec && descriptor.range === spec.from)
|
|
342
|
+
return spec.to;
|
|
343
|
+
// Refino packages depend on each other at one exact version.
|
|
344
|
+
if (descriptor.range === request.from)
|
|
345
|
+
return request.to;
|
|
346
|
+
return refuse(`Something else in the repository asks for ${name}@${descriptor.range}.`);
|
|
347
|
+
});
|
|
348
|
+
lines[index] = `${[...new Set(next)].map((range) => `"${name}@${range}"`).sort().join(", ")}:`;
|
|
349
|
+
handled.add(index);
|
|
350
|
+
seen.add(name);
|
|
351
|
+
block = name;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!block)
|
|
355
|
+
return;
|
|
356
|
+
const from = request.fromReleases[block];
|
|
357
|
+
const to = request.toReleases[block];
|
|
358
|
+
const field = /^ {2}(version|resolved|integrity) (.+)$/.exec(line);
|
|
359
|
+
if (field) {
|
|
360
|
+
const [, key, value] = field;
|
|
361
|
+
if (key === "version") {
|
|
362
|
+
if (value !== `"${request.from}"`)
|
|
363
|
+
refuse(`${block} is locked at ${value}, not ${request.from}.`);
|
|
364
|
+
lines[index] = ` version "${request.to}"`;
|
|
365
|
+
}
|
|
366
|
+
else if (key === "resolved") {
|
|
367
|
+
const match = /^"(https:\/\/registry\.(?:yarnpkg\.com|npmjs\.org))\/(.+)#([0-9a-f]{40})"$/.exec(value);
|
|
368
|
+
const path = new URL(from.tarball).pathname.slice(1);
|
|
369
|
+
if (!match || match[2] !== path || match[3] !== from.shasum)
|
|
370
|
+
refuse(`${block} ${request.from} in the lockfile did not come from the public npm registry.`);
|
|
371
|
+
lines[index] = ` resolved "${match[1]}/${new URL(to.tarball).pathname.slice(1)}#${to.shasum}"`;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
if (value !== from.integrity)
|
|
375
|
+
refuse(`${block} ${request.from} in the lockfile did not come from the public npm registry.`);
|
|
376
|
+
lines[index] = ` integrity ${to.integrity}`;
|
|
377
|
+
}
|
|
378
|
+
handled.add(index);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const dependency = /^ {4}"(@[^"]+)" "([^"]+)"$/.exec(line);
|
|
382
|
+
if (dependency && names.includes(dependency[1])) {
|
|
383
|
+
if (dependency[2] !== request.from)
|
|
384
|
+
refuse(`${block} depends on ${dependency[1]} ${dependency[2]}.`);
|
|
385
|
+
lines[index] = ` "${dependency[1]}" "${request.to}"`;
|
|
386
|
+
handled.add(index);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
assertAccounted(lines, handled, names);
|
|
390
|
+
assertSeen(seen, names, "entries");
|
|
391
|
+
return lines.join("\n");
|
|
392
|
+
}
|
|
393
|
+
function readYarn(text, specs) {
|
|
394
|
+
checkYarnVersion(text);
|
|
395
|
+
const versions = {};
|
|
396
|
+
let block = null;
|
|
397
|
+
for (const line of text.split("\n")) {
|
|
398
|
+
if (line.trim() === "" || line.startsWith("#"))
|
|
399
|
+
continue;
|
|
400
|
+
if (indentOf(line) === 0) {
|
|
401
|
+
block = null;
|
|
402
|
+
const descriptors = line.replace(/:$/, "").split(/,\s*/).map(parseDescriptor);
|
|
403
|
+
const match = descriptors.find((descriptor) => descriptor && specs[descriptor.name] === descriptor.range);
|
|
404
|
+
if (match)
|
|
405
|
+
block = match.name;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const version = block ? /^ {2}version "([^"]+)"$/.exec(line)?.[1] : undefined;
|
|
409
|
+
if (block && version)
|
|
410
|
+
versions[block] = version;
|
|
411
|
+
}
|
|
412
|
+
return versions;
|
|
413
|
+
}
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
/**
|
|
416
|
+
* The versions a lockfile resolves the app's direct Refino dependencies to.
|
|
417
|
+
* `specs` maps each dependency name to its package.json specifier.
|
|
418
|
+
*/
|
|
419
|
+
export function readLockedVersions(kind, text, importer, specs) {
|
|
420
|
+
try {
|
|
421
|
+
const names = Object.keys(specs);
|
|
422
|
+
const versions = kind === "npm" ? readNpm(text, importer, names) : kind === "pnpm" ? readPnpm(text, importer, names) : readYarn(text, specs);
|
|
423
|
+
return { ok: true, versions };
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
if (error instanceof Refusal)
|
|
427
|
+
return { ok: false, reason: error.message };
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Rewrite a lockfile so the Refino packages move from `request.from` to `request.to`, or say why it cannot be done safely. */
|
|
432
|
+
export function rewriteLockfile(kind, text, request) {
|
|
433
|
+
return attempt(() => {
|
|
434
|
+
switch (kind) {
|
|
435
|
+
case "npm":
|
|
436
|
+
return rewriteNpm(text, request);
|
|
437
|
+
case "pnpm":
|
|
438
|
+
return rewritePnpm(text, request);
|
|
439
|
+
case "yarn":
|
|
440
|
+
return rewriteYarn(text, request);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { HostIntegration, HostKind, MigrationPlan, PlanOptions, RefinoSite, RepositoryInspection, ScanResult } from "./types.js";
|
|
2
|
-
export
|
|
3
|
-
/**
|
|
4
|
-
* The published version of this package. A prerelease build names the commit
|
|
5
|
-
* it came from, so plans, instructions and the dependency pins a site
|
|
6
|
-
* receives say exactly which build produced them.
|
|
7
|
-
*/
|
|
8
|
-
export declare const TOOL_VERSION: string;
|
|
2
|
+
export { BOILERPLATE_DIR, CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_DIR, PLAN_FILE, TOOL_NAME, TOOL_VERSION } from "./constants.js";
|
|
9
3
|
/**
|
|
10
4
|
* Dependency range for the runtime packages. Any prerelease (a smoke build or
|
|
11
5
|
* a release candidate) is pinned exactly, so a site records the one build it
|
|
@@ -13,11 +7,6 @@ export declare const TOOL_VERSION: string;
|
|
|
13
7
|
* eventual stable release. Stable versions get a caret.
|
|
14
8
|
*/
|
|
15
9
|
export declare function dependencyRange(version?: string): string;
|
|
16
|
-
export declare const PLAN_DIR = ".refino";
|
|
17
|
-
export declare const PLAN_FILE = ".refino/plan.json";
|
|
18
|
-
export declare const INSTRUCTIONS_FILE = ".refino/AGENT_INSTRUCTIONS.md";
|
|
19
|
-
export declare const CONFIG_FILE = "refino.config.json";
|
|
20
|
-
export declare const BOILERPLATE_DIR = "refino";
|
|
21
10
|
export declare function defaultContentFile(inspection: RepositoryInspection): string;
|
|
22
11
|
/** Validate and normalize the Refino connection given on the command line. */
|
|
23
12
|
export declare function resolveRefinoSite(option: PlanOptions["refino"]): RefinoSite | null;
|
package/dist/plan.js
CHANGED
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
* plan is the contract between the CLI, the agent instructions, the safe
|
|
4
4
|
* apply step and the verifier.
|
|
5
5
|
*/
|
|
6
|
-
import { createRequire } from "node:module";
|
|
7
6
|
import { join } from "node:path";
|
|
8
7
|
import { parseCopy } from "@getrefino/core";
|
|
8
|
+
import { BOILERPLATE_DIR, CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_FILE, TOOL_NAME, TOOL_VERSION } from "./constants.js";
|
|
9
9
|
import { exists, isDirectory, readText } from "./fs.js";
|
|
10
10
|
import { appDirectory, packageManagerExec, packageManagerRun } from "./inspect.js";
|
|
11
11
|
import { scanCopy } from "./scan.js";
|
|
12
12
|
import { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN } from "./templates-hosted.js";
|
|
13
|
-
export
|
|
14
|
-
/**
|
|
15
|
-
* The published version of this package. A prerelease build names the commit
|
|
16
|
-
* it came from, so plans, instructions and the dependency pins a site
|
|
17
|
-
* receives say exactly which build produced them.
|
|
18
|
-
*/
|
|
19
|
-
export const TOOL_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
13
|
+
export { BOILERPLATE_DIR, CONFIG_FILE, INSTRUCTIONS_FILE, PLAN_DIR, PLAN_FILE, TOOL_NAME, TOOL_VERSION } from "./constants.js";
|
|
20
14
|
const PRERELEASE_VERSION = /^\d+\.\d+\.\d+-/;
|
|
21
15
|
/**
|
|
22
16
|
* Dependency range for the runtime packages. Any prerelease (a smoke build or
|
|
@@ -27,11 +21,6 @@ const PRERELEASE_VERSION = /^\d+\.\d+\.\d+-/;
|
|
|
27
21
|
export function dependencyRange(version = TOOL_VERSION) {
|
|
28
22
|
return PRERELEASE_VERSION.test(version) ? version : `^${version}`;
|
|
29
23
|
}
|
|
30
|
-
export const PLAN_DIR = ".refino";
|
|
31
|
-
export const PLAN_FILE = `${PLAN_DIR}/plan.json`;
|
|
32
|
-
export const INSTRUCTIONS_FILE = `${PLAN_DIR}/AGENT_INSTRUCTIONS.md`;
|
|
33
|
-
export const CONFIG_FILE = "refino.config.json";
|
|
34
|
-
export const BOILERPLATE_DIR = "refino";
|
|
35
24
|
export function defaultContentFile(inspection) {
|
|
36
25
|
if (inspection.refino.contentFile)
|
|
37
26
|
return inspection.refino.contentFile;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the npm registry says about one published version of a package, and
|
|
3
|
+
* the one question an in-place upgrade needs answered from it: does moving
|
|
4
|
+
* from this release to that one change anything a lockfile records other
|
|
5
|
+
* than the version, the tarball and its hashes?
|
|
6
|
+
*/
|
|
7
|
+
import type { PackageRelease } from "./lockfiles.js";
|
|
8
|
+
export declare const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
9
|
+
/** The fields of a version manifest (`GET /<name>/<version>`) that this code reads. */
|
|
10
|
+
export interface RegistryManifest {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly version: string;
|
|
13
|
+
readonly dist: {
|
|
14
|
+
readonly tarball: string;
|
|
15
|
+
readonly integrity: string;
|
|
16
|
+
readonly shasum: string;
|
|
17
|
+
};
|
|
18
|
+
readonly deprecated?: string;
|
|
19
|
+
readonly [field: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface PackageRegistry {
|
|
22
|
+
/** The manifest of one exact version, or null when the registry has no such version. Throws when the registry cannot be reached. */
|
|
23
|
+
manifest(name: string, version: string): Promise<RegistryManifest | null>;
|
|
24
|
+
}
|
|
25
|
+
export interface NpmRegistryOptions {
|
|
26
|
+
readonly url?: string;
|
|
27
|
+
readonly fetch?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
export declare function createNpmRegistry(options?: NpmRegistryOptions): PackageRegistry;
|
|
30
|
+
export declare function toRelease(manifest: RegistryManifest): PackageRelease;
|
|
31
|
+
/**
|
|
32
|
+
* The Refino packages a release depends on (`dependencies` only). Refino
|
|
33
|
+
* publishes them in lockstep, so each is at the release's own version.
|
|
34
|
+
*/
|
|
35
|
+
export declare function refinoDependencies(manifest: RegistryManifest, scope?: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Why moving from `from` to `to` would change a lockfile beyond the
|
|
38
|
+
* version, tarball and hashes of the packages being moved, or null when it
|
|
39
|
+
* would not. `moving` is every package that moves; each may depend on the
|
|
40
|
+
* others at exactly its own version and at nothing else.
|
|
41
|
+
*/
|
|
42
|
+
export declare function releaseShapeChange(from: RegistryManifest, to: RegistryManifest, moving: readonly string[]): string | null;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2
|
+
function isManifest(value, name, version) {
|
|
3
|
+
if (typeof value !== "object" || value === null)
|
|
4
|
+
return false;
|
|
5
|
+
const manifest = value;
|
|
6
|
+
const dist = manifest.dist;
|
|
7
|
+
return (manifest.name === name &&
|
|
8
|
+
manifest.version === version &&
|
|
9
|
+
typeof dist === "object" &&
|
|
10
|
+
dist !== null &&
|
|
11
|
+
typeof dist.tarball === "string" &&
|
|
12
|
+
typeof dist.integrity === "string" &&
|
|
13
|
+
/^sha512-/.test(dist.integrity) &&
|
|
14
|
+
typeof dist.shasum === "string" &&
|
|
15
|
+
/^[0-9a-f]{40}$/.test(dist.shasum));
|
|
16
|
+
}
|
|
17
|
+
export function createNpmRegistry(options = {}) {
|
|
18
|
+
const base = (options.url ?? NPM_REGISTRY_URL).replace(/\/+$/, "");
|
|
19
|
+
const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
20
|
+
return {
|
|
21
|
+
async manifest(name, version) {
|
|
22
|
+
const response = await doFetch(`${base}/${name.replace("/", "%2f")}/${encodeURIComponent(version)}`, {
|
|
23
|
+
headers: { accept: "application/json" },
|
|
24
|
+
});
|
|
25
|
+
if (response.status === 404)
|
|
26
|
+
return null;
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`The npm registry answered HTTP ${response.status} for ${name}@${version}.`);
|
|
29
|
+
const body = await response.json();
|
|
30
|
+
if (!isManifest(body, name, version))
|
|
31
|
+
throw new Error(`The npm registry returned an unexpected manifest for ${name}@${version}.`);
|
|
32
|
+
return body;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function toRelease(manifest) {
|
|
37
|
+
return {
|
|
38
|
+
name: manifest.name,
|
|
39
|
+
version: manifest.version,
|
|
40
|
+
tarball: manifest.dist.tarball,
|
|
41
|
+
integrity: manifest.dist.integrity,
|
|
42
|
+
shasum: manifest.dist.shasum,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Manifest fields that npm, pnpm or yarn copy into a lockfile entry or act on at install time. */
|
|
46
|
+
const LOCKFILE_FIELDS = [
|
|
47
|
+
"dependencies",
|
|
48
|
+
"optionalDependencies",
|
|
49
|
+
"peerDependencies",
|
|
50
|
+
"peerDependenciesMeta",
|
|
51
|
+
"bundleDependencies",
|
|
52
|
+
"bundledDependencies",
|
|
53
|
+
"engines",
|
|
54
|
+
"os",
|
|
55
|
+
"cpu",
|
|
56
|
+
"libc",
|
|
57
|
+
"bin",
|
|
58
|
+
"license",
|
|
59
|
+
"funding",
|
|
60
|
+
];
|
|
61
|
+
const INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
62
|
+
function stable(value) {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return `[${value.map(stable).join(",")}]`;
|
|
65
|
+
if (typeof value === "object" && value !== null) {
|
|
66
|
+
return `{${Object.keys(value)
|
|
67
|
+
.sort()
|
|
68
|
+
.map((key) => `${JSON.stringify(key)}:${stable(value[key])}`)
|
|
69
|
+
.join(",")}}`;
|
|
70
|
+
}
|
|
71
|
+
return JSON.stringify(value ?? null);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The Refino packages a release depends on (`dependencies` only). Refino
|
|
75
|
+
* publishes them in lockstep, so each is at the release's own version.
|
|
76
|
+
*/
|
|
77
|
+
export function refinoDependencies(manifest, scope = "@getrefino/") {
|
|
78
|
+
const dependencies = (manifest.dependencies ?? {});
|
|
79
|
+
return Object.keys(dependencies).filter((name) => name.startsWith(scope));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Why moving from `from` to `to` would change a lockfile beyond the
|
|
83
|
+
* version, tarball and hashes of the packages being moved, or null when it
|
|
84
|
+
* would not. `moving` is every package that moves; each may depend on the
|
|
85
|
+
* others at exactly its own version and at nothing else.
|
|
86
|
+
*/
|
|
87
|
+
export function releaseShapeChange(from, to, moving) {
|
|
88
|
+
if (to.deprecated)
|
|
89
|
+
return `${to.name}@${to.version} is deprecated.`;
|
|
90
|
+
const normalize = (manifest, field) => {
|
|
91
|
+
const value = manifest[field];
|
|
92
|
+
if (field !== "dependencies" || typeof value !== "object" || value === null)
|
|
93
|
+
return stable(value);
|
|
94
|
+
const own = {};
|
|
95
|
+
for (const [name, range] of Object.entries(value)) {
|
|
96
|
+
own[name] = moving.includes(name) && range === manifest.version ? "<release>" : range;
|
|
97
|
+
}
|
|
98
|
+
return stable(own);
|
|
99
|
+
};
|
|
100
|
+
for (const field of LOCKFILE_FIELDS) {
|
|
101
|
+
if (normalize(from, field) !== normalize(to, field))
|
|
102
|
+
return `${to.name}@${to.version} changes its ${field}.`;
|
|
103
|
+
}
|
|
104
|
+
const scripts = (manifest) => {
|
|
105
|
+
const all = (manifest.scripts ?? {});
|
|
106
|
+
return INSTALL_SCRIPTS.filter((name) => name in all).join(",");
|
|
107
|
+
};
|
|
108
|
+
if (scripts(from) !== scripts(to))
|
|
109
|
+
return `${to.name}@${to.version} changes its install scripts.`;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
package/dist/semver.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough semver for comparing Refino release versions: exact versions
|
|
3
|
+
* only (`1.2.3`, `0.1.0-rc.4`), ordered by the semver 2.0 precedence rules.
|
|
4
|
+
* Ranges are never interpreted here; code that meets one treats it as
|
|
5
|
+
* something to preserve or refuse, not something to reason about.
|
|
6
|
+
*/
|
|
7
|
+
/** Whether `value` is one exact version (no range, no tag, no `v` prefix). */
|
|
8
|
+
export declare function isExactVersion(value: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Negative when `a` precedes `b`, positive when it follows, zero when they
|
|
11
|
+
* are the same release. Throws on anything that is not an exact version, so
|
|
12
|
+
* a caller can never mistake "could not compare" for "equal".
|
|
13
|
+
*/
|
|
14
|
+
export declare function compareVersions(a: string, b: string): number;
|