treelay 0.1.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.
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/SPEC.md +920 -0
- package/dist/chunk-QKQUPZVD.js +2665 -0
- package/dist/chunk-QKQUPZVD.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +293 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1399 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1399 @@
|
|
|
1
|
+
import { Liquid } from 'liquidjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layer reference syntax — SPEC §2 ("Referencing layers") and §3.
|
|
5
|
+
*
|
|
6
|
+
* A ref names where a layer's content comes from. Three origins, distinguished
|
|
7
|
+
* purely by shape so nothing has to be declared twice:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* ../base local path (relative or absolute)
|
|
11
|
+
* file:./base local path, explicit
|
|
12
|
+
* git+https://host/o/r.git#v1.2.0 git at a commit-ish
|
|
13
|
+
* git+ssh://git@host/o/r.git#main git over ssh
|
|
14
|
+
* git+file:///srv/repos/pkgs.git#v1 git with a local remote (offline/tests)
|
|
15
|
+
* github:acme/base#v2 shorthand for git+https://github.com/acme/base.git
|
|
16
|
+
* @acme/base@^2 npm package, resolved through node_modules
|
|
17
|
+
* npm:@acme/base@^2 npm, explicit
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Any non-local form may carry `?path=<subdir>` to use a subdirectory of the
|
|
21
|
+
* fetched tree as the layer root — the monorepo-of-layers case (`?path=core/_layer`).
|
|
22
|
+
*
|
|
23
|
+
* Parsing is pure and total: it never touches the network or the filesystem.
|
|
24
|
+
* {@link canonicalRef} is the single normalized string used as a lockfile key,
|
|
25
|
+
* so two spellings of the same thing cannot occupy two lock entries.
|
|
26
|
+
*/
|
|
27
|
+
/** Where a layer's bytes come from. */
|
|
28
|
+
type RefKind = "local" | "git" | "npm";
|
|
29
|
+
/** A ref that is fetched and pinned — everything except a local path. */
|
|
30
|
+
type RemoteRefKind = Exclude<RefKind, "local">;
|
|
31
|
+
interface BaseRef {
|
|
32
|
+
/** The ref exactly as written in the manifest. */
|
|
33
|
+
raw: string;
|
|
34
|
+
}
|
|
35
|
+
/** A path on this machine; never locked, never cached. */
|
|
36
|
+
interface LocalRef extends BaseRef {
|
|
37
|
+
kind: "local";
|
|
38
|
+
/** The path portion, with any `file:` scheme stripped. */
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
/** A git repository at a commit-ish (branch, tag, or full SHA). */
|
|
42
|
+
interface GitRef extends BaseRef {
|
|
43
|
+
kind: "git";
|
|
44
|
+
/** Clone URL, scheme included, `git+` prefix stripped. */
|
|
45
|
+
url: string;
|
|
46
|
+
/** Branch / tag / SHA as written. Defaults to `HEAD`. */
|
|
47
|
+
committish: string;
|
|
48
|
+
/** Subdirectory of the repo that forms the layer root. */
|
|
49
|
+
subdir?: string;
|
|
50
|
+
}
|
|
51
|
+
/** An npm package, resolved through the installed `node_modules` tree. */
|
|
52
|
+
interface NpmRef extends BaseRef {
|
|
53
|
+
kind: "npm";
|
|
54
|
+
/** Package name, scope included. */
|
|
55
|
+
name: string;
|
|
56
|
+
/** Semver range as written. Defaults to `*`. */
|
|
57
|
+
range: string;
|
|
58
|
+
/** Subdirectory of the package that forms the layer root. */
|
|
59
|
+
subdir?: string;
|
|
60
|
+
}
|
|
61
|
+
type ParsedRef = LocalRef | GitRef | NpmRef;
|
|
62
|
+
/** Refs that are fetched, cached and pinned in `treelay.lock`. */
|
|
63
|
+
type RemoteRef = GitRef | NpmRef;
|
|
64
|
+
/** Raised when a ref cannot be parsed at all. */
|
|
65
|
+
declare class InvalidRefError extends Error {
|
|
66
|
+
readonly ref: string;
|
|
67
|
+
constructor(ref: string, reason: string);
|
|
68
|
+
}
|
|
69
|
+
/** Is this ref a local path (the only kind that is never fetched)? */
|
|
70
|
+
declare function isLocalRef(ref: string): boolean;
|
|
71
|
+
/** Parse a layer reference. Pure: no network, no filesystem. */
|
|
72
|
+
declare function parseRef(ref: string): ParsedRef;
|
|
73
|
+
/**
|
|
74
|
+
* The normalized spelling of a ref — the lockfile key.
|
|
75
|
+
*
|
|
76
|
+
* Two manifests writing `github:acme/base#v2` and
|
|
77
|
+
* `git+https://github.com/acme/base.git#v2` mean the same layer and must share
|
|
78
|
+
* one lock entry, so the key is derived from the parsed shape rather than the
|
|
79
|
+
* text. Local refs canonicalize to themselves; they are never locked.
|
|
80
|
+
*/
|
|
81
|
+
declare function canonicalRef(ref: ParsedRef | string): string;
|
|
82
|
+
/**
|
|
83
|
+
* The immutable spelling of a ref once resolved — what the lock pins to.
|
|
84
|
+
*
|
|
85
|
+
* `git+…#main` resolves to `git+…#<sha>`; `npm:pkg@^2` to `npm:pkg@2.3.1`.
|
|
86
|
+
* Recording this (alongside the requested form) is what lets drift detection
|
|
87
|
+
* say precisely *what moved*.
|
|
88
|
+
*/
|
|
89
|
+
declare function pinnedRef(ref: RemoteRef, revision: string): string;
|
|
90
|
+
/** A 40-hex git object name — already immutable, so nothing to re-resolve. */
|
|
91
|
+
declare function isCommitSha(committish: string): boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `treelay.lock` — resolved lineage and pinned revisions (SPEC §3, §9).
|
|
95
|
+
*
|
|
96
|
+
* The lockfile sits beside the **leaf layer's manifest** and is committed to
|
|
97
|
+
* version control. It answers one question: *what exactly was materialized?* A
|
|
98
|
+
* manifest says `#main`; the lock says which commit `main` was when the tree was
|
|
99
|
+
* last resolved, plus an integrity hash of the content that produced.
|
|
100
|
+
*
|
|
101
|
+
* Note the division of labour with `<dest>/.treelay/lock.json` (§7): that file
|
|
102
|
+
* records what a *destination* was built from, and is regenerated on every
|
|
103
|
+
* compile. `treelay.lock` records what the *source composition* pins to, and
|
|
104
|
+
* changes only when a ref is added or deliberately advanced.
|
|
105
|
+
*
|
|
106
|
+
* Serialization is deterministic — sorted keys throughout, fixed field order,
|
|
107
|
+
* two-space indent, trailing newline — so re-running `treelay lock` on an
|
|
108
|
+
* unchanged tree produces a byte-identical file and never shows up in a diff.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/** Filename of the source-side lockfile, beside the leaf manifest. */
|
|
112
|
+
declare const LOCKFILE_NAME = "treelay.lock";
|
|
113
|
+
/** Bumped when the on-disk shape changes incompatibly. */
|
|
114
|
+
declare const LOCKFILE_VERSION = 1;
|
|
115
|
+
/** One pinned layer reference. */
|
|
116
|
+
interface LockEntry {
|
|
117
|
+
kind: RemoteRefKind;
|
|
118
|
+
/** Clone URL (git) or package name (npm). */
|
|
119
|
+
source: string;
|
|
120
|
+
/** The mutable thing that was asked for: branch, tag, or semver range. */
|
|
121
|
+
requested: string;
|
|
122
|
+
/** The immutable revision it resolved to: commit SHA, or exact version. */
|
|
123
|
+
resolved: string;
|
|
124
|
+
/** `sha256:…` over the materialized tree — detects a tampered cache. */
|
|
125
|
+
integrity: string;
|
|
126
|
+
/** Subdirectory of the fetched tree used as the layer root, if any. */
|
|
127
|
+
path?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Which layers asked for this ref, as paths relative to the lockfile (local
|
|
130
|
+
* layers) or canonical refs (remote ones). Sorted. Purely informational, but
|
|
131
|
+
* it is what makes a held-back pin explainable months later.
|
|
132
|
+
*/
|
|
133
|
+
requestedBy?: string[];
|
|
134
|
+
}
|
|
135
|
+
/** The parsed lockfile. */
|
|
136
|
+
interface TreelayLock {
|
|
137
|
+
lockfileVersion: number;
|
|
138
|
+
/** Canonical ref → what it pinned to. */
|
|
139
|
+
refs: Record<string, LockEntry>;
|
|
140
|
+
}
|
|
141
|
+
/** An empty, valid lock — what a tree with no remote refs serializes to. */
|
|
142
|
+
declare function emptyLock(): TreelayLock;
|
|
143
|
+
/** Absolute path of the lockfile for a leaf layer directory. */
|
|
144
|
+
declare function lockfilePath(leafDir: string): string;
|
|
145
|
+
/** Raised when a lockfile exists but cannot be used. */
|
|
146
|
+
declare class LockfileError extends Error {
|
|
147
|
+
constructor(file: string, reason: string);
|
|
148
|
+
}
|
|
149
|
+
/** Read the lockfile beside a leaf layer, or an empty lock when there is none. */
|
|
150
|
+
declare function readLock(leafDir: string): TreelayLock;
|
|
151
|
+
/**
|
|
152
|
+
* Render a lock to its canonical text.
|
|
153
|
+
*
|
|
154
|
+
* Field order is fixed rather than alphabetical because these files are read by
|
|
155
|
+
* humans during incident review: what was asked for, then what it became.
|
|
156
|
+
*/
|
|
157
|
+
declare function serializeLock(lock: TreelayLock): string;
|
|
158
|
+
/** Write the lock beside a leaf layer. Returns true when the bytes changed. */
|
|
159
|
+
declare function writeLock(leafDir: string, lock: TreelayLock): boolean;
|
|
160
|
+
/** Do two locks pin the same things to the same revisions? */
|
|
161
|
+
declare function locksEqual(a: TreelayLock, b: TreelayLock): boolean;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Core type definitions for treelay.
|
|
165
|
+
*
|
|
166
|
+
* See SPEC.md — these mirror the manifest (§2), merge semantics (§4), the
|
|
167
|
+
* `.treelay` sidecar (§4), variables (§6), and the programmatic API (§10).
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/** A reference to another layer: local path, npm spec, or git spec. */
|
|
171
|
+
type LayerRef = string;
|
|
172
|
+
/**
|
|
173
|
+
* Where a layer's content came from, once resolved (§3).
|
|
174
|
+
*
|
|
175
|
+
* Local layers are edited in place; fetched ones live in the content-addressed
|
|
176
|
+
* cache at an exact revision and are read-only by construction — which is also
|
|
177
|
+
* what disqualifies them as promotion targets (§8).
|
|
178
|
+
*/
|
|
179
|
+
interface LayerOrigin {
|
|
180
|
+
kind: "local" | "git" | "npm";
|
|
181
|
+
/** Canonical ref (lockfile key). Absent for the leaf and plain local layers. */
|
|
182
|
+
ref?: string;
|
|
183
|
+
/** Exact revision materialized: commit SHA or package version. */
|
|
184
|
+
revision?: string;
|
|
185
|
+
/** `sha256:…` over the materialized tree, as recorded in the lock. */
|
|
186
|
+
integrity?: string;
|
|
187
|
+
}
|
|
188
|
+
/** Per-file merge strategy (§4). */
|
|
189
|
+
type MergeStrategy = "replace" | "deep-merge" | "patch" | "append" | "prepend" | "delete";
|
|
190
|
+
/**
|
|
191
|
+
* A `.treelay` sidecar's `op:` (§4). Distinct from `MergeStrategy`: `merge` here
|
|
192
|
+
* is a *structured patch* (RFC 7386/6902) onto the inherited file, whereas the
|
|
193
|
+
* strategy `deep-merge` merges two whole files when the same path exists in
|
|
194
|
+
* multiple layers.
|
|
195
|
+
*/
|
|
196
|
+
type SidecarOpKind = "replace" | "patch" | "merge" | "append" | "prepend" | "delete";
|
|
197
|
+
/** How `deep-merge` treats arrays (§4). */
|
|
198
|
+
type ArrayPolicy = "replace" | "concat" | "by-key";
|
|
199
|
+
/** Whether non-suffixed text files are rendered (§6). */
|
|
200
|
+
type RenderMode = "suffix" | "all-text";
|
|
201
|
+
/** The declared type of a template variable (§6). */
|
|
202
|
+
type VariableType = "string" | "number" | "boolean" | "json" | "yaml" | "path";
|
|
203
|
+
/** A single variable declaration in a manifest (§6). */
|
|
204
|
+
interface VariableDecl {
|
|
205
|
+
type: VariableType;
|
|
206
|
+
/** Prompt text; omit to never prompt (pure default/computed). */
|
|
207
|
+
prompt?: string;
|
|
208
|
+
/** Default value; may itself be a Liquid template string. */
|
|
209
|
+
default?: unknown;
|
|
210
|
+
/** Optional enum of allowed values. */
|
|
211
|
+
choices?: unknown[];
|
|
212
|
+
/** Liquid expression — when falsy, the question and value are skipped. */
|
|
213
|
+
when?: string;
|
|
214
|
+
/** Liquid template that renders to "" when valid, else the error message. */
|
|
215
|
+
validate?: string;
|
|
216
|
+
/** Masked input; excluded from the persisted answers file. */
|
|
217
|
+
secret?: boolean;
|
|
218
|
+
/** Derived from other variables; never prompted. */
|
|
219
|
+
computed?: boolean;
|
|
220
|
+
}
|
|
221
|
+
/** The `treelay.json` manifest (or `"treelay"` key in package.json) — §2. */
|
|
222
|
+
interface Manifest {
|
|
223
|
+
name?: string;
|
|
224
|
+
/** Inherit-only; not compilable as a leaf. */
|
|
225
|
+
abstract?: boolean;
|
|
226
|
+
parents?: LayerRef[];
|
|
227
|
+
mixins?: LayerRef[];
|
|
228
|
+
/**
|
|
229
|
+
* Trees vendored into the composed output at a fixed subpath (§3).
|
|
230
|
+
*
|
|
231
|
+
* `{ "packages": "git+file:///srv/packages.git#v1.2.0" }` materializes that
|
|
232
|
+
* repo at `packages/` in every compiled tree. Mount *paths* merge across the
|
|
233
|
+
* graph by ordinary layer precedence, so a leaf can hold one back at an older
|
|
234
|
+
* ref while its parents float — a ref override, not a separate mechanism.
|
|
235
|
+
*/
|
|
236
|
+
mounts?: Record<string, LayerRef>;
|
|
237
|
+
ignore?: string[];
|
|
238
|
+
/** glob → strategy defaults. */
|
|
239
|
+
merge?: Record<string, MergeStrategy>;
|
|
240
|
+
arrays?: ArrayPolicy;
|
|
241
|
+
/** Suffix that marks a file for rendering (default ".tmpl"). */
|
|
242
|
+
templateSuffix?: string;
|
|
243
|
+
render?: RenderMode;
|
|
244
|
+
variables?: Record<string, VariableDecl>;
|
|
245
|
+
}
|
|
246
|
+
/** A loaded layer: its resolved location plus parsed manifest. */
|
|
247
|
+
interface Layer {
|
|
248
|
+
/**
|
|
249
|
+
* Stable identity. A local layer is identified by its absolute directory; a
|
|
250
|
+
* fetched one by its canonical ref, so provenance stays meaningful across
|
|
251
|
+
* machines whose caches sit in different places.
|
|
252
|
+
*/
|
|
253
|
+
id: string;
|
|
254
|
+
/** Absolute path to the layer's root directory (a cache path when fetched). */
|
|
255
|
+
dir: string;
|
|
256
|
+
manifest: Manifest;
|
|
257
|
+
/** Whether the layer dir is writable (false for node_modules/git installs) — §8. */
|
|
258
|
+
writable: boolean;
|
|
259
|
+
/** Where the content came from; local with no ref when omitted. */
|
|
260
|
+
origin?: LayerOrigin;
|
|
261
|
+
/**
|
|
262
|
+
* Subpath this layer's files are re-rooted under in the output (§3 mounts).
|
|
263
|
+
* Absent for ordinary layers, which compose at the tree root.
|
|
264
|
+
*/
|
|
265
|
+
mountPath?: string;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The fully resolved composition: layers ordered lowest → highest precedence,
|
|
269
|
+
* plus the merged variable schema across all of them (§3, §6).
|
|
270
|
+
*/
|
|
271
|
+
interface ResolvedGraph {
|
|
272
|
+
/** Lowest precedence first; the leaf/self is last. */
|
|
273
|
+
layers: Layer[];
|
|
274
|
+
/** Merged variable declarations across the whole graph (§6). */
|
|
275
|
+
variables: Record<string, VariableDecl>;
|
|
276
|
+
/**
|
|
277
|
+
* Pinned revisions for every remote ref this graph touched, in memory.
|
|
278
|
+
*
|
|
279
|
+
* Resolution deliberately does not write it: `resolve` is called by read-only
|
|
280
|
+
* commands too, and a lockfile appearing as a side effect of `explain` would
|
|
281
|
+
* be a surprise. Callers that own the source tree (`compile`, `treelay lock`)
|
|
282
|
+
* persist it when {@link ResolvedGraph.lockDirty} is set.
|
|
283
|
+
*
|
|
284
|
+
* Optional because a graph can also be *synthesized* — reflux builds partial
|
|
285
|
+
* stacks to test shadowing (§8) — and a synthetic stack pins nothing.
|
|
286
|
+
*/
|
|
287
|
+
lock?: TreelayLock;
|
|
288
|
+
/** True when `lock` differs from what is on disk (entries added or advanced). */
|
|
289
|
+
lockDirty?: boolean;
|
|
290
|
+
/** Leaf directory the lockfile belongs beside. */
|
|
291
|
+
lockDir?: string;
|
|
292
|
+
}
|
|
293
|
+
/** A `.treelay` sidecar operation on an inherited file (§4). */
|
|
294
|
+
interface SidecarOp {
|
|
295
|
+
op: SidecarOpKind;
|
|
296
|
+
/**
|
|
297
|
+
* Hash of the parent content the patch was authored against (§5). Detects
|
|
298
|
+
* drift: when it still matches the inherited file, the patch is known to
|
|
299
|
+
* apply exactly; when it doesn't, compile takes the reconciling path.
|
|
300
|
+
*/
|
|
301
|
+
base?: string;
|
|
302
|
+
/**
|
|
303
|
+
* The parent content itself. A hash can detect drift but cannot reconstruct
|
|
304
|
+
* the base, so this is what actually enables a *true* diff3 once the
|
|
305
|
+
* inherited file has moved on. Reflux (§8) records it automatically.
|
|
306
|
+
*/
|
|
307
|
+
baseContent?: string;
|
|
308
|
+
/** Liquid expression; skip the op when falsy. */
|
|
309
|
+
when?: string;
|
|
310
|
+
/** Render the payload/result with variables (§6). */
|
|
311
|
+
render?: boolean;
|
|
312
|
+
/** Unified-diff payload for `op: patch`. */
|
|
313
|
+
patch?: string;
|
|
314
|
+
/** RFC 7386 JSON Merge Patch for `op: merge`. */
|
|
315
|
+
merge?: unknown;
|
|
316
|
+
/** RFC 6902 JSON Patch for `op: merge`. */
|
|
317
|
+
jsonPatch?: unknown[];
|
|
318
|
+
/** Literal payload for `op: append` / `prepend` / `replace`. */
|
|
319
|
+
content?: string;
|
|
320
|
+
}
|
|
321
|
+
/** Where a compiled file came from, for `explain` (§10). */
|
|
322
|
+
interface FileProvenance {
|
|
323
|
+
/** Layer id that last produced/owned the content. */
|
|
324
|
+
fromLayer: string;
|
|
325
|
+
strategy: MergeStrategy;
|
|
326
|
+
/** Layer ids whose patches/merges were applied, in order. */
|
|
327
|
+
patchedFrom?: string[];
|
|
328
|
+
/** True if user-created in the destination, not template-generated (§7). */
|
|
329
|
+
owned: boolean;
|
|
330
|
+
}
|
|
331
|
+
/** Result of a compile (§10). */
|
|
332
|
+
interface CompileResult {
|
|
333
|
+
files: Record<string, FileProvenance>;
|
|
334
|
+
}
|
|
335
|
+
/** A resolved set of variable values keyed by name. */
|
|
336
|
+
type Values = Record<string, unknown>;
|
|
337
|
+
/** The kind of change a destination file represents vs baseline (§8). */
|
|
338
|
+
type ChangeKind = "modified" | "added" | "deleted";
|
|
339
|
+
/** A single change detected by `status` (§8). */
|
|
340
|
+
interface Change {
|
|
341
|
+
path: string;
|
|
342
|
+
kind: ChangeKind;
|
|
343
|
+
/** Layer id currently producing this file (undefined for user-added). */
|
|
344
|
+
producingLayer?: string;
|
|
345
|
+
/**
|
|
346
|
+
* Writable layers this change could be promoted into, already filtered to
|
|
347
|
+
* those a higher layer would not immediately shadow (§8 guard 1).
|
|
348
|
+
*/
|
|
349
|
+
targets?: string[];
|
|
350
|
+
/** Layers that patched/merged on top of the producer, in order. */
|
|
351
|
+
patchedBy?: string[];
|
|
352
|
+
/** True when the destination owns the file and no layer produces it (§7). */
|
|
353
|
+
owned?: boolean;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Error types used across treelay. */
|
|
357
|
+
/** A cycle was found in the parents/mixins graph (§3). */
|
|
358
|
+
declare class CycleError extends Error {
|
|
359
|
+
readonly path: string[];
|
|
360
|
+
constructor(path: string[]);
|
|
361
|
+
}
|
|
362
|
+
/** C3 could not produce a consistent linearization (§3). */
|
|
363
|
+
declare class InconsistentHierarchyError extends Error {
|
|
364
|
+
constructor(message: string);
|
|
365
|
+
}
|
|
366
|
+
/** A patch/merge could not be applied cleanly (§5). */
|
|
367
|
+
declare class MergeConflictError extends Error {
|
|
368
|
+
readonly file: string;
|
|
369
|
+
constructor(file: string, message: string);
|
|
370
|
+
}
|
|
371
|
+
/** Placeholder for not-yet-built functionality during scaffolding. */
|
|
372
|
+
declare class NotImplementedError extends Error {
|
|
373
|
+
constructor(what: string);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* C3 linearization (Python's MRO) over the `parents` graph — SPEC §3.
|
|
378
|
+
*
|
|
379
|
+
* This is the principled answer to multiple inheritance: it resolves diamonds
|
|
380
|
+
* (a shared ancestor appears once, before its descendants), respects the local
|
|
381
|
+
* order parents were declared in, and is monotonic. The naive alternative
|
|
382
|
+
* (depth-first + last-wins dedupe) produces surprising orders in diamonds.
|
|
383
|
+
*
|
|
384
|
+
* Output is MRO order: most-derived first (root, then ancestors high → low
|
|
385
|
+
* precedence). Callers that apply layers lowest-precedence-first should reverse
|
|
386
|
+
* the ancestor portion. See `resolve.ts`.
|
|
387
|
+
*/
|
|
388
|
+
/**
|
|
389
|
+
* Linearize `root` over a parent graph.
|
|
390
|
+
*
|
|
391
|
+
* @param root the starting node
|
|
392
|
+
* @param parentsOf returns the direct parents of a node, in declaration order
|
|
393
|
+
* @param key stable identity for a node (defaults to String)
|
|
394
|
+
* @returns MRO: `[root, ...ancestors]`, highest precedence first
|
|
395
|
+
*/
|
|
396
|
+
declare function c3Linearize<T>(root: T, parentsOf: (node: T) => T[], key?: (node: T) => string): T[];
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolve a leaf overlay directory into an ordered, linearized layer stack
|
|
400
|
+
* plus the merged variable schema — SPEC §3, §6.
|
|
401
|
+
*
|
|
402
|
+
* Precedence, lowest → highest: mounts < parents (C3-linearized) < mixins < self.
|
|
403
|
+
*
|
|
404
|
+
* Resolution is synchronous even though it may clone repositories. Every caller
|
|
405
|
+
* in the library and CLI treats `resolve` as a plain function, and a build
|
|
406
|
+
* cannot proceed without its layers anyway, so there is nothing to overlap with
|
|
407
|
+
* — the cost of going async would be paid by every consumer for no gain.
|
|
408
|
+
*/
|
|
409
|
+
|
|
410
|
+
interface ResolveOptions {
|
|
411
|
+
/**
|
|
412
|
+
* Refuse to resolve any ref the lockfile does not already pin. The CI
|
|
413
|
+
* posture: a build either reproduces from what was committed, or it fails.
|
|
414
|
+
*/
|
|
415
|
+
frozen?: boolean;
|
|
416
|
+
/** Re-resolve moving refs to their current upstream revision. */
|
|
417
|
+
updateRefs?: boolean;
|
|
418
|
+
/** Ignore the lockfile entirely — resolve everything live, pin nothing. */
|
|
419
|
+
noLock?: boolean;
|
|
420
|
+
/** Override the fetch cache root (tests). */
|
|
421
|
+
cacheDir?: string;
|
|
422
|
+
}
|
|
423
|
+
/** Raised when a `mounts` declaration cannot be materialized coherently. */
|
|
424
|
+
declare class MountError extends Error {
|
|
425
|
+
constructor(mountPath: string, reason: string);
|
|
426
|
+
}
|
|
427
|
+
/** Resolve a leaf directory into its full composition. */
|
|
428
|
+
declare function resolve(srcDir: string, options?: ResolveOptions): ResolvedGraph;
|
|
429
|
+
/**
|
|
430
|
+
* Resolve a layer reference to an absolute directory on disk.
|
|
431
|
+
*
|
|
432
|
+
* Retained as the low-level entry point: local paths resolve directly, and
|
|
433
|
+
* fetched refs are materialized into the cache and their root returned.
|
|
434
|
+
*/
|
|
435
|
+
declare function resolveRef(ref: LayerRef, fromDir: string, options?: ResolveOptions & {
|
|
436
|
+
lock?: TreelayLock;
|
|
437
|
+
}): string;
|
|
438
|
+
|
|
439
|
+
/** Manifest loading — SPEC §2. */
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Load the manifest for a layer directory. Looks for a standalone
|
|
443
|
+
* `treelay.{json,yaml,yml}`, then falls back to a `"treelay"` key in
|
|
444
|
+
* `package.json`. Returns an empty manifest if none is found (a plain
|
|
445
|
+
* directory is a valid, parent-less layer).
|
|
446
|
+
*/
|
|
447
|
+
declare function loadManifest(dir: string): Manifest;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Variable declaration merge + value resolution — SPEC §6.
|
|
451
|
+
*
|
|
452
|
+
* Declarations merge across the linearized stack (parents < mixins < self),
|
|
453
|
+
* producing one merged questionnaire for the whole composition. Values are then
|
|
454
|
+
* resolved lowest → highest: declared default → answers file → prompts → CLI/env.
|
|
455
|
+
*/
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Merge variable declarations across layers (lowest → highest precedence).
|
|
459
|
+
* Same-named declarations merge per-key, so a child can override just the
|
|
460
|
+
* `default` while inheriting the parent's `prompt`/`type`.
|
|
461
|
+
*/
|
|
462
|
+
declare function mergeVariableDecls(layers: Layer[]): Record<string, VariableDecl>;
|
|
463
|
+
/** Options controlling how values are sourced. */
|
|
464
|
+
interface ResolveValuesOptions {
|
|
465
|
+
/** Pre-supplied answers (e.g. loaded from `.treelay/answers.json`). */
|
|
466
|
+
answers?: Values;
|
|
467
|
+
/** CLI `--set k=v` overrides (highest precedence before computed). */
|
|
468
|
+
set?: Values;
|
|
469
|
+
/** Whether to prompt interactively for missing, prompt-able variables. */
|
|
470
|
+
prompt?: boolean;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Resolve final variable values for a graph (§6 steps 3–4): collect from
|
|
474
|
+
* defaults/answers/overrides, prompt for the rest, then evaluate computed and
|
|
475
|
+
* templated defaults in dependency order via a fixpoint over the declarations.
|
|
476
|
+
*/
|
|
477
|
+
declare function resolveValues(graph: ResolvedGraph, options?: ResolveValuesOptions): Promise<Values>;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Template rendering via LiquidJS — SPEC §6.
|
|
481
|
+
*
|
|
482
|
+
* LiquidJS is chosen for safety: layers arrive as third-party npm packages, so
|
|
483
|
+
* the engine must not allow arbitrary code execution or filesystem/network
|
|
484
|
+
* access. We construct the engine with file-system access disabled; a template
|
|
485
|
+
* can only read the resolved variable values.
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
/** A sandboxed Liquid engine — no includes/layouts from disk. */
|
|
489
|
+
declare function createEngine(): Liquid;
|
|
490
|
+
/** Render a single template string with the given values. */
|
|
491
|
+
declare function renderString(template: string, values: Values): Promise<string>;
|
|
492
|
+
/**
|
|
493
|
+
* Decide whether a file path should be rendered, and return its output name.
|
|
494
|
+
* Suffix opt-in (default): only `*.tmpl` files render, with the suffix stripped.
|
|
495
|
+
*/
|
|
496
|
+
declare function templateTarget(path: string, suffix?: string): {
|
|
497
|
+
render: boolean;
|
|
498
|
+
outPath: string;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* `.treelay` sidecar parsing & detection — SPEC §4.
|
|
503
|
+
*
|
|
504
|
+
* A `<targetPath>.treelay` file describes an operation on the inherited file:
|
|
505
|
+
* the strategy, an optional base hash (enabling true 3-way merge, §5), an
|
|
506
|
+
* optional `when:` guard, and the payload. This is the canonical, full-power
|
|
507
|
+
* form; the filename suffixes (`.patch`/`.append`/`.delete`) are sugar that
|
|
508
|
+
* desugar to one of these ops.
|
|
509
|
+
*/
|
|
510
|
+
|
|
511
|
+
/** Is this path a sidecar? (Distinct from the `.treelay/` state dir, §7.) */
|
|
512
|
+
declare function isSidecar(path: string): boolean;
|
|
513
|
+
/** Map a sidecar path to the inherited file it operates on. */
|
|
514
|
+
declare function sidecarTarget(path: string): string;
|
|
515
|
+
/** Parse sidecar YAML into a validated op. */
|
|
516
|
+
declare function parseSidecar(yaml: string): SidecarOp;
|
|
517
|
+
/**
|
|
518
|
+
* If `path` ends in a known merge suffix, return the desugared op and the
|
|
519
|
+
* target path it applies to. A bare `.patch` has no recorded `base`, so it is
|
|
520
|
+
* best-effort apply rather than true 3-way (§5).
|
|
521
|
+
*/
|
|
522
|
+
declare function desugarSuffix(path: string): {
|
|
523
|
+
op: SidecarOpKind;
|
|
524
|
+
target: string;
|
|
525
|
+
} | undefined;
|
|
526
|
+
|
|
527
|
+
/** Recursive deep merge for structured files — SPEC §4. */
|
|
528
|
+
|
|
529
|
+
type Json = unknown;
|
|
530
|
+
/**
|
|
531
|
+
* Deep-merge `over` onto `base`. Objects merge recursively; arrays follow the
|
|
532
|
+
* `arrays` policy. Default `replace` because concat surprises people (§4).
|
|
533
|
+
*/
|
|
534
|
+
declare function deepMerge(base: Json, over: Json, arrays?: ArrayPolicy): Json;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Unified-diff 3-way merge — SPEC §5.
|
|
538
|
+
*
|
|
539
|
+
* Two paths, chosen by whether the caller can supply the base *content* the
|
|
540
|
+
* patch was authored against:
|
|
541
|
+
*
|
|
542
|
+
* - **base known** → reconstruct the author's intent (`base` → `patched`), then
|
|
543
|
+
* reconcile it against the drift the inherited file actually took
|
|
544
|
+
* (`base` → `current`) with a real diff3. This resolves cleanly far more
|
|
545
|
+
* often than a flat apply and produces honest conflicts when it can't.
|
|
546
|
+
* - **base unknown** → best-effort apply onto `current`. jsdiff searches for
|
|
547
|
+
* each hunk's location, so hunks that merely *moved* still land; changed
|
|
548
|
+
* context is rejected rather than guessed at.
|
|
549
|
+
*
|
|
550
|
+
* Either way the function is all-or-nothing: it returns fully merged text or
|
|
551
|
+
* throws `MergeConflictError`. It never returns partially-patched content.
|
|
552
|
+
*/
|
|
553
|
+
/** Default conflict-marker labels, phrased for the `update` direction (§7). */
|
|
554
|
+
declare const MERGE_LABELS: {
|
|
555
|
+
readonly ours: "ours (your edits)";
|
|
556
|
+
readonly base: "base (last template output)";
|
|
557
|
+
readonly theirs: "theirs (new template)";
|
|
558
|
+
};
|
|
559
|
+
interface TextMerge3Args {
|
|
560
|
+
/** Common ancestor both sides diverged from. */
|
|
561
|
+
base: string;
|
|
562
|
+
/** The local side — kept first in conflict markers, as git does. */
|
|
563
|
+
ours: string;
|
|
564
|
+
/** The incoming side. */
|
|
565
|
+
theirs: string;
|
|
566
|
+
labels?: {
|
|
567
|
+
ours: string;
|
|
568
|
+
base: string;
|
|
569
|
+
theirs: string;
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
interface TextMerge3Result {
|
|
573
|
+
/** False when the two sides edited the same region. */
|
|
574
|
+
clean: boolean;
|
|
575
|
+
/** Merged text; carries diff3-style conflict markers when `clean` is false. */
|
|
576
|
+
text: string;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Three-way merge of raw text — the same diff3 engine `applyPatch3Way` uses,
|
|
580
|
+
* exposed directly for `update`, which already holds all three versions and has
|
|
581
|
+
* no patch to apply.
|
|
582
|
+
*
|
|
583
|
+
* Conflicts are *returned*, not thrown: unlike compile, `update` has to write
|
|
584
|
+
* something (markers or a `.rej`) so the user can resolve it in place.
|
|
585
|
+
* Markers include the base section, so you can see what the template previously
|
|
586
|
+
* produced rather than guessing why the two sides disagree.
|
|
587
|
+
*/
|
|
588
|
+
declare function mergeText3(args: TextMerge3Args): TextMerge3Result;
|
|
589
|
+
interface Patch3WayArgs {
|
|
590
|
+
/** Relative path, for error messages. */
|
|
591
|
+
file: string;
|
|
592
|
+
/** The inherited content the patch should land on. */
|
|
593
|
+
current: string;
|
|
594
|
+
/** Unified diff. Bare `@@` hunks are accepted (no `---`/`+++` headers). */
|
|
595
|
+
patch: string;
|
|
596
|
+
/**
|
|
597
|
+
* The exact content the patch was authored against. When supplied, enables a
|
|
598
|
+
* true 3-way merge; omit it for best-effort apply.
|
|
599
|
+
*/
|
|
600
|
+
base?: string;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Apply a unified diff onto `current`, reconciling against `base` when known.
|
|
604
|
+
*
|
|
605
|
+
* Returns the merged text, or throws MergeConflictError — never half-applies.
|
|
606
|
+
*/
|
|
607
|
+
declare function applyPatch3Way(args: Patch3WayArgs): string;
|
|
608
|
+
|
|
609
|
+
/** Structured patches for config files — SPEC §5. */
|
|
610
|
+
/**
|
|
611
|
+
* Apply an RFC 7386 JSON Merge Patch. `null` values in the patch delete keys;
|
|
612
|
+
* everything else recurses. The target is deep-cloned first so the caller's
|
|
613
|
+
* value is never mutated (the underlying library mutates in place).
|
|
614
|
+
*/
|
|
615
|
+
declare function applyMergePatch(target: unknown, patch: unknown): unknown;
|
|
616
|
+
/**
|
|
617
|
+
* Apply an RFC 6902 JSON Patch (precise array/path ops). Non-mutating: returns
|
|
618
|
+
* the new document and leaves the input untouched.
|
|
619
|
+
*/
|
|
620
|
+
declare function applyJsonPatch(target: unknown, ops: unknown[]): unknown;
|
|
621
|
+
interface StructuredMerge3Result {
|
|
622
|
+
clean: boolean;
|
|
623
|
+
/** The merged document; only meaningful when `clean`. */
|
|
624
|
+
value: unknown;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Three-way merge two structured documents by reconciling their *changes*
|
|
628
|
+
* rather than their lines (§7).
|
|
629
|
+
*
|
|
630
|
+
* Line-based merging conflicts on edits that happen to sit near each other;
|
|
631
|
+
* comparing `base → ours` and `base → theirs` as merge patches instead means
|
|
632
|
+
* two sides adding different keys — the common case for `package.json` — merges
|
|
633
|
+
* cleanly no matter where those keys landed in the file.
|
|
634
|
+
*/
|
|
635
|
+
declare function mergeStructured3(base: unknown, ours: unknown, theirs: unknown): StructuredMerge3Result;
|
|
636
|
+
|
|
637
|
+
/** Per-file merge strategy dispatch — SPEC §4. */
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Default strategy for a path when no manifest glob or sidecar specifies one:
|
|
641
|
+
* structured files deep-merge, binaries replace, everything else replaces (text
|
|
642
|
+
* gets patch/append only via explicit sidecar/suffix).
|
|
643
|
+
*/
|
|
644
|
+
declare function defaultStrategy(path: string): MergeStrategy;
|
|
645
|
+
/** Resolve the strategy for a path given the manifest `merge` globs (§4). */
|
|
646
|
+
declare function strategyFor(path: string, globs?: Record<string, MergeStrategy>): MergeStrategy;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Content hashing — the single source of truth for the `sha256:…` format used
|
|
650
|
+
* by `.treelay` sidecar `base:` fields (§5) and the destination baseline (§7).
|
|
651
|
+
*/
|
|
652
|
+
/** Hash content into the canonical `sha256:<hex>` form. */
|
|
653
|
+
declare function hashContent(data: Buffer | string): string;
|
|
654
|
+
/** Whether `data` matches a recorded `sha256:…` hash. */
|
|
655
|
+
declare function matchesHash(data: Buffer | string, recorded: string): boolean;
|
|
656
|
+
/**
|
|
657
|
+
* Hash a whole directory tree into one `sha256:…` — the lockfile's `integrity`.
|
|
658
|
+
*
|
|
659
|
+
* Deliberately independent of any VCS: it digests the *materialized content*,
|
|
660
|
+
* which is the thing a build actually consumes. A git commit SHA already says
|
|
661
|
+
* which revision was asked for; this says the bytes on disk still are that
|
|
662
|
+
* revision, so a corrupted or hand-edited cache entry is caught rather than
|
|
663
|
+
* silently compiled in.
|
|
664
|
+
*
|
|
665
|
+
* Paths are sorted and mixed into the digest alongside their content, so
|
|
666
|
+
* renaming a file changes the hash even when the bytes are identical.
|
|
667
|
+
*/
|
|
668
|
+
declare function hashTree(dir: string): string;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Destination `.treelay/` state — SPEC §7.
|
|
672
|
+
*
|
|
673
|
+
* Written into the compiled destination so the template link survives: the
|
|
674
|
+
* lockfile (resolved lineage), persisted answers (deterministic re-render),
|
|
675
|
+
* the baseline (merge base for update & reflux), and the generated/owned map.
|
|
676
|
+
*/
|
|
677
|
+
|
|
678
|
+
declare const STATE_DIR = ".treelay";
|
|
679
|
+
interface TreelayState {
|
|
680
|
+
lock: LockFile;
|
|
681
|
+
answers: Values;
|
|
682
|
+
/** relative path → content hash of what the template last produced. */
|
|
683
|
+
baseline: Record<string, string>;
|
|
684
|
+
/** relative path → whether the template owns it (vs user-created). */
|
|
685
|
+
manifest: Record<string, {
|
|
686
|
+
owned: boolean;
|
|
687
|
+
fromLayer: string;
|
|
688
|
+
}>;
|
|
689
|
+
}
|
|
690
|
+
interface LockFile {
|
|
691
|
+
/**
|
|
692
|
+
* Layer ids/refs lowest → highest precedence at last compile. The last entry
|
|
693
|
+
* is the leaf — this is how `update <dest>` rediscovers its source template
|
|
694
|
+
* without being told where it came from.
|
|
695
|
+
*/
|
|
696
|
+
lineage: string[];
|
|
697
|
+
/** Resolved version refs per layer (npm/git), for drift detection. */
|
|
698
|
+
versions: Record<string, string>;
|
|
699
|
+
}
|
|
700
|
+
declare const statePaths: (destDir: string) => {
|
|
701
|
+
dir: string;
|
|
702
|
+
lock: string;
|
|
703
|
+
answers: string;
|
|
704
|
+
baseline: string;
|
|
705
|
+
/** Content snapshot of the last template output — the diff3 merge base (§7). */
|
|
706
|
+
baselineDir: string;
|
|
707
|
+
manifest: string;
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* The leaf template a destination was compiled from, recovered from the lock.
|
|
711
|
+
* Returns undefined for a lockfile with no lineage (nothing to update against).
|
|
712
|
+
*/
|
|
713
|
+
declare function sourceOf(state: TreelayState): string | undefined;
|
|
714
|
+
/** Whether a destination already carries treelay state (⇒ `update`, not compile). */
|
|
715
|
+
declare function hasState(destDir: string): boolean;
|
|
716
|
+
/**
|
|
717
|
+
* Persist the state artifacts into `<destDir>/.treelay/`.
|
|
718
|
+
*
|
|
719
|
+
* `snapshot` is the exact content the template just produced. It is stored
|
|
720
|
+
* alongside the hash index because the two answer different questions: the hash
|
|
721
|
+
* cheaply decides *whether* a file changed, but only the content can serve as
|
|
722
|
+
* the merge base when both the template and the user changed it (§7). Passing
|
|
723
|
+
* it replaces any previous snapshot wholesale, so files the template no longer
|
|
724
|
+
* produces don't linger as stale merge bases.
|
|
725
|
+
*/
|
|
726
|
+
declare function writeState(destDir: string, state: TreelayState, decls: Record<string, VariableDecl>, snapshot?: ReadonlyMap<string, Buffer>): void;
|
|
727
|
+
/**
|
|
728
|
+
* Read one file's baseline content — the merge base for `update` (§7).
|
|
729
|
+
*
|
|
730
|
+
* Undefined when the destination predates snapshotting, never had the file, or
|
|
731
|
+
* the snapshot has gone stale. `expectedHash` is what makes the last case safe:
|
|
732
|
+
* any writer that advances `baseline.json` without passing a fresh `snapshot` to
|
|
733
|
+
* {@link writeState} leaves content that no longer matches, and merging against
|
|
734
|
+
* a wrong base corrupts silently. Verifying here turns that into a plain
|
|
735
|
+
* "no base available", which callers surface as a conflict instead.
|
|
736
|
+
*/
|
|
737
|
+
declare function readBaselineFile(destDir: string, rel: string, expectedHash?: string): Buffer | undefined;
|
|
738
|
+
/** Read back persisted state (used by update/status/reflux). */
|
|
739
|
+
declare function readState(destDir: string): TreelayState;
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Compile pipeline — SPEC §6 (the eight steps) and §7 (state).
|
|
743
|
+
*
|
|
744
|
+
* resolve graph → merge var decls → resolve values → eval computed →
|
|
745
|
+
* render each layer → merge rendered layers per-file → drop empty-named
|
|
746
|
+
* files → materialize + persist state.
|
|
747
|
+
*
|
|
748
|
+
* Render-then-merge: a child's op is authored against the parent's *rendered*
|
|
749
|
+
* output, so each layer is rendered before the merge step.
|
|
750
|
+
*/
|
|
751
|
+
|
|
752
|
+
interface CompileOptions {
|
|
753
|
+
destDir: string;
|
|
754
|
+
/** Final variable values (from `resolveValues`). */
|
|
755
|
+
values?: Values;
|
|
756
|
+
/**
|
|
757
|
+
* Persist newly-resolved pins back into the source `treelay.lock` (§3).
|
|
758
|
+
*
|
|
759
|
+
* Defaults to true, following every package manager: the first build after a
|
|
760
|
+
* ref is added records what it resolved to, so the *next* build reproduces it.
|
|
761
|
+
* A frozen resolve never gets here — it fails on the unpinned ref instead.
|
|
762
|
+
*/
|
|
763
|
+
writeLockfile?: boolean;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* One accumulated output file as it builds up across the layer stack. Also the
|
|
767
|
+
* public shape of an in-memory composition (see {@link composeFiles}).
|
|
768
|
+
*/
|
|
769
|
+
interface FileEntry {
|
|
770
|
+
data: Buffer;
|
|
771
|
+
strategy: MergeStrategy;
|
|
772
|
+
fromLayer: string;
|
|
773
|
+
patchedFrom: string[];
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Compose a graph into an in-memory file map, writing nothing.
|
|
777
|
+
*
|
|
778
|
+
* This is the whole pipeline minus materialization, split out because `update`
|
|
779
|
+
* (§7) needs the *would-be* output to merge against a project that already
|
|
780
|
+
* exists — recompiling straight onto disk would destroy the very edits it is
|
|
781
|
+
* trying to preserve.
|
|
782
|
+
*
|
|
783
|
+
* `destDir` is only used to prune a destination nested inside the source tree,
|
|
784
|
+
* so a compose is safe to run with the real destination path.
|
|
785
|
+
*/
|
|
786
|
+
declare function composeFiles(graph: ResolvedGraph, values?: Values, destDir?: string): Promise<Map<string, FileEntry>>;
|
|
787
|
+
/** Provenance + baseline bookkeeping derived from a composition. */
|
|
788
|
+
declare function summarize(acc: ReadonlyMap<string, FileEntry>): {
|
|
789
|
+
result: CompileResult;
|
|
790
|
+
baseline: Record<string, string>;
|
|
791
|
+
manifest: TreelayState["manifest"];
|
|
792
|
+
};
|
|
793
|
+
/** Materialize a resolved graph into a destination directory. */
|
|
794
|
+
declare function compile(graph: ResolvedGraph, options: CompileOptions): Promise<CompileResult>;
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Layer file enumeration & classification — the shared walk beneath both
|
|
798
|
+
* `compile` (§6/§7) and `explain` (§9).
|
|
799
|
+
*
|
|
800
|
+
* Everything that decides *which* files in a layer participate in composition,
|
|
801
|
+
* and *what kind* of contribution each one makes, lives here. Compile turns
|
|
802
|
+
* those entries into bytes; explain turns them into provenance. Keeping the
|
|
803
|
+
* walk in one place is what stops the two from disagreeing about what a layer
|
|
804
|
+
* contains.
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Globs never materialized into the output.
|
|
809
|
+
*
|
|
810
|
+
* `.git` is excluded in **both** forms deliberately (§4, "built-in tombstone"):
|
|
811
|
+
* `**\/.git/**` covers a normal repository directory's contents, while
|
|
812
|
+
* `**\/.git` covers the *gitlink file* that a git **submodule** checkout carries
|
|
813
|
+
* in place of a directory. A layer vendored as a submodule would otherwise
|
|
814
|
+
* publish its gitlink into every compiled tree, producing output that git reads
|
|
815
|
+
* as a broken submodule. `.gitignore`/`.gitmodules` are ordinary files and are
|
|
816
|
+
* intentionally **not** matched by the exact-name `.git` glob.
|
|
817
|
+
*/
|
|
818
|
+
declare const ALWAYS_IGNORE: string[];
|
|
819
|
+
/** What sort of contribution a source file in a layer makes. */
|
|
820
|
+
type EntryKind = "file" | "sidecar" | "suffix";
|
|
821
|
+
/** One classified source file within a layer. */
|
|
822
|
+
interface LayerEntry {
|
|
823
|
+
/** Path of the source file relative to the layer dir. */
|
|
824
|
+
source: string;
|
|
825
|
+
/** The path after stripping the template suffix (may still carry an op suffix). */
|
|
826
|
+
inner: string;
|
|
827
|
+
/** True when the source carried the template suffix (content renders, §6). */
|
|
828
|
+
isTemplate: boolean;
|
|
829
|
+
kind: EntryKind;
|
|
830
|
+
/** For sidecar/suffix entries, the operation being performed. */
|
|
831
|
+
op?: SidecarOpKind;
|
|
832
|
+
/**
|
|
833
|
+
* The output path this entry targets, *before* Liquid path rendering.
|
|
834
|
+
* For ops this is the inherited file the op applies to.
|
|
835
|
+
*/
|
|
836
|
+
rawTarget: string;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Raised when a destination directory is a layer root itself — compiling a
|
|
840
|
+
* layer on top of itself is degenerate, not merely awkward, so it fails loud
|
|
841
|
+
* rather than half-consuming its own output.
|
|
842
|
+
*/
|
|
843
|
+
declare class SelfCompileError extends Error {
|
|
844
|
+
constructor(dir: string);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Extra ignore globs so a destination nested *inside* a layer never feeds its
|
|
848
|
+
* own prior output back into the composition (§7).
|
|
849
|
+
*
|
|
850
|
+
* The autom-lake shape — compiling into a gitignored `build/` inside the source
|
|
851
|
+
* repo — is explicitly supported: a destination that is a strict descendant of
|
|
852
|
+
* a layer is pruned from that layer's walk. A destination that *is* the layer
|
|
853
|
+
* root is rejected via {@link SelfCompileError}.
|
|
854
|
+
*/
|
|
855
|
+
declare function destExclusions(layerDir: string, destDir?: string): string[];
|
|
856
|
+
/**
|
|
857
|
+
* Re-root an output path under a layer's mount point (§3).
|
|
858
|
+
*
|
|
859
|
+
* A mounted layer composes exactly like any other — same strategies, same
|
|
860
|
+
* sidecars — except that every path it produces or operates on is prefixed.
|
|
861
|
+
* Keeping that in one function is what stops `compile` and `explain` from
|
|
862
|
+
* disagreeing about where a mounted file lands.
|
|
863
|
+
*/
|
|
864
|
+
declare function mountTarget(layer: Layer, target: string): string;
|
|
865
|
+
/** List the composable source files of a layer, lowest-level primitive. */
|
|
866
|
+
declare function listLayerFiles(layer: Layer, destDir?: string): string[];
|
|
867
|
+
/**
|
|
868
|
+
* Classify one source path into the contribution it makes.
|
|
869
|
+
*
|
|
870
|
+
* The template suffix is outermost (§6): strip and render first, then the inner
|
|
871
|
+
* name decides whether this is a sidecar, suffix sugar, or a plain file.
|
|
872
|
+
*/
|
|
873
|
+
declare function classifyEntry(source: string, templateSuffix: string): LayerEntry;
|
|
874
|
+
/** Enumerate a layer's classified entries, in stable (sorted) order. */
|
|
875
|
+
declare function enumerateLayer(layer: Layer, destDir?: string): LayerEntry[];
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* `explain` — per-file provenance across the layer stack (SPEC §9, §12 step 7).
|
|
879
|
+
*
|
|
880
|
+
* The debugging story for a system whose whole job is "this file came from
|
|
881
|
+
* somewhere non-obvious": for every output path, which layers contributed, via
|
|
882
|
+
* which strategy, in what precedence order, and which one won.
|
|
883
|
+
*
|
|
884
|
+
* Read-only. It walks the same enumeration as `compile` (see `layer-files.ts`)
|
|
885
|
+
* and mirrors its accumulation bookkeeping — `winner`/`strategy`/`patchedFrom`
|
|
886
|
+
* are defined to match `CompileResult.files[path]` exactly, and a test asserts
|
|
887
|
+
* that equivalence so the two cannot drift apart silently.
|
|
888
|
+
*
|
|
889
|
+
* Unlike compile, explain never throws on a not-yet-implemented strategy: a
|
|
890
|
+
* unified-diff patch is *described* rather than applied, so `explain` stays
|
|
891
|
+
* useful precisely where a build is failing.
|
|
892
|
+
*/
|
|
893
|
+
|
|
894
|
+
/** Why a layer is in the stack (§1, §3). */
|
|
895
|
+
type LayerRole = "parent" | "mixin" | "self" | "mount";
|
|
896
|
+
/** What a contribution did to the accumulating file. */
|
|
897
|
+
type ContributionAction = "create" | "replace" | "deep-merge" | "append" | "prepend" | "delete" | "merge" | "patch";
|
|
898
|
+
/** A layer's position and identity within the resolved stack. */
|
|
899
|
+
interface LayerSummary {
|
|
900
|
+
id: string;
|
|
901
|
+
name: string;
|
|
902
|
+
role: LayerRole;
|
|
903
|
+
/** 1-based position, lowest precedence first. */
|
|
904
|
+
position: number;
|
|
905
|
+
writable: boolean;
|
|
906
|
+
/** Canonical ref this layer was fetched from (§3); absent for local layers. */
|
|
907
|
+
ref?: string;
|
|
908
|
+
/** Exact revision materialized — commit SHA or package version. */
|
|
909
|
+
revision?: string;
|
|
910
|
+
/** Subpath a mounted layer's files are re-rooted under (§3). */
|
|
911
|
+
mountPath?: string;
|
|
912
|
+
}
|
|
913
|
+
/** One layer's contribution to one output path. */
|
|
914
|
+
interface Contribution {
|
|
915
|
+
layer: string;
|
|
916
|
+
name: string;
|
|
917
|
+
role: LayerRole;
|
|
918
|
+
position: number;
|
|
919
|
+
/** Source file within the layer that produced this contribution. */
|
|
920
|
+
source: string;
|
|
921
|
+
action: ContributionAction;
|
|
922
|
+
/** How the file would be combined at this step (§4). */
|
|
923
|
+
strategy: MergeStrategy;
|
|
924
|
+
kind: LayerEntry["kind"];
|
|
925
|
+
/** Recorded base hash from a sidecar, enabling true 3-way merge (§5). */
|
|
926
|
+
base?: string;
|
|
927
|
+
/** True when a `when:` guard evaluated falsy and the op was skipped (§4). */
|
|
928
|
+
skipped?: boolean;
|
|
929
|
+
/** Human note (unsupported strategy, unrendered path, …). */
|
|
930
|
+
note?: string;
|
|
931
|
+
}
|
|
932
|
+
/** The full provenance of one output path. */
|
|
933
|
+
interface FileExplanation {
|
|
934
|
+
path: string;
|
|
935
|
+
/** In precedence order, lowest → highest. */
|
|
936
|
+
contributions: Contribution[];
|
|
937
|
+
/** Whether the file survives to the output (false ⇒ tombstoned). */
|
|
938
|
+
present: boolean;
|
|
939
|
+
/** Layer id whose content won — mirrors `CompileResult.files[path].fromLayer`. */
|
|
940
|
+
winner?: string;
|
|
941
|
+
/** Winning strategy — mirrors `CompileResult.files[path].strategy`. */
|
|
942
|
+
strategy?: MergeStrategy;
|
|
943
|
+
/** Layers whose patches/merges were folded in — mirrors `patchedFrom`. */
|
|
944
|
+
patchedFrom: string[];
|
|
945
|
+
/** True for a destination file with no template origin (user-owned, §7). */
|
|
946
|
+
owned?: boolean;
|
|
947
|
+
}
|
|
948
|
+
/** The whole composition, explained. */
|
|
949
|
+
interface ExplainResult {
|
|
950
|
+
layers: LayerSummary[];
|
|
951
|
+
/** Keyed by output path, insertion-ordered by path. */
|
|
952
|
+
files: Record<string, FileExplanation>;
|
|
953
|
+
}
|
|
954
|
+
interface ExplainOptions {
|
|
955
|
+
/** Variable values used to render templated paths and `when:` guards (§6). */
|
|
956
|
+
values?: Values;
|
|
957
|
+
/** A destination nested inside a layer, pruned from the walk (§7). */
|
|
958
|
+
destDir?: string;
|
|
959
|
+
}
|
|
960
|
+
/** Classify each layer by why it is in the stack. */
|
|
961
|
+
declare function summarizeLayers(graph: ResolvedGraph): LayerSummary[];
|
|
962
|
+
/** Explain every output path produced by a resolved graph. */
|
|
963
|
+
declare function explain(graph: ResolvedGraph, options?: ExplainOptions): Promise<ExplainResult>;
|
|
964
|
+
/** Explain a single output path; undefined when no layer touches it. */
|
|
965
|
+
declare function explainFile(graph: ResolvedGraph, path: string, options?: ExplainOptions): Promise<FileExplanation | undefined>;
|
|
966
|
+
/**
|
|
967
|
+
* Explain a compiled destination (§7).
|
|
968
|
+
*
|
|
969
|
+
* The lockfile's lineage records absolute layer dirs lowest → highest, so its
|
|
970
|
+
* last entry *is* the source root: a destination can reconstruct its own graph
|
|
971
|
+
* and re-explain itself with the answers it was built from. Files the template
|
|
972
|
+
* does not own are reported as user-owned rather than omitted.
|
|
973
|
+
*/
|
|
974
|
+
declare function explainDest(destDir: string): Promise<ExplainResult>;
|
|
975
|
+
/** Render an explanation as aligned, human-readable text. */
|
|
976
|
+
declare function formatExplanation(result: ExplainResult, only?: string): string;
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Upstream drift — has a ref moved since the lock pinned it? (SPEC §3, §9)
|
|
980
|
+
*
|
|
981
|
+
* Drift is *reported*, never acted on. A build whose lock says `abc123` keeps
|
|
982
|
+
* producing `abc123` even after `main` advances; that is the entire point of
|
|
983
|
+
* pinning. What treelay owes the user is that the divergence is visible —
|
|
984
|
+
* `explain` annotates the layer, `update` says so before merging, and
|
|
985
|
+
* `treelay lock --update` is the one command that advances a pin.
|
|
986
|
+
*
|
|
987
|
+
* The probe is best-effort by construction. Being offline, lacking credentials,
|
|
988
|
+
* or having deleted the package makes the current revision *unknown*, which is
|
|
989
|
+
* a distinct answer from "unchanged" and is presented as one.
|
|
990
|
+
*/
|
|
991
|
+
|
|
992
|
+
/** Whether a pinned ref still matches its upstream. */
|
|
993
|
+
type DriftStatus = "in-sync" | "moved" | "unknown" | "immutable";
|
|
994
|
+
/** One ref's drift verdict. */
|
|
995
|
+
interface DriftReport {
|
|
996
|
+
/** Canonical ref (the lockfile key). */
|
|
997
|
+
ref: string;
|
|
998
|
+
/** What was asked for — branch, tag, or semver range. */
|
|
999
|
+
requested: string;
|
|
1000
|
+
/** The revision the lock pins, and what any build here materializes. */
|
|
1001
|
+
locked: string;
|
|
1002
|
+
/** What the ref points at now; undefined when it could not be determined. */
|
|
1003
|
+
current?: string;
|
|
1004
|
+
status: DriftStatus;
|
|
1005
|
+
/** Layers composed from this ref, for a message that names names. */
|
|
1006
|
+
layers: string[];
|
|
1007
|
+
}
|
|
1008
|
+
/** Has anything actually moved? (unknown is not drift — it is ignorance). */
|
|
1009
|
+
declare function hasDrift(reports: readonly DriftReport[]): boolean;
|
|
1010
|
+
/**
|
|
1011
|
+
* Check every pinned ref in a resolved graph against its upstream.
|
|
1012
|
+
*
|
|
1013
|
+
* Contacts the network once per distinct ref, so callers should treat it as an
|
|
1014
|
+
* explicit action rather than something to fold into a hot path.
|
|
1015
|
+
*/
|
|
1016
|
+
declare function checkDrift(graph: ResolvedGraph, fromDir?: string): DriftReport[];
|
|
1017
|
+
/** Human-readable drift summary. Empty string when nothing has moved. */
|
|
1018
|
+
declare function formatDrift(reports: readonly DriftReport[]): string;
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Living-template update — SPEC §7.
|
|
1022
|
+
*
|
|
1023
|
+
* Reloads saved answers (prompting only for newly-introduced variables),
|
|
1024
|
+
* recompiles `theirs`, and three-way merges against the baseline (`base`) and
|
|
1025
|
+
* the working copy (`ours`), then rewrites the baseline.
|
|
1026
|
+
*
|
|
1027
|
+
* The whole resolution is computed before anything is written. `update` touches
|
|
1028
|
+
* a project the user has been working in, so a failure partway through must
|
|
1029
|
+
* leave that project exactly as it was rather than half-updated.
|
|
1030
|
+
*/
|
|
1031
|
+
|
|
1032
|
+
/** How each file was resolved. */
|
|
1033
|
+
type Resolution = "take-theirs" | "keep-ours" | "merged" | "conflict" | "delete"
|
|
1034
|
+
/** All three sides agree — nothing to do. Lets a no-op update say so. */
|
|
1035
|
+
| "unchanged";
|
|
1036
|
+
interface UpdateOptions {
|
|
1037
|
+
/**
|
|
1038
|
+
* How conflicts are surfaced:
|
|
1039
|
+
* - `markers` (default) — write the merged file with diff3 conflict markers.
|
|
1040
|
+
* - `rej` — leave the working file untouched and drop the incoming version
|
|
1041
|
+
* beside it as `<file>.rej`, for projects where a file must stay parseable.
|
|
1042
|
+
*/
|
|
1043
|
+
onConflict?: "markers" | "rej";
|
|
1044
|
+
/** CLI `--set k=v` overrides applied over saved answers. */
|
|
1045
|
+
set?: Record<string, unknown>;
|
|
1046
|
+
/** Prompt for variables the new template version introduced (default true). */
|
|
1047
|
+
prompt?: boolean;
|
|
1048
|
+
/** Refuse to resolve refs `treelay.lock` does not pin (§3). */
|
|
1049
|
+
frozen?: boolean;
|
|
1050
|
+
}
|
|
1051
|
+
interface UpdatePlan {
|
|
1052
|
+
/** Per file: the resolution that update would apply. */
|
|
1053
|
+
files: Record<string, Resolution>;
|
|
1054
|
+
/** Paths that could not be merged cleanly. */
|
|
1055
|
+
conflicts: string[];
|
|
1056
|
+
/** Variables the new template version introduced and now has answers for. */
|
|
1057
|
+
newVariables: string[];
|
|
1058
|
+
/**
|
|
1059
|
+
* Pinned refs whose upstream has moved since `treelay.lock` was written (§3).
|
|
1060
|
+
*
|
|
1061
|
+
* Reported, never followed. An update that silently recomposed at a newer
|
|
1062
|
+
* revision because a branch advanced would make "pull my template's changes
|
|
1063
|
+
* down" mean two different things depending on the day.
|
|
1064
|
+
*/
|
|
1065
|
+
drift: DriftReport[];
|
|
1066
|
+
}
|
|
1067
|
+
/** Dry-run: compute the per-file 3-way resolution without writing (§10). */
|
|
1068
|
+
declare function planUpdate(destDir: string, options?: UpdateOptions): Promise<UpdatePlan>;
|
|
1069
|
+
/** Apply a template update into an existing destination. */
|
|
1070
|
+
declare function update(destDir: string, options?: UpdateOptions): Promise<UpdatePlan>;
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Blast radius — SPEC §8 guard 3.
|
|
1074
|
+
*
|
|
1075
|
+
* "Promoting up reaches *every sibling* inheriting that layer — the intent, but
|
|
1076
|
+
* a footgun." Before a promote lands, report who else is downstream of the
|
|
1077
|
+
* target so the reach is a stated fact rather than a surprise on someone else's
|
|
1078
|
+
* next `update`.
|
|
1079
|
+
*
|
|
1080
|
+
* Two populations, found two different ways:
|
|
1081
|
+
*
|
|
1082
|
+
* - **dependents** — other *layers* declaring the target as a parent or mixin.
|
|
1083
|
+
* Found by reading manifests. These inherit the change by composition.
|
|
1084
|
+
* - **destinations** — already-compiled *projects* whose lockfile lineage
|
|
1085
|
+
* includes the target. Found by reading `.treelay/lock.json`. These are live
|
|
1086
|
+
* deployments that will absorb the change when they next update.
|
|
1087
|
+
*
|
|
1088
|
+
* The search is bounded: reflux passes a root (normally the common ancestor of
|
|
1089
|
+
* the composition) rather than letting this wander the filesystem.
|
|
1090
|
+
*/
|
|
1091
|
+
interface BlastRadius {
|
|
1092
|
+
/** The layer being promoted into. */
|
|
1093
|
+
layer: string;
|
|
1094
|
+
searchRoot: string;
|
|
1095
|
+
/** Other layer dirs that declare the target as a parent or mixin. */
|
|
1096
|
+
dependents: string[];
|
|
1097
|
+
/** Compiled destination dirs whose lineage includes the target. */
|
|
1098
|
+
destinations: string[];
|
|
1099
|
+
/**
|
|
1100
|
+
* True when the scan may be incomplete — consumers can live outside the
|
|
1101
|
+
* search root, so "none found" is never proof of "none exist".
|
|
1102
|
+
*/
|
|
1103
|
+
bounded: boolean;
|
|
1104
|
+
}
|
|
1105
|
+
interface BlastRadiusOptions {
|
|
1106
|
+
/** Directory to scan. Consumers outside it are invisible to this scan. */
|
|
1107
|
+
searchRoot: string;
|
|
1108
|
+
/** Destination doing the promoting; excluded from `destinations`. */
|
|
1109
|
+
excludeDest?: string;
|
|
1110
|
+
/** Layer doing the promoting; excluded from `dependents`. */
|
|
1111
|
+
excludeLayer?: string;
|
|
1112
|
+
/** Directory depth to scan. Deep enough for a monorepo, bounded for sanity. */
|
|
1113
|
+
depth?: number;
|
|
1114
|
+
}
|
|
1115
|
+
/** Find every layer and compiled destination downstream of `layerDir`. */
|
|
1116
|
+
declare function blastRadius(layerDir: string, options: BlastRadiusOptions): BlastRadius;
|
|
1117
|
+
/** A one-line warning in the shape SPEC §8 guard 3 asks for. */
|
|
1118
|
+
declare function describeBlastRadius(radius: BlastRadius, layerName: string): string;
|
|
1119
|
+
/** The deepest directory containing every given path — a natural search root. */
|
|
1120
|
+
declare function commonAncestor(paths: string[]): string;
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Reflux — pushing instance edits back up the graph — SPEC §8.
|
|
1124
|
+
*
|
|
1125
|
+
* `compile` is instantiation; reflux is "pull member up". `status` lists what
|
|
1126
|
+
* diverged from the baseline and blames the layer that produced it, `promote`
|
|
1127
|
+
* pushes a change into an existing ancestor, and `extract` factors it into a
|
|
1128
|
+
* brand-new overlay.
|
|
1129
|
+
*
|
|
1130
|
+
* Every write-back runs the three guards of §8:
|
|
1131
|
+
*
|
|
1132
|
+
* 1. **Precedence shadowing** — refuse a target a higher layer would override,
|
|
1133
|
+
* because the change would silently vanish on the next recompile.
|
|
1134
|
+
* 2. **Round-trip verification** — recompile and assert the destination
|
|
1135
|
+
* reproduces byte-for-byte, rolling back the layer writes if it doesn't.
|
|
1136
|
+
* 3. **Blast radius** — report which other layers and compiled destinations
|
|
1137
|
+
* the target feeds before the change reaches them.
|
|
1138
|
+
*
|
|
1139
|
+
* Guards 1 and 2 divide the work: guard 1 catches the cases that provably
|
|
1140
|
+
* cannot work (a wholesale override sits above the target) with a precise,
|
|
1141
|
+
* actionable message; guard 2 catches everything subtler — partial merges,
|
|
1142
|
+
* append ordering, re-render losses — by simply checking whether the graph
|
|
1143
|
+
* reproduces reality.
|
|
1144
|
+
*/
|
|
1145
|
+
|
|
1146
|
+
/** List changes in a destination vs its baseline, with provenance (§8). */
|
|
1147
|
+
declare function status(destDir: string): Promise<Change[]>;
|
|
1148
|
+
/** Render `status` output in the annotated form of SPEC §8. */
|
|
1149
|
+
declare function formatStatus(changes: Change[], graph: ResolvedGraph): string;
|
|
1150
|
+
/** How a promoted change was written into its target layer. */
|
|
1151
|
+
type LandingMode = "rewrite" | "patch" | "create" | "tombstone";
|
|
1152
|
+
interface LandedChange {
|
|
1153
|
+
path: string;
|
|
1154
|
+
mode: LandingMode;
|
|
1155
|
+
/** File written inside the target layer, relative to the layer root. */
|
|
1156
|
+
wrote: string;
|
|
1157
|
+
}
|
|
1158
|
+
interface PromoteResult {
|
|
1159
|
+
/** Layer id promoted into. */
|
|
1160
|
+
target: string;
|
|
1161
|
+
targetName: string;
|
|
1162
|
+
landed: LandedChange[];
|
|
1163
|
+
verified: boolean;
|
|
1164
|
+
blastRadius: BlastRadius;
|
|
1165
|
+
/** Human-readable §8 guard 3 warning. */
|
|
1166
|
+
blastRadiusWarning: string;
|
|
1167
|
+
}
|
|
1168
|
+
interface PromoteOptions {
|
|
1169
|
+
/** Target layer to promote into; if omitted, auto-suggest from provenance. */
|
|
1170
|
+
to?: LayerRef;
|
|
1171
|
+
/** Recompile and assert byte-identity after promoting (default true). */
|
|
1172
|
+
verify?: boolean;
|
|
1173
|
+
/** Root for the blast-radius scan (default: common ancestor of the graph). */
|
|
1174
|
+
searchRoot?: string;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Promote changes up into a target layer. Throws on precedence-shadowing,
|
|
1178
|
+
* read-only targets, or a failed round-trip verification (§8 guards). Generates
|
|
1179
|
+
* a `.treelay` sidecar recording both `base` and `baseContent` when the change
|
|
1180
|
+
* lands as a patch rather than a rewrite (§5).
|
|
1181
|
+
*/
|
|
1182
|
+
declare function promote(destDir: string, changes: Change[], options?: PromoteOptions): Promise<PromoteResult>;
|
|
1183
|
+
interface ExtractOptions {
|
|
1184
|
+
/** Path for the new overlay layer. */
|
|
1185
|
+
as: string;
|
|
1186
|
+
/** Wire it into the leaf's `mixins` (vs leaving it free-standing). */
|
|
1187
|
+
asMixin?: boolean;
|
|
1188
|
+
/** Name for the new layer's manifest (default: its directory name). */
|
|
1189
|
+
name?: string;
|
|
1190
|
+
/** Recompile and assert byte-identity afterwards (default true when wired). */
|
|
1191
|
+
verify?: boolean;
|
|
1192
|
+
}
|
|
1193
|
+
interface ExtractResult {
|
|
1194
|
+
/** Absolute path of the new layer. */
|
|
1195
|
+
layer: string;
|
|
1196
|
+
name: string;
|
|
1197
|
+
files: string[];
|
|
1198
|
+
/** Whether the leaf was rewired to consume the new layer. */
|
|
1199
|
+
wired: boolean;
|
|
1200
|
+
verified: boolean;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Capture changes as a new overlay layer (§8).
|
|
1204
|
+
*
|
|
1205
|
+
* A free-standing extraction (`asMixin: false`) deliberately skips round-trip
|
|
1206
|
+
* verification and leaves the baseline alone: the new layer is not in the graph
|
|
1207
|
+
* yet, so the destination provably *cannot* reproduce from it. Verifying would
|
|
1208
|
+
* fail by construction, and rewriting the baseline would falsely mark the edits
|
|
1209
|
+
* as inherited.
|
|
1210
|
+
*/
|
|
1211
|
+
declare function extract(destDir: string, changes: Change[], options: ExtractOptions): Promise<ExtractResult>;
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Round-trip verification — SPEC §8 guard 2.
|
|
1215
|
+
*
|
|
1216
|
+
* "After any promote/extract, recompile and assert the working copy is
|
|
1217
|
+
* byte-identical." This is the guard that makes reflux trustworthy: a promoted
|
|
1218
|
+
* edit is only accepted once the graph provably reproduces it, so merge-order
|
|
1219
|
+
* interactions surface as a loud failure instead of silent drift.
|
|
1220
|
+
*
|
|
1221
|
+
* The comparison is deliberately one-directional. Every path the template
|
|
1222
|
+
* produces must match the destination exactly; files the destination has but
|
|
1223
|
+
* the template does not are *user-owned* (§7) and are none of verification's
|
|
1224
|
+
* business. Without that asymmetry every unpromoted local file would read as a
|
|
1225
|
+
* failure.
|
|
1226
|
+
*/
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Compose a graph and return its output in memory, without touching any real
|
|
1230
|
+
* destination — verification must not be able to disturb what it is checking.
|
|
1231
|
+
*/
|
|
1232
|
+
declare function composeToMemory(graph: ResolvedGraph, values: Values, destDir?: string): Promise<Map<string, Buffer>>;
|
|
1233
|
+
type MismatchKind = "content-differs" | "missing-in-dest" | "should-have-been-removed";
|
|
1234
|
+
interface VerifyMismatch {
|
|
1235
|
+
path: string;
|
|
1236
|
+
kind: MismatchKind;
|
|
1237
|
+
detail: string;
|
|
1238
|
+
}
|
|
1239
|
+
interface VerifyResult {
|
|
1240
|
+
ok: boolean;
|
|
1241
|
+
mismatches: VerifyMismatch[];
|
|
1242
|
+
/** The recomposed output, reusable as the new baseline on success. */
|
|
1243
|
+
composed: Map<string, Buffer>;
|
|
1244
|
+
}
|
|
1245
|
+
interface VerifyOptions {
|
|
1246
|
+
/**
|
|
1247
|
+
* Paths a promotion intended to remove. Verification asserts the recompiled
|
|
1248
|
+
* template no longer produces them — otherwise a promoted tombstone that
|
|
1249
|
+
* failed to take would pass unnoticed, since the file is already gone from
|
|
1250
|
+
* the destination either way.
|
|
1251
|
+
*/
|
|
1252
|
+
expectAbsent?: string[];
|
|
1253
|
+
}
|
|
1254
|
+
/** Recompile `graph` and assert `destDir` reproduces it byte-for-byte. */
|
|
1255
|
+
declare function roundTripVerify(destDir: string, graph: ResolvedGraph, values: Values, options?: VerifyOptions): Promise<VerifyResult>;
|
|
1256
|
+
/** Render mismatches into a message suitable for a thrown error. */
|
|
1257
|
+
declare function describeMismatches(mismatches: VerifyMismatch[]): string;
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Git-backed layers (SPEC §3).
|
|
1261
|
+
*
|
|
1262
|
+
* Two operations, deliberately separated because they have very different
|
|
1263
|
+
* costs and failure modes:
|
|
1264
|
+
*
|
|
1265
|
+
* - **resolve** — what commit does `#main` mean *right now*? Needs the remote.
|
|
1266
|
+
* - **materialize** — give me the tree at commit `abc123`. Needs only the cache
|
|
1267
|
+
* once that commit has been fetched, and is what a locked build does.
|
|
1268
|
+
*
|
|
1269
|
+
* That split is the whole of "compile honours the lock": a locked build never
|
|
1270
|
+
* asks the first question, so a moved branch cannot change what it produces.
|
|
1271
|
+
*
|
|
1272
|
+
* Everything shells out to the user's `git`. Reimplementing the wire protocol
|
|
1273
|
+
* to save a process spawn would trade a well-tested dependency every developer
|
|
1274
|
+
* already has for a novel one that has to learn about proxies, credential
|
|
1275
|
+
* helpers, SSH agents and partial clones.
|
|
1276
|
+
*/
|
|
1277
|
+
|
|
1278
|
+
/** Raised when a git operation fails, carrying git's own message. */
|
|
1279
|
+
declare class GitFetchError extends Error {
|
|
1280
|
+
readonly url: string;
|
|
1281
|
+
constructor(url: string, message: string);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* npm-backed layers (SPEC §3).
|
|
1286
|
+
*
|
|
1287
|
+
* An overlay *is* a normal npm package (§2), so treelay resolves npm refs the
|
|
1288
|
+
* way any Node program resolves a dependency: through the installed
|
|
1289
|
+
* `node_modules` tree, starting at the layer that declared the ref.
|
|
1290
|
+
*
|
|
1291
|
+
* **treelay deliberately does not install anything.** Package installation is
|
|
1292
|
+
* already owned by a tool with a lockfile, an integrity model, a registry
|
|
1293
|
+
* configuration and an offline cache; shipping a second, worse copy of that
|
|
1294
|
+
* inside a composition engine would mean two lockfiles disagreeing about the
|
|
1295
|
+
* same tree. treelay's job is to record *which* version composition consumed,
|
|
1296
|
+
* and to fail clearly when the package is not installed at all.
|
|
1297
|
+
*
|
|
1298
|
+
* The consequence worth naming: an npm layer's exact version is whatever the
|
|
1299
|
+
* package manager put on disk. `treelay.lock` pins what was used and
|
|
1300
|
+
* {@link liveNpmVersion} reports when the installed tree has since moved, but
|
|
1301
|
+
* reproducing an old pin is `npm ci`'s job, not treelay's.
|
|
1302
|
+
*/
|
|
1303
|
+
|
|
1304
|
+
/** Raised when an npm layer cannot be resolved from the installed tree. */
|
|
1305
|
+
declare class NpmResolveError extends Error {
|
|
1306
|
+
readonly packageName: string;
|
|
1307
|
+
constructor(packageName: string, message: string);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Content-addressed cache for fetched layers (SPEC §3).
|
|
1312
|
+
*
|
|
1313
|
+
* Every fetched tree lands at a path derived from *what it is*, never from who
|
|
1314
|
+
* asked for it: `<root>/git/<repo-key>/rev/<commit>/`. Two layers pinning the
|
|
1315
|
+
* same commit share one checkout, a held-back pin coexists with a floating one
|
|
1316
|
+
* instead of fighting over a working directory, and the cache is safe to delete
|
|
1317
|
+
* at any time — worst case the next resolve re-fetches.
|
|
1318
|
+
*
|
|
1319
|
+
* `TREELAY_CACHE_DIR` overrides the location, which is how tests stay hermetic.
|
|
1320
|
+
*/
|
|
1321
|
+
/** Root of the cache: `$TREELAY_CACHE_DIR`, else `~/.cache/treelay`. */
|
|
1322
|
+
declare function cacheRoot(override?: string): string;
|
|
1323
|
+
/**
|
|
1324
|
+
* A filesystem-safe, collision-resistant key for a remote location.
|
|
1325
|
+
*
|
|
1326
|
+
* The readable slug is there for humans poking at the cache; the hash suffix is
|
|
1327
|
+
* what actually guarantees uniqueness, since sanitizing a URL is lossy.
|
|
1328
|
+
*/
|
|
1329
|
+
declare function locationKey(location: string): string;
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Fetching a remote layer, lock-aware (SPEC §3).
|
|
1333
|
+
*
|
|
1334
|
+
* One entry point sits between resolution and the per-transport fetchers, and
|
|
1335
|
+
* it is where the lockfile earns its keep:
|
|
1336
|
+
*
|
|
1337
|
+
* - **locked** — the lock already pins this ref → materialize *that* revision.
|
|
1338
|
+
* No remote query, so a branch that has since moved cannot change the build.
|
|
1339
|
+
* - **unlocked** — resolve the ref live, materialize, and hand back an entry for
|
|
1340
|
+
* the caller to record.
|
|
1341
|
+
* - **frozen** — an unlocked ref is an error, not a silent lock update. This is
|
|
1342
|
+
* the CI posture: builds reproduce or they fail.
|
|
1343
|
+
*
|
|
1344
|
+
* Everything here is synchronous by design. Layer resolution is called from
|
|
1345
|
+
* `resolve()`, which the entire library and CLI treat as a plain function; making
|
|
1346
|
+
* it async to accommodate a subprocess would ripple through every caller for no
|
|
1347
|
+
* behavioural gain, since a build cannot proceed without its layers anyway.
|
|
1348
|
+
*/
|
|
1349
|
+
|
|
1350
|
+
/** Raised when a frozen resolve meets a ref the lockfile does not pin. */
|
|
1351
|
+
declare class LockMissingError extends Error {
|
|
1352
|
+
readonly ref: string;
|
|
1353
|
+
constructor(ref: string);
|
|
1354
|
+
}
|
|
1355
|
+
/** Raised when cached content no longer matches the integrity the lock records. */
|
|
1356
|
+
declare class IntegrityError extends Error {
|
|
1357
|
+
constructor(ref: string, expected: string, actual: string);
|
|
1358
|
+
}
|
|
1359
|
+
interface FetchOptions {
|
|
1360
|
+
/** Directory the ref was declared in — the anchor for npm resolution. */
|
|
1361
|
+
fromDir: string;
|
|
1362
|
+
/** Existing pins; consulted before any remote is contacted. */
|
|
1363
|
+
lock: TreelayLock;
|
|
1364
|
+
/** Refuse to resolve refs the lock does not already pin (CI). */
|
|
1365
|
+
frozen?: boolean;
|
|
1366
|
+
/** Re-resolve moving refs to their current revision, advancing the lock. */
|
|
1367
|
+
updateRefs?: boolean;
|
|
1368
|
+
/** Override the cache root (tests). */
|
|
1369
|
+
cacheDir?: string;
|
|
1370
|
+
}
|
|
1371
|
+
/** A materialized remote layer plus the lock entry describing it. */
|
|
1372
|
+
interface FetchedLayer {
|
|
1373
|
+
/** Canonical ref — the lockfile key. */
|
|
1374
|
+
ref: string;
|
|
1375
|
+
/** Absolute path of the layer root on disk. */
|
|
1376
|
+
dir: string;
|
|
1377
|
+
revision: string;
|
|
1378
|
+
integrity: string;
|
|
1379
|
+
entry: LockEntry;
|
|
1380
|
+
/** True when the lock gained or changed an entry because of this fetch. */
|
|
1381
|
+
changed: boolean;
|
|
1382
|
+
}
|
|
1383
|
+
/** Fetch (or reuse) a remote layer, honouring the lock. */
|
|
1384
|
+
declare function fetchLayer(ref: RemoteRef, options: FetchOptions): FetchedLayer;
|
|
1385
|
+
/**
|
|
1386
|
+
* Re-hash a materialized layer. Used by `treelay lock --check` to notice a
|
|
1387
|
+
* cache that has been edited in place without re-fetching anything.
|
|
1388
|
+
*/
|
|
1389
|
+
declare function verifyIntegrity(dir: string, expected: string): boolean;
|
|
1390
|
+
/**
|
|
1391
|
+
* What a ref points at on the remote right now — the drift probe.
|
|
1392
|
+
*
|
|
1393
|
+
* Undefined means "could not tell" (offline, no credentials, package not
|
|
1394
|
+
* installed), never "unchanged": an advisory check that silently reports
|
|
1395
|
+
* in-sync when it could not look would be worse than saying nothing.
|
|
1396
|
+
*/
|
|
1397
|
+
declare function liveRevision(ref: RemoteRef, fromDir: string): string | undefined;
|
|
1398
|
+
|
|
1399
|
+
export { ALWAYS_IGNORE, type ArrayPolicy, type BlastRadius, type BlastRadiusOptions, type Change, type ChangeKind, type CompileResult, CycleError, type DriftReport, type DriftStatus, type ExtractOptions, type ExtractResult, type FetchOptions, type FetchedLayer, type FileEntry, type FileProvenance, GitFetchError, type GitRef, InconsistentHierarchyError, IntegrityError, InvalidRefError, LOCKFILE_NAME, LOCKFILE_VERSION, type LandedChange, type LandingMode, type Layer, type LayerOrigin, type LayerRef, type LocalRef, type LockEntry, type LockFile, LockMissingError, LockfileError, MERGE_LABELS, type Manifest, MergeConflictError, type MergeStrategy, type MismatchKind, MountError, NotImplementedError, type NpmRef, NpmResolveError, type ParsedRef, type Patch3WayArgs, type PromoteOptions, type PromoteResult, type RefKind, type RemoteRef, type RemoteRefKind, type RenderMode, type Resolution, type ResolveOptions, type ResolvedGraph, STATE_DIR, SelfCompileError, type SidecarOp, type SidecarOpKind, type TextMerge3Args, type TextMerge3Result, type TreelayLock, type TreelayState, type UpdateOptions, type UpdatePlan, type Values, type VariableDecl, type VariableType, type VerifyMismatch, type VerifyResult, applyJsonPatch, applyMergePatch, applyPatch3Way, blastRadius, c3Linearize, cacheRoot, canonicalRef, checkDrift, classifyEntry, commonAncestor, compile, composeFiles, composeToMemory, createEngine, deepMerge, defaultStrategy, describeBlastRadius, describeMismatches, destExclusions, desugarSuffix, emptyLock, enumerateLayer, explain, explainDest, explainFile, extract, fetchLayer, formatDrift, formatExplanation, formatStatus, hasDrift, hasState, hashContent, hashTree, isCommitSha, isLocalRef, isSidecar, listLayerFiles, liveRevision, loadManifest, locationKey, lockfilePath, locksEqual, matchesHash, mergeStructured3, mergeText3, mergeVariableDecls, mountTarget, parseRef, parseSidecar, pinnedRef, planUpdate, promote, readBaselineFile, readLock, readState, renderString, resolve, resolveRef, resolveValues, roundTripVerify, serializeLock, sidecarTarget, sourceOf, statePaths, status, strategyFor, summarize, summarizeLayers, templateTarget, update, verifyIntegrity, writeLock, writeState };
|