@getrefino/onboarding 0.1.0-rc.4 → 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.
@@ -0,0 +1,12 @@
1
+ export declare const TOOL_NAME = "refino";
2
+ /**
3
+ * The published version of this package. A prerelease build names the commit
4
+ * it came from, so plans, instructions and the dependency pins a site
5
+ * receives say exactly which build produced them.
6
+ */
7
+ export declare const TOOL_VERSION: string;
8
+ export declare const PLAN_DIR = ".refino";
9
+ export declare const PLAN_FILE = ".refino/plan.json";
10
+ export declare const INSTRUCTIONS_FILE = ".refino/AGENT_INSTRUCTIONS.md";
11
+ export declare const CONFIG_FILE = "refino.config.json";
12
+ export declare const BOILERPLATE_DIR = "refino";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Names and the version, with no filesystem or inspection code attached, so
3
+ * modules that only need them (generated-file rendering and classification,
4
+ * integration updates) can be loaded somewhere that must not pull in the
5
+ * repository scanner, such as the hosted app's server.
6
+ */
7
+ import { createRequire } from "node:module";
8
+ export const TOOL_NAME = "refino";
9
+ /**
10
+ * The published version of this package. A prerelease build names the commit
11
+ * it came from, so plans, instructions and the dependency pins a site
12
+ * receives say exactly which build produced them.
13
+ */
14
+ export const TOOL_VERSION = createRequire(import.meta.url)("../package.json").version;
15
+ export const PLAN_DIR = ".refino";
16
+ export const PLAN_FILE = `${PLAN_DIR}/plan.json`;
17
+ export const INSTRUCTIONS_FILE = `${PLAN_DIR}/AGENT_INSTRUCTIONS.md`;
18
+ export const CONFIG_FILE = "refino.config.json";
19
+ export const BOILERPLATE_DIR = "refino";
@@ -11,6 +11,8 @@ export declare const GENERATED_MANIFEST_FILE = ".refino/generated.json";
11
11
  export declare const COMPARISON_SUFFIX = ".refino-new";
12
12
  export declare function hashContent(text: string): string;
13
13
  export declare function readGeneratedManifest(appDir: string): GeneratedManifest | null;
14
+ /** `readGeneratedManifest` over text the caller already read. */
15
+ export declare function parseGeneratedManifest(text: string | null): GeneratedManifest | null;
14
16
  export declare function serializeGeneratedManifest(manifest: GeneratedManifest): string;
15
17
  /**
16
18
  * Classify one generated file against the repository. Read-only: deciding and
@@ -18,6 +20,12 @@ export declare function serializeGeneratedManifest(manifest: GeneratedManifest):
18
20
  * without touching anything.
19
21
  */
20
22
  export declare function classifyGeneratedFile(appDir: string, file: TemplateFile, manifest: GeneratedManifest | null): GeneratedFileState;
23
+ /**
24
+ * The same decision over bytes the caller already has (`null` when the file
25
+ * does not exist), for a repository that is not on this machine's disk:
26
+ * Refino's hosted update reads it through the GitHub API.
27
+ */
28
+ export declare function classifyGeneratedContent(file: TemplateFile, current: string | null, manifest: GeneratedManifest | null): GeneratedFileState;
21
29
  export declare function classifyGeneratedFiles(appDir: string, files: readonly TemplateFile[], manifest: GeneratedManifest | null): GeneratedFileState[];
22
30
  /** A file whose bytes Refino is leaving behind although its template moved on. */
23
31
  export declare function needsReview(states: readonly GeneratedFileState[]): boolean;
package/dist/generated.js CHANGED
@@ -27,7 +27,7 @@
27
27
  import { createHash } from "node:crypto";
28
28
  import { join } from "node:path";
29
29
  import { exists, readJson, readText } from "./fs.js";
30
- import { PLAN_DIR } from "./plan.js";
30
+ import { PLAN_DIR } from "./constants.js";
31
31
  /** The repository's record of what Refino generated. Machine-owned, committed. */
32
32
  export const GENERATED_MANIFEST_FILE = `${PLAN_DIR}/generated.json`;
33
33
  /**
@@ -41,7 +41,20 @@ export function hashContent(text) {
41
41
  return createHash("sha256").update(text, "utf8").digest("hex");
42
42
  }
43
43
  export function readGeneratedManifest(appDir) {
44
- const manifest = readJson(join(appDir, GENERATED_MANIFEST_FILE));
44
+ return validManifest(readJson(join(appDir, GENERATED_MANIFEST_FILE)));
45
+ }
46
+ /** `readGeneratedManifest` over text the caller already read. */
47
+ export function parseGeneratedManifest(text) {
48
+ if (text === null)
49
+ return null;
50
+ try {
51
+ return validManifest(JSON.parse(text));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function validManifest(manifest) {
45
58
  if (!manifest || manifest.version !== 1 || typeof manifest.files !== "object" || manifest.files === null)
46
59
  return null;
47
60
  return manifest;
@@ -82,13 +95,20 @@ function renderingIsCurrent(file, sha) {
82
95
  * without touching anything.
83
96
  */
84
97
  export function classifyGeneratedFile(appDir, file, manifest) {
98
+ const absolute = join(appDir, file.path);
99
+ return classifyGeneratedContent(file, exists(absolute) ? (readText(absolute) ?? "") : null, manifest);
100
+ }
101
+ /**
102
+ * The same decision over bytes the caller already has (`null` when the file
103
+ * does not exist), for a repository that is not on this machine's disk:
104
+ * Refino's hosted update reads it through the GitHub API.
105
+ */
106
+ export function classifyGeneratedContent(file, current, manifest) {
85
107
  const to = file.version;
86
108
  const base = { path: file.path, ownership: file.ownership, to };
87
- const absolute = join(appDir, file.path);
88
- if (!exists(absolute)) {
109
+ if (current === null) {
89
110
  return { ...base, status: "create", from: null, reason: "Not present; generated by this run." };
90
111
  }
91
- const current = readText(absolute) ?? "";
92
112
  const recorded = manifest?.files[file.path];
93
113
  if (current === file.content) {
94
114
  return { ...base, status: "current", from: to, reason: `Already the ${to} version.` };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { applyPlan } from "./apply.js";
9
9
  export type { ApplyOptions, RefinoConfig } from "./apply.js";
10
10
  export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
11
11
  export type { TemplateFile, TemplateRendering } from "./templates.js";
12
- export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
12
+ export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedContent, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, parseGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
13
13
  export { INSTALL_METHODS, isInstallMethod, resolveInstallMethod } from "./install-method.js";
14
14
  export type { InstallMethod } from "./install-method.js";
15
15
  export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, analyticsFramework, analyticsHosting, hostedFiles } from "./templates-hosted.js";
@@ -17,3 +17,10 @@ export type { RefinoSiteConfig } from "./templates-hosted.js";
17
17
  export { verifyIntegration } from "./verify.js";
18
18
  export type { VerifyOptions } from "./verify.js";
19
19
  export type { ApplyResult, CandidateStatus, CheckStatus, CopyCandidate, CopyClassification, DetectedPackage, Framework, GeneratedFileState, GeneratedFileStatus, GeneratedManifest, GeneratedManifestEntry, GeneratedOwnership, GitState, HostIntegration, HostKind, Language, MigrationPlan, PackageManager, PlanOptions, PlannedFile, RefinoSite, RepositoryInspection, RouteInfo, Router, ScanResult, ScriptSet, VerificationCheck, VerificationResult, } from "./types.js";
20
+ export { RUNTIME_PACKAGES, nextSpecifier, planIntegrationUpdate, setDependencySpecifiers, updateCommitMessage } from "./integration-update.js";
21
+ export type { FileChange, IntegrationUpdateOptions, IntegrationUpdatePlan, ManualUpdateReason, PackageManagerName, RepositoryFiles } from "./integration-update.js";
22
+ export { LOCKFILES, readLockedVersions, rewriteLockfile } from "./lockfiles.js";
23
+ export type { LockfileKind, LockfileResult, LockfileRewrite, PackageRelease } from "./lockfiles.js";
24
+ export { NPM_REGISTRY_URL, createNpmRegistry, refinoDependencies, releaseShapeChange, toRelease } from "./registry.js";
25
+ export type { NpmRegistryOptions, PackageRegistry, RegistryManifest } from "./registry.js";
26
+ export { compareVersions, isExactVersion } from "./semver.js";
package/dist/index.js CHANGED
@@ -5,7 +5,11 @@ export { buildPlan, planMigration, planContent, defaultContentFile, integrationP
5
5
  export { renderAgentInstructions } from "./agent.js";
6
6
  export { applyPlan } from "./apply.js";
7
7
  export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
8
- export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
8
+ export { COMPARISON_SUFFIX, GENERATED_MANIFEST_FILE, classifyGeneratedContent, classifyGeneratedFile, classifyGeneratedFiles, hashContent, needsReview, nextGeneratedManifest, parseGeneratedManifest, readGeneratedManifest, serializeGeneratedManifest, } from "./generated.js";
9
9
  export { INSTALL_METHODS, isInstallMethod, resolveInstallMethod } from "./install-method.js";
10
10
  export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, analyticsFramework, analyticsHosting, hostedFiles } from "./templates-hosted.js";
11
11
  export { verifyIntegration } from "./verify.js";
12
+ export { RUNTIME_PACKAGES, nextSpecifier, planIntegrationUpdate, setDependencySpecifiers, updateCommitMessage } from "./integration-update.js";
13
+ export { LOCKFILES, readLockedVersions, rewriteLockfile } from "./lockfiles.js";
14
+ export { NPM_REGISTRY_URL, createNpmRegistry, refinoDependencies, releaseShapeChange, toRelease } from "./registry.js";
15
+ export { compareVersions, isExactVersion } from "./semver.js";
@@ -0,0 +1,83 @@
1
+ import type { LockfileKind } from "./lockfiles.js";
2
+ import type { PackageRegistry } from "./registry.js";
3
+ /** The runtime packages a site declares. The CLI and onboarding are tools and are never moved here. */
4
+ export declare const RUNTIME_PACKAGES: readonly ["@getrefino/core", "@getrefino/react", "@getrefino/github"];
5
+ /** A repository at one commit, read lazily. Paths are repository-relative with `/` separators. */
6
+ export interface RepositoryFiles {
7
+ readonly paths: readonly string[];
8
+ /** The file's text, or null when it does not exist. */
9
+ read(path: string): Promise<string | null>;
10
+ }
11
+ export interface IntegrationUpdateOptions {
12
+ /** Repository-relative path of the site's copy file: which app in the repository is the site. */
13
+ readonly copyPath: string;
14
+ readonly registry: PackageRegistry;
15
+ }
16
+ export type PackageManagerName = LockfileKind | "none";
17
+ /**
18
+ * Why an update Refino knows about cannot be applied automatically. A closed
19
+ * vocabulary, so a caller can explain each in plain words without showing a
20
+ * developer-facing detail to a site owner.
21
+ */
22
+ export type ManualUpdateReason = "unsupported-package-manager" | "multiple-lockfiles" | "unsupported-specifier" | "inconsistent-versions" | "dependency-change" | "lockfile" | "package-json" | "customized-integration" | "removed-integration" | "registry";
23
+ export interface FileChange {
24
+ readonly path: string;
25
+ readonly content: string;
26
+ }
27
+ export type IntegrationUpdatePlan =
28
+ /** Nothing to do: packages and generated files are this release. */
29
+ {
30
+ readonly kind: "current";
31
+ readonly installed: string;
32
+ readonly target: string;
33
+ }
34
+ /** The site runs a newer release than this Refino renders. Never downgraded. */
35
+ | {
36
+ readonly kind: "ahead";
37
+ readonly installed: string;
38
+ readonly target: string;
39
+ }
40
+ /** The target release is not on the registry (yet); there is nothing to offer. */
41
+ | {
42
+ readonly kind: "unreleased";
43
+ readonly installed: string;
44
+ readonly target: string;
45
+ }
46
+ /** Not a Refino-hosted integration this code can read; say nothing to the owner. */
47
+ | {
48
+ readonly kind: "unknown";
49
+ readonly detail: string;
50
+ }
51
+ /** An update exists and needs a developer. */
52
+ | {
53
+ readonly kind: "manual";
54
+ readonly installed: string;
55
+ readonly target: string;
56
+ readonly reason: ManualUpdateReason;
57
+ readonly detail: string;
58
+ } | {
59
+ readonly kind: "update";
60
+ readonly installed: string;
61
+ readonly target: string;
62
+ readonly packageManager: PackageManagerName;
63
+ readonly files: readonly FileChange[];
64
+ readonly message: string;
65
+ };
66
+ export declare function updateCommitMessage(version: string): string;
67
+ /** The specifier to write for `to`, in the style of the one already there; null when the style is not one to reason about. */
68
+ export declare function nextSpecifier(spec: string, to: string): string | null;
69
+ /**
70
+ * Change dependency specifiers in package.json text without re-serializing
71
+ * it: the owner's formatting, key order and every other value stay byte for
72
+ * byte. Refuses when a name appears anywhere else in the file (an override,
73
+ * a resolution), because that would be left pointing at the old release.
74
+ */
75
+ export declare function setDependencySpecifiers(text: string, changes: Readonly<Record<string, {
76
+ readonly from: string;
77
+ readonly to: string;
78
+ }>>): string | null;
79
+ /**
80
+ * Decide what updating this repository's Refino integration to this
81
+ * release means, without changing anything.
82
+ */
83
+ export declare function planIntegrationUpdate(files: RepositoryFiles, options: IntegrationUpdateOptions): Promise<IntegrationUpdatePlan>;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Updating a Refino-hosted site's integration in its repository, decided
3
+ * from the repository's own files and nothing else.
4
+ *
5
+ * An integration update is exactly what the documented manual upgrade does
6
+ * (move the `@getrefino/*` packages, then run `init` from the new version),
7
+ * minus everything `init` does that is not an upgrade: no inspection, no
8
+ * scan, no plan or instructions rewritten, no copy touched. What changes:
9
+ *
10
+ * - the app's package.json: the Refino specifiers, and nothing else;
11
+ * - the lockfile the repository already has, through lockfiles.ts;
12
+ * - Refino's generated files, where generated.ts can prove they are still
13
+ * Refino's, and `.refino/generated.json`, which records that.
14
+ *
15
+ * The plan is pure: files in (through `RepositoryFiles`), a list of file
16
+ * contents out, or a reason it cannot be done safely. It never writes. The
17
+ * hosted app commits the result through its GitHub integration; a CLI could
18
+ * write the same result to disk.
19
+ *
20
+ * The target is always this package's own version, because it is the only
21
+ * version whose generated files this code can render.
22
+ */
23
+ import { classifyGeneratedContent, GENERATED_MANIFEST_FILE, nextGeneratedManifest, parseGeneratedManifest, serializeGeneratedManifest } from "./generated.js";
24
+ import { LOCKFILES, readLockedVersions, rewriteLockfile } from "./lockfiles.js";
25
+ import { PLAN_FILE, TOOL_VERSION } from "./constants.js";
26
+ import { refinoDependencies, releaseShapeChange, toRelease } from "./registry.js";
27
+ import { compareVersions, isExactVersion } from "./semver.js";
28
+ import { boilerplateFiles } from "./templates.js";
29
+ /** The runtime packages a site declares. The CLI and onboarding are tools and are never moved here. */
30
+ export const RUNTIME_PACKAGES = ["@getrefino/core", "@getrefino/react", "@getrefino/github"];
31
+ function dirname(path) {
32
+ const index = path.lastIndexOf("/");
33
+ return index === -1 ? "." : path.slice(0, index);
34
+ }
35
+ function join(dir, path) {
36
+ return dir === "." ? path : `${dir}/${path}`;
37
+ }
38
+ /** `dir` relative to `base`, both repository-relative; "." when equal. */
39
+ function relative(base, dir) {
40
+ if (base === dir)
41
+ return ".";
42
+ return base === "." ? dir : dir.slice(base.length + 1);
43
+ }
44
+ function parseJson(text) {
45
+ if (text === null)
46
+ return null;
47
+ try {
48
+ return JSON.parse(text);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ export function updateCommitMessage(version) {
55
+ return `chore: update Refino to ${version}`;
56
+ }
57
+ /** The specifier to write for `to`, in the style of the one already there; null when the style is not one to reason about. */
58
+ export function nextSpecifier(spec, to) {
59
+ if (isExactVersion(spec))
60
+ return to;
61
+ const match = /^([\^~])(.+)$/.exec(spec);
62
+ return match && isExactVersion(match[2]) ? `${match[1]}${to}` : null;
63
+ }
64
+ function escapeRegExp(text) {
65
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
+ }
67
+ /**
68
+ * Change dependency specifiers in package.json text without re-serializing
69
+ * it: the owner's formatting, key order and every other value stay byte for
70
+ * byte. Refuses when a name appears anywhere else in the file (an override,
71
+ * a resolution), because that would be left pointing at the old release.
72
+ */
73
+ export function setDependencySpecifiers(text, changes) {
74
+ let next = text;
75
+ for (const [name, { from, to }] of Object.entries(changes)) {
76
+ if (next.split(`"${name}"`).length !== 2)
77
+ return null;
78
+ const pattern = new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*)"${escapeRegExp(from)}"`);
79
+ if (!pattern.test(next))
80
+ return null;
81
+ next = next.replace(pattern, (_, prefix) => `${prefix}"${to}"`);
82
+ }
83
+ const before = parseJson(text);
84
+ const after = parseJson(next);
85
+ if (!before || !after)
86
+ return null;
87
+ for (const [name, { to }] of Object.entries(changes)) {
88
+ for (const group of ["dependencies", "devDependencies"]) {
89
+ if (before[group]?.[name] !== undefined)
90
+ before[group][name] = to;
91
+ }
92
+ }
93
+ return JSON.stringify(before) === JSON.stringify(after) ? next : null;
94
+ }
95
+ async function locateApp(files, copyPath) {
96
+ const candidates = files.paths.filter((path) => path === PLAN_FILE || path.endsWith(`/${PLAN_FILE}`));
97
+ for (const path of candidates) {
98
+ const dir = path === PLAN_FILE ? "." : path.slice(0, -(PLAN_FILE.length + 1));
99
+ const plan = parseJson(await files.read(path));
100
+ if (plan?.contentFile?.path && join(dir, plan.contentFile.path) === copyPath)
101
+ return { dir, plan };
102
+ }
103
+ return null;
104
+ }
105
+ function findLockfile(paths, appDir) {
106
+ let dir = appDir;
107
+ for (;;) {
108
+ const present = Object.keys(LOCKFILES).filter((name) => paths.has(join(dir, name)));
109
+ if (present.length > 1)
110
+ return { kind: "refused", reason: "multiple-lockfiles", detail: `${dir} has more than one lockfile: ${present.join(", ")}.` };
111
+ const name = present[0];
112
+ if (name) {
113
+ const kind = LOCKFILES[name];
114
+ if (kind === "unsupported")
115
+ return { kind: "refused", reason: "unsupported-package-manager", detail: `${join(dir, name)} is not a lockfile Refino can update.` };
116
+ return { kind, path: join(dir, name) };
117
+ }
118
+ if (dir === ".")
119
+ return { kind: "none" };
120
+ dir = dirname(dir);
121
+ }
122
+ }
123
+ function lowest(versions) {
124
+ return [...versions].sort(compareVersions)[0];
125
+ }
126
+ /**
127
+ * Decide what updating this repository's Refino integration to this
128
+ * release means, without changing anything.
129
+ */
130
+ export async function planIntegrationUpdate(files, options) {
131
+ const target = TOOL_VERSION;
132
+ const paths = new Set(files.paths);
133
+ const app = await locateApp(files, options.copyPath);
134
+ if (!app)
135
+ return { kind: "unknown", detail: `No ${PLAN_FILE} in the repository describes ${options.copyPath}.` };
136
+ if (!app.plan.refino)
137
+ return { kind: "unknown", detail: "The integration is self-hosted; it is not updated through Refino." };
138
+ const packageJsonPath = join(app.dir, "package.json");
139
+ const packageJsonText = await files.read(packageJsonPath);
140
+ const packageJson = parseJson(packageJsonText);
141
+ if (!packageJson || packageJsonText === null)
142
+ return { kind: "unknown", detail: `${packageJsonPath} is missing or not valid JSON.` };
143
+ const specs = {};
144
+ for (const name of RUNTIME_PACKAGES) {
145
+ const spec = packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
146
+ if (typeof spec === "string")
147
+ specs[name] = spec;
148
+ }
149
+ if (Object.keys(specs).length === 0)
150
+ return { kind: "unknown", detail: `${packageJsonPath} declares no Refino packages.` };
151
+ // What is installed: what the lockfile resolves, or failing that an exact pin.
152
+ const lockfile = findLockfile(paths, app.dir);
153
+ let lockText = null;
154
+ let resolved = {};
155
+ if (lockfile.kind !== "none" && lockfile.kind !== "refused") {
156
+ lockText = await files.read(lockfile.path);
157
+ const read = lockText === null ? null : readLockedVersions(lockfile.kind, lockText, relative(dirname(lockfile.path), app.dir), specs);
158
+ if (read?.ok)
159
+ resolved = { ...read.versions };
160
+ }
161
+ for (const [name, spec] of Object.entries(specs)) {
162
+ if (resolved[name] === undefined && isExactVersion(spec))
163
+ resolved[name] = spec;
164
+ }
165
+ const versions = Object.values(resolved).filter(isExactVersion);
166
+ if (versions.length === 0)
167
+ return { kind: "unknown", detail: "No installed Refino version can be read from the repository." };
168
+ const installed = lowest(versions);
169
+ if (compareVersions(installed, target) > 0)
170
+ return { kind: "ahead", installed, target };
171
+ // Refino's generated files, judged exactly as `init` and `verify` judge them.
172
+ let templates;
173
+ try {
174
+ templates = boilerplateFiles(app.plan);
175
+ }
176
+ catch (error) {
177
+ return { kind: "unknown", detail: `${join(app.dir, PLAN_FILE)} could not be read as a plan: ${String(error)}` };
178
+ }
179
+ const manifestPath = join(app.dir, GENERATED_MANIFEST_FILE);
180
+ const manifestText = await files.read(manifestPath);
181
+ const manifest = parseGeneratedManifest(manifestText);
182
+ const states = await Promise.all(templates.map(async (file) => classifyGeneratedContent(file, await files.read(join(app.dir, file.path)), manifest)));
183
+ const packagesCurrent = Object.keys(specs).every((name) => resolved[name] === target);
184
+ const filesCurrent = states.every((state) => state.status === "current" || state.status === "customized");
185
+ if (packagesCurrent && filesCurrent)
186
+ return { kind: "current", installed, target };
187
+ const manual = (reason, detail) => ({ kind: "manual", installed, target, reason, detail });
188
+ // From here on there is an update. Is the target actually published?
189
+ let targetManifests;
190
+ try {
191
+ targetManifests = await manifestsFor(options.registry, Object.keys(specs), target);
192
+ }
193
+ catch (error) {
194
+ return manual("registry", String(error));
195
+ }
196
+ if (Object.keys(targetManifests).length === 0)
197
+ return { kind: "unreleased", installed, target };
198
+ const review = states.find((state) => state.status === "review");
199
+ if (review)
200
+ return manual("customized-integration", `${join(app.dir, review.path)}: ${review.reason}`);
201
+ const removed = states.find((state) => state.status === "create" && manifest?.files[state.path]);
202
+ if (removed)
203
+ return manual("removed-integration", `${join(app.dir, removed.path)} was generated by Refino and has since been deleted.`);
204
+ const changes = [];
205
+ let packageManager = "none";
206
+ if (!packagesCurrent) {
207
+ if (new Set(versions).size > 1 || versions.length !== Object.keys(specs).length) {
208
+ return manual("inconsistent-versions", `The Refino packages are not all at one version: ${JSON.stringify(resolved)}.`);
209
+ }
210
+ if (lockfile.kind === "refused")
211
+ return manual(lockfile.reason, lockfile.detail);
212
+ const specChanges = {};
213
+ for (const [name, spec] of Object.entries(specs)) {
214
+ const to = nextSpecifier(spec, target);
215
+ if (!to)
216
+ return manual("unsupported-specifier", `${packageJsonPath} asks for ${name}@${spec}; only exact versions and ^/~ ranges are updated automatically.`);
217
+ specChanges[name] = { from: spec, to };
218
+ }
219
+ const moving = [...new Set([...Object.keys(specs), ...Object.values(targetManifests).flatMap((manifest) => refinoDependencies(manifest))])];
220
+ let fromManifests;
221
+ let toManifests;
222
+ try {
223
+ toManifests = await manifestsFor(options.registry, moving, target);
224
+ fromManifests = await manifestsFor(options.registry, moving, installed);
225
+ }
226
+ catch (error) {
227
+ return manual("registry", String(error));
228
+ }
229
+ for (const name of moving) {
230
+ const from = fromManifests[name];
231
+ const to = toManifests[name];
232
+ if (!from || !to)
233
+ return manual("registry", `${name} is not on the registry at ${from ? target : installed}.`);
234
+ const change = releaseShapeChange(from, to, moving);
235
+ if (change)
236
+ return manual("dependency-change", change);
237
+ }
238
+ const nextPackageJson = setDependencySpecifiers(packageJsonText, specChanges);
239
+ if (nextPackageJson === null)
240
+ return manual("package-json", `${packageJsonPath} names a Refino package somewhere other than its dependencies, or could not be edited in place.`);
241
+ if (nextPackageJson !== packageJsonText)
242
+ changes.push({ path: packageJsonPath, content: nextPackageJson });
243
+ if (lockfile.kind !== "none") {
244
+ if (lockText === null)
245
+ return manual("lockfile", `${lockfile.path} could not be read.`);
246
+ const releases = (manifests) => Object.fromEntries(Object.entries(manifests).map(([name, manifest]) => [name, toRelease(manifest)]));
247
+ const rewritten = rewriteLockfile(lockfile.kind, lockText, {
248
+ importer: relative(dirname(lockfile.path), app.dir),
249
+ specs: specChanges,
250
+ packages: moving,
251
+ from: installed,
252
+ to: target,
253
+ fromReleases: releases(fromManifests),
254
+ toReleases: releases(toManifests),
255
+ });
256
+ if (!rewritten.ok)
257
+ return manual("lockfile", rewritten.reason);
258
+ if (rewritten.text !== lockText)
259
+ changes.push({ path: lockfile.path, content: rewritten.text });
260
+ packageManager = lockfile.kind;
261
+ }
262
+ }
263
+ const byPath = new Map(templates.map((file) => [file.path, file]));
264
+ for (const state of states) {
265
+ if (state.status === "create" || state.status === "update")
266
+ changes.push({ path: join(app.dir, state.path), content: byPath.get(state.path).content });
267
+ }
268
+ const nextManifest = serializeGeneratedManifest(nextGeneratedManifest(templates, states, manifest, target));
269
+ if (nextManifest !== manifestText)
270
+ changes.push({ path: manifestPath, content: nextManifest });
271
+ return { kind: "update", installed, target, packageManager, files: changes, message: updateCommitMessage(target) };
272
+ }
273
+ async function manifestsFor(registry, names, version) {
274
+ const found = {};
275
+ for (const name of names) {
276
+ const manifest = await registry.manifest(name, version);
277
+ if (manifest)
278
+ found[name] = manifest;
279
+ }
280
+ return found;
281
+ }
@@ -0,0 +1,73 @@
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
+ export type LockfileKind = "npm" | "pnpm" | "yarn";
26
+ /** The lockfile names this module understands, and the ones it recognises only to refuse. */
27
+ export declare const LOCKFILES: Readonly<Record<string, LockfileKind | "unsupported">>;
28
+ /** One published version of a package, as the registry describes its tarball. */
29
+ export interface PackageRelease {
30
+ readonly name: string;
31
+ readonly version: string;
32
+ readonly tarball: string;
33
+ /** Subresource-integrity string, e.g. `sha512-…`. */
34
+ readonly integrity: string;
35
+ /** Hex sha1 of the tarball; yarn classic appends it to the resolved URL. */
36
+ readonly shasum: string;
37
+ }
38
+ export interface LockfileRewrite {
39
+ /** App directory relative to the lockfile's directory; "." when they are the same. */
40
+ readonly importer: string;
41
+ /** Direct dependencies of the app, by name: the specifier now and the one to write. */
42
+ readonly specs: Readonly<Record<string, {
43
+ readonly from: string;
44
+ readonly to: string;
45
+ }>>;
46
+ /** Every package being moved: the direct ones plus Refino packages they depend on. */
47
+ readonly packages: readonly string[];
48
+ /** The one version every moved package resolves to now. */
49
+ readonly from: string;
50
+ readonly to: string;
51
+ readonly fromReleases: Readonly<Record<string, PackageRelease>>;
52
+ readonly toReleases: Readonly<Record<string, PackageRelease>>;
53
+ }
54
+ export type LockfileResult = {
55
+ readonly ok: true;
56
+ readonly text: string;
57
+ } | {
58
+ readonly ok: false;
59
+ readonly reason: string;
60
+ };
61
+ /**
62
+ * The versions a lockfile resolves the app's direct Refino dependencies to.
63
+ * `specs` maps each dependency name to its package.json specifier.
64
+ */
65
+ export declare function readLockedVersions(kind: LockfileKind, text: string, importer: string, specs: Readonly<Record<string, string>>): {
66
+ readonly ok: true;
67
+ readonly versions: Readonly<Record<string, string>>;
68
+ } | {
69
+ readonly ok: false;
70
+ readonly reason: string;
71
+ };
72
+ /** Rewrite a lockfile so the Refino packages move from `request.from` to `request.to`, or say why it cannot be done safely. */
73
+ export declare function rewriteLockfile(kind: LockfileKind, text: string, request: LockfileRewrite): LockfileResult;