@johnhenry/sitefile 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +120 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +119 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
@@ -0,0 +1,372 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { SitefileError } from './errors.js';
4
+ import { defaultOutName } from './export-formats.js';
5
+ import { EXPORT_FORMATS, parseExportFormat } from './language/ast.js';
6
+ import { parseYaml } from './yaml.js';
7
+ /**
8
+ * `sitefile bake`: build several variants of a site from one bake file
9
+ * (`sitefile.bake.yaml`, `.yml` or `.json`), like `docker buildx bake`.
10
+ *
11
+ * The bake file names targets (each one a `sitefile build`: context, Sitefile,
12
+ * build args, arg files, `--target` stage, output, format, tags), optional
13
+ * `matrix` values that expand one target into several, `inherits` and groups.
14
+ * This module only reads and resolves the file into a {@link BakePlan}; the CLI
15
+ * (src/cli/bake.ts) runs the builds. No IO besides reading the bake file.
16
+ */
17
+ /** The default bake file names, looked up in this order in the current directory. */
18
+ export const BAKE_FILES = ['sitefile.bake.yaml', 'sitefile.bake.yml', 'sitefile.bake.json'];
19
+ const NAME_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
20
+ const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
21
+ const TARGET_KEYS = ['context', 'file', 'args', 'arg-files', 'target', 'output', 'format', 'tags', 'matrix', 'inherits'];
22
+ function fail(label, detail) {
23
+ throw new SitefileError('E_DATA', `${label}: ${detail}`);
24
+ }
25
+ const isMapping = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
26
+ const isScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
27
+ /** Find the bake file: `file` when given, else the first of {@link BAKE_FILES} in the current directory. */
28
+ export async function findBakeFile(file) {
29
+ if (file !== undefined)
30
+ return file;
31
+ for (const name of BAKE_FILES) {
32
+ const found = await readFile(name).then(() => true, () => false);
33
+ if (found)
34
+ return name;
35
+ }
36
+ throw new SitefileError('E_NOT_FOUND', `no bake file: ${BAKE_FILES.join(', ')} not found in the current directory (or pass -f)`);
37
+ }
38
+ /** Read and parse a bake file into its definition (no resolution yet). */
39
+ export async function loadBakeFile(file) {
40
+ const base = path.basename(file).toLowerCase();
41
+ const json = base.endsWith('.json');
42
+ if (!json && !base.endsWith('.yaml') && !base.endsWith('.yml')) {
43
+ throw new SitefileError('E_USAGE', `bake file '${file}': unknown format (use a .yaml, .yml or .json file)`);
44
+ }
45
+ let text;
46
+ try {
47
+ text = await readFile(file, 'utf8');
48
+ }
49
+ catch (e) {
50
+ throw new SitefileError('E_NOT_FOUND', `cannot read bake file '${file}'`, undefined, { cause: e });
51
+ }
52
+ if (!json)
53
+ return parseYaml(text, file);
54
+ try {
55
+ return JSON.parse(text.replace(/^/, ''));
56
+ }
57
+ catch (e) {
58
+ throw new SitefileError('E_DATA', `${file} is not valid JSON: ${e.message}`, undefined, { cause: e });
59
+ }
60
+ }
61
+ function stringField(label, where, key, v) {
62
+ if (typeof v !== 'string')
63
+ fail(label, `${where}: ${key} must be a string`);
64
+ return v;
65
+ }
66
+ function stringList(label, where, key, v) {
67
+ if (!Array.isArray(v) || !v.every((x) => typeof x === 'string'))
68
+ fail(label, `${where}: ${key} must be a list of strings`);
69
+ return v;
70
+ }
71
+ function parseTarget(label, name, v) {
72
+ const where = `target '${name}'`;
73
+ if (v === null)
74
+ return { inherits: [] };
75
+ if (!isMapping(v))
76
+ fail(label, `${where} must be a mapping`);
77
+ const t = { inherits: [] };
78
+ for (const [key, value] of Object.entries(v)) {
79
+ switch (key) {
80
+ case 'context':
81
+ case 'file':
82
+ case 'target':
83
+ case 'output':
84
+ case 'format':
85
+ t[key] = stringField(label, where, key, value);
86
+ break;
87
+ case 'arg-files':
88
+ case 'tags':
89
+ t[key] = stringList(label, where, key, value);
90
+ break;
91
+ case 'inherits':
92
+ t.inherits = typeof value === 'string' ? [value] : stringList(label, where, key, value);
93
+ break;
94
+ case 'args': {
95
+ if (!isMapping(value))
96
+ fail(label, `${where}: args must be a mapping of NAME: value`);
97
+ const args = {};
98
+ for (const [k, a] of Object.entries(value)) {
99
+ if (!KEY_RE.test(k))
100
+ fail(label, `${where}: '${k}' is not a valid ARG name`);
101
+ if (!isScalar(a))
102
+ fail(label, `${where}: args.${k} must be a string, number or boolean`);
103
+ args[k] = String(a);
104
+ }
105
+ t.args = args;
106
+ break;
107
+ }
108
+ case 'matrix': {
109
+ if (!isMapping(value) || Object.keys(value).length === 0)
110
+ fail(label, `${where}: matrix must be a mapping of name: [values]`);
111
+ const m = [];
112
+ for (const [k, list] of Object.entries(value)) {
113
+ if (!KEY_RE.test(k))
114
+ fail(label, `${where}: '${k}' is not a valid matrix variable name`);
115
+ if (!Array.isArray(list) || list.length === 0 || !list.every(isScalar)) {
116
+ fail(label, `${where}: matrix.${k} must be a non-empty list of strings, numbers or booleans`);
117
+ }
118
+ m.push([k, list.map((x) => String(x))]);
119
+ }
120
+ t.matrix = m;
121
+ break;
122
+ }
123
+ default:
124
+ fail(label, `${where}: unknown key '${key}' (expected ${TARGET_KEYS.join(', ')})`);
125
+ }
126
+ }
127
+ return t;
128
+ }
129
+ /** Check the shape of a parsed bake file. */
130
+ export function parseBakeDefinition(value, label) {
131
+ if (!isMapping(value))
132
+ fail(label, 'a bake file must be a mapping with targets (and optional groups)');
133
+ for (const key of Object.keys(value)) {
134
+ if (key !== 'targets' && key !== 'groups')
135
+ fail(label, `unknown key '${key}' (expected targets, groups)`);
136
+ }
137
+ const rawTargets = value['targets'];
138
+ if (!isMapping(rawTargets) || Object.keys(rawTargets).length === 0)
139
+ fail(label, 'targets must be a non-empty mapping of name: target');
140
+ const targets = new Map();
141
+ for (const [name, t] of Object.entries(rawTargets)) {
142
+ if (!NAME_RE.test(name))
143
+ fail(label, `'${name}' is not a valid target name (letters, digits, '.', '_', '-')`);
144
+ targets.set(name, parseTarget(label, name, t));
145
+ }
146
+ const groups = new Map();
147
+ const rawGroups = value['groups'];
148
+ if (rawGroups !== undefined && rawGroups !== null) {
149
+ if (!isMapping(rawGroups))
150
+ fail(label, 'groups must be a mapping of name: [targets]');
151
+ for (const [name, list] of Object.entries(rawGroups)) {
152
+ if (!NAME_RE.test(name))
153
+ fail(label, `'${name}' is not a valid group name (letters, digits, '.', '_', '-')`);
154
+ if (targets.has(name))
155
+ fail(label, `'${name}' is both a target and a group`);
156
+ groups.set(name, stringList(label, `group '${name}'`, 'the group', list));
157
+ }
158
+ }
159
+ return { label, targets, groups };
160
+ }
161
+ function merge(into, from) {
162
+ for (const key of ['context', 'file', 'target', 'output', 'format', 'arg-files', 'tags']) {
163
+ if (from[key] !== undefined)
164
+ into[key] = from[key];
165
+ }
166
+ if (from.args)
167
+ into.args = Object.assign(into.args ?? {}, from.args);
168
+ }
169
+ /** A target's fields with its `inherits` applied (parents in order, then its own fields). */
170
+ function inherited(def, name, stack) {
171
+ const t = def.targets.get(name);
172
+ const out = {};
173
+ for (const parent of t.inherits) {
174
+ if (!def.targets.has(parent))
175
+ fail(def.label, `target '${name}' inherits unknown target '${parent}'`);
176
+ if (parent === name || stack.includes(parent))
177
+ fail(def.label, `inherits cycle: ${[...stack, name, parent].join(' -> ')}`);
178
+ merge(out, inherited(def, parent, [...stack, name]));
179
+ }
180
+ merge(out, t); // matrix and inherits are not inherited
181
+ if (out.args)
182
+ out.args = { ...out.args };
183
+ return out;
184
+ }
185
+ /** Every combination of the matrix values, the first variable varying slowest. */
186
+ function combinations(matrix) {
187
+ let out = [[]];
188
+ for (const [k, values] of matrix)
189
+ out = out.flatMap((c) => values.map((v) => [...c, [k, v]]));
190
+ return out;
191
+ }
192
+ function interpolate(def, name, field, s, vars) {
193
+ return s.replace(/\$\{([^}]*)\}/g, (whole, v) => {
194
+ const value = vars.get(v);
195
+ if (value === undefined)
196
+ fail(def.label, `target '${name}': ${field}: '${whole}' is not a matrix variable (${[...vars.keys()].join(', ')})`);
197
+ return value;
198
+ });
199
+ }
200
+ const joinTo = (dir, p) => (path.isAbsolute(p) ? path.normalize(p) : path.join(dir, p));
201
+ /** Expand and resolve every target of the definition, in file order (matrix targets in place). */
202
+ function expand(def, bakeDir) {
203
+ const out = new Map();
204
+ const seen = new Map();
205
+ for (const [base, raw] of def.targets) {
206
+ const fields = inherited(def, base, []);
207
+ const combos = raw.matrix ? combinations(raw.matrix) : [[]];
208
+ const list = [];
209
+ for (const combo of combos) {
210
+ const name = [base, ...combo.map(([, v]) => v.replace(/[^A-Za-z0-9._-]/g, '_'))].join('-');
211
+ const prior = seen.get(name);
212
+ if (prior !== undefined)
213
+ fail(def.label, `two targets are named '${name}' (from '${prior}' and '${base}')`);
214
+ seen.set(name, base);
215
+ const vars = new Map(combo);
216
+ const sub = (field, s) => (raw.matrix ? interpolate(def, name, field, s, vars) : s);
217
+ const format = fields.format !== undefined ? sub('format', fields.format) : null;
218
+ if (format !== null && !parseExportFormat(format)) {
219
+ fail(def.label, `target '${name}': format must be one of ${EXPORT_FORMATS.join(', ')} (or tar.gz), got '${format}'`);
220
+ }
221
+ const context = joinTo(bakeDir, sub('context', fields.context ?? '.'));
222
+ const args = {};
223
+ for (const [k, v] of Object.entries(fields.args ?? {}))
224
+ args[k] = sub(`args.${k}`, v);
225
+ const defaultOut = path.join('dist', name + defaultOutName(format !== null ? parseExportFormat(format) : 'directory').slice('dist'.length));
226
+ list.push({
227
+ name,
228
+ context,
229
+ file: fields.file !== undefined ? joinTo(bakeDir, sub('file', fields.file)) : path.join(context, 'Sitefile'),
230
+ args,
231
+ argFiles: (fields['arg-files'] ?? []).map((f) => joinTo(bakeDir, sub('arg-files', f))),
232
+ target: fields.target !== undefined ? sub('target', fields.target) : null,
233
+ output: joinTo(bakeDir, fields.output !== undefined ? sub('output', fields.output) : defaultOut),
234
+ format,
235
+ tags: (fields.tags ?? []).map((t) => sub('tags', t)),
236
+ matrix: raw.matrix ? Object.fromEntries(combo) : null,
237
+ });
238
+ }
239
+ out.set(base, list);
240
+ }
241
+ return out;
242
+ }
243
+ /**
244
+ * Resolve a bake definition into the targets to build: `selected` names targets
245
+ * (a matrix target's name selects all its variants; a variant's own name selects
246
+ * it) and groups, in order, without duplicates. With none, the `default` group if
247
+ * there is one, else every target in file order. Unknown names are usage errors;
248
+ * targets whose outputs overlap, or that share a tag, are E_CONFLICT.
249
+ */
250
+ export function resolveBake(def, file, selected) {
251
+ const bakeDir = path.dirname(file);
252
+ const all = expand(def, bakeDir);
253
+ const byName = new Map();
254
+ for (const list of all.values())
255
+ for (const t of list)
256
+ byName.set(t.name, t);
257
+ const picked = [];
258
+ const add = (t) => {
259
+ if (!picked.includes(t))
260
+ picked.push(t);
261
+ };
262
+ const visit = (name, stack, fromGroup) => {
263
+ const group = def.groups.get(name);
264
+ if (group) {
265
+ if (stack.includes(name))
266
+ fail(def.label, `group '${name}' includes itself (${[...stack, name].join(' -> ')})`);
267
+ for (const member of group)
268
+ visit(member, [...stack, name], name);
269
+ return;
270
+ }
271
+ const list = all.get(name);
272
+ if (list)
273
+ return list.forEach(add);
274
+ const one = byName.get(name);
275
+ if (one)
276
+ return add(one);
277
+ if (fromGroup !== undefined)
278
+ fail(def.label, `group '${fromGroup}': no target or group named '${name}'`);
279
+ const groups = [...def.groups.keys()];
280
+ throw new SitefileError('E_USAGE', `bake: no target or group named '${name}' (targets: ${[...def.targets.keys()].join(', ')}${groups.length ? `; groups: ${groups.join(', ')}` : ''})`);
281
+ };
282
+ if (selected.length)
283
+ for (const n of selected)
284
+ visit(n, [], undefined);
285
+ else if (def.groups.has('default'))
286
+ visit('default', [], undefined);
287
+ else
288
+ for (const list of all.values())
289
+ list.forEach(add);
290
+ checkConflicts(picked);
291
+ return { file, targets: picked };
292
+ }
293
+ function checkConflicts(targets) {
294
+ const abs = targets.map((t) => path.resolve(t.output));
295
+ const under = (a, b) => a.startsWith(b.endsWith(path.sep) ? b : b + path.sep);
296
+ for (let i = 0; i < targets.length; i++) {
297
+ for (let j = i + 1; j < targets.length; j++) {
298
+ const [a, b] = [targets[i], targets[j]];
299
+ if (abs[i] === abs[j])
300
+ throw new SitefileError('E_CONFLICT', `bake targets '${a.name}' and '${b.name}' both write '${a.output}'`);
301
+ if (under(abs[j], abs[i]) || under(abs[i], abs[j])) {
302
+ throw new SitefileError('E_CONFLICT', `bake targets '${a.name}' and '${b.name}' write overlapping outputs '${a.output}' and '${b.output}'`);
303
+ }
304
+ const tag = a.tags.find((t) => b.tags.includes(t));
305
+ if (tag !== undefined)
306
+ throw new SitefileError('E_CONFLICT', `bake targets '${a.name}' and '${b.name}' both tag '${tag}'`);
307
+ }
308
+ }
309
+ }
310
+ /** Read, check and resolve a bake file. */
311
+ export async function bakePlan(file, selected) {
312
+ return resolveBake(parseBakeDefinition(await loadBakeFile(file), file), file, selected);
313
+ }
314
+ /** `--print --json`. */
315
+ export function bakePlanJson(plan) {
316
+ return {
317
+ file: plan.file,
318
+ targets: plan.targets.map((t) => ({
319
+ name: t.name,
320
+ context: t.context,
321
+ file: t.file,
322
+ args: t.args,
323
+ argFiles: t.argFiles,
324
+ target: t.target,
325
+ output: t.output,
326
+ format: t.format,
327
+ tags: t.tags,
328
+ matrix: t.matrix,
329
+ })),
330
+ };
331
+ }
332
+ /** `--print`: one block per target, then a count. */
333
+ export function bakePlanText(plan) {
334
+ const line = (label, value) => ` ${label.padEnd(10)}${value}\n`;
335
+ let s = '';
336
+ for (const t of plan.targets) {
337
+ s += `${t.name}\n`;
338
+ s += line('context', t.context);
339
+ s += line('file', t.file);
340
+ if (t.target !== null)
341
+ s += line('target', t.target);
342
+ s += line('output', t.output);
343
+ if (t.format !== null)
344
+ s += line('format', t.format);
345
+ for (const [k, v] of Object.entries(t.args))
346
+ s += line('arg', `${k}=${v}`);
347
+ for (const f of t.argFiles)
348
+ s += line('arg-file', f);
349
+ for (const tag of t.tags)
350
+ s += line('tag', tag);
351
+ if (t.matrix)
352
+ s += line('matrix', Object.entries(t.matrix).map(([k, v]) => `${k}=${v}`).join(' '));
353
+ }
354
+ return s + `${plan.targets.length} target${plan.targets.length === 1 ? '' : 's'} from ${plan.file}\n`;
355
+ }
356
+ /** The `sitefile build` arguments of one target (before the CLI's pass-through flags). */
357
+ export function buildArgv(t) {
358
+ return [
359
+ 'build',
360
+ t.context,
361
+ '-f',
362
+ t.file,
363
+ '-o',
364
+ t.output,
365
+ ...(t.target !== null ? ['--target', t.target] : []),
366
+ ...(t.format !== null ? ['--format', t.format] : []),
367
+ ...t.argFiles.flatMap((f) => ['--build-arg-file', f]),
368
+ ...Object.entries(t.args).flatMap(([k, v]) => ['--build-arg', `${k}=${v}`]),
369
+ ...t.tags.flatMap((tag) => ['-t', tag]),
370
+ ];
371
+ }
372
+ //# sourceMappingURL=bake.js.map
@@ -0,0 +1,76 @@
1
+ import { type SourceSpan } from './errors.js';
2
+ import type { BaseImage, Descriptor } from './oci.js';
3
+ import { type ImageRef } from './ref.js';
4
+ import { type Credentials } from './registry-auth.js';
5
+ import { RegistryClient, type RegistryOptions } from './registry.js';
6
+ import type { Lockfile } from './resources/lockfile.js';
7
+ import type { Vendor } from './resources/vendor.js';
8
+ import type { Store } from './store.js';
9
+ import type { Tree } from './tree.js';
10
+ /** Registries reachable over plain http: `--insecure-registry` flags plus $SITEFILE_INSECURE_REGISTRIES (comma-separated). */
11
+ export declare function insecureRegistriesFromEnv(extra?: readonly string[], env?: NodeJS.ProcessEnv): string[];
12
+ /** Build a client for a (canonical) registry name, with credentials resolved lazily. */
13
+ export declare function makeClient(registry: string, options?: RegistryOptions & {
14
+ readonly explicitCredentials?: Credentials | undefined;
15
+ }): RegistryClient;
16
+ export interface ResolveBaseOptions {
17
+ readonly store: Store;
18
+ /** `os/arch` of the base to use (default linux/amd64). */
19
+ readonly platform?: string | undefined;
20
+ readonly lock?: Lockfile | undefined;
21
+ /** `--frozen-lockfile`: never resolve a tag; it must be in the lockfile (or pinned by digest). */
22
+ readonly frozen?: boolean | undefined;
23
+ /** `--update-lock`: re-resolve the tag and re-record it. */
24
+ readonly update?: boolean | undefined;
25
+ /** `sitefile vendor`'s directory: a tag it recorded is used when the lockfile has none, and its blobs seed the store. */
26
+ readonly vendor?: Vendor | undefined;
27
+ readonly registry?: RegistryOptions | undefined;
28
+ readonly warn?: ((message: string) => void) | undefined;
29
+ readonly span?: SourceSpan | undefined;
30
+ /** How messages name the reference (default `BASE`; `FROM image` for a stage base). */
31
+ readonly lead?: string | undefined;
32
+ /** Fail when the image's platform differs from `platform` (default true; FROM image reads only files). */
33
+ readonly enforcePlatform?: boolean | undefined;
34
+ }
35
+ /** A BASE reference (a bare name means Docker Hub), how it is shown, and its `Sitefile.lock` `images` key. */
36
+ export declare function baseRef(input: string, span?: SourceSpan, lead?: string): {
37
+ ref: ImageRef;
38
+ registry: string;
39
+ display: string;
40
+ lockKey: string;
41
+ };
42
+ /**
43
+ * Resolve `EXPORT ... AS oci BASE <image>` to a base image whose blobs are in the local store.
44
+ *
45
+ * - `image@sha256:...` is pinned by the Sitefile itself and used as is;
46
+ * - `image:tag` is looked up in Sitefile.lock (`images`) and fetched BY DIGEST, so a tag that has
47
+ * moved on the registry does not change the build; without a lock entry the tag is resolved once and
48
+ * recorded (an error with --frozen-lockfile; re-resolved with --update-lock);
49
+ * - a base already in the store (matching manifest and blobs) is used without any network access.
50
+ * A bare name (`nginx:alpine`) means Docker Hub (`docker.io/library/nginx`).
51
+ */
52
+ export declare function resolveBase(input: string, o: ResolveBaseOptions): Promise<BaseImage>;
53
+ /** A `FROM image` stage base, resolved: its files and the image (manifest) digest they came from. */
54
+ export interface FromImage {
55
+ readonly tree: Tree;
56
+ /** The platform manifest digest (the image ID). */
57
+ readonly digest: string;
58
+ /** `registry/name:tag` (or the local name as written), without a digest. */
59
+ readonly name: string;
60
+ }
61
+ /**
62
+ * Resolve `FROM image <ref> [AT <dir>]` to the image's files.
63
+ *
64
+ * - A reference with a registry host is resolved exactly like a BASE (see {@link resolveBase}): pinned by
65
+ * `@sha256:` or a Sitefile.lock `images` entry, fetched by digest, honouring --frozen-lockfile,
66
+ * --update-lock, the vendor directory and the registry's auth/TLS/network rules. Any platform is accepted
67
+ * (an index is narrowed to `platform`): only the files are read.
68
+ * - A reference without a registry host (`theme:1`, `theme@sha256:...`, an image ID) names an image in the
69
+ * local store (`sitefile build -t`, `pull`, `load`) and never touches the network or the lockfile.
70
+ *
71
+ * The files are the image's site layer under its `dev.sitefile.root` label (`/` without one), or under `at`.
72
+ * Their origins read `image <name>@<digest>:<path in the layer>`.
73
+ */
74
+ export declare function resolveFromImage(input: string, at: string | undefined, local: boolean, o: ResolveBaseOptions): Promise<FromImage>;
75
+ /** Read the blobs of `layers` from the store (verifying each digest), for writing a self-contained layout. */
76
+ export declare function readLayerBlobs(store: Store, layers: readonly Descriptor[]): Promise<Map<string, Uint8Array>>;
@@ -0,0 +1,138 @@
1
+ import { SitefileError } from './errors.js';
2
+ import { parseRef, withDefaultRegistry } from './ref.js';
3
+ import { resolveCredentials } from './registry-auth.js';
4
+ import { pullImage, resolveDigest } from './registry-ops.js';
5
+ import { RegistryClient } from './registry.js';
6
+ /** Registries reachable over plain http: `--insecure-registry` flags plus $SITEFILE_INSECURE_REGISTRIES (comma-separated). */
7
+ export function insecureRegistriesFromEnv(extra = [], env = process.env) {
8
+ const fromEnv = (env.SITEFILE_INSECURE_REGISTRIES ?? '').split(',').map((s) => s.trim()).filter(Boolean);
9
+ return [...new Set([...extra, ...fromEnv])];
10
+ }
11
+ /** Build a client for a (canonical) registry name, with credentials resolved lazily. */
12
+ export function makeClient(registry, options = {}) {
13
+ const { explicitCredentials, ...rest } = options;
14
+ return new RegistryClient(registry, {
15
+ ...rest,
16
+ credentials: rest.credentials ?? (() => resolveCredentials(registry, { explicit: explicitCredentials })),
17
+ });
18
+ }
19
+ /** A BASE reference (a bare name means Docker Hub), how it is shown, and its `Sitefile.lock` `images` key. */
20
+ export function baseRef(input, span, lead = 'BASE') {
21
+ let ref;
22
+ try {
23
+ ref = withDefaultRegistry(parseRef(input));
24
+ }
25
+ catch (e) {
26
+ if (e instanceof SitefileError)
27
+ throw new SitefileError(e.code, `${lead}: ${e.detail}`, span, { cause: e });
28
+ throw e;
29
+ }
30
+ const registry = ref.registry;
31
+ const display = `${registry}/${ref.name}${ref.tag !== undefined || ref.digest === undefined ? `:${ref.tag ?? 'latest'}` : ''}`;
32
+ const lockKey = `${registry}/${ref.name}:${ref.tag ?? 'latest'}`;
33
+ return { ref, registry, display, lockKey };
34
+ }
35
+ /**
36
+ * Resolve `EXPORT ... AS oci BASE <image>` to a base image whose blobs are in the local store.
37
+ *
38
+ * - `image@sha256:...` is pinned by the Sitefile itself and used as is;
39
+ * - `image:tag` is looked up in Sitefile.lock (`images`) and fetched BY DIGEST, so a tag that has
40
+ * moved on the registry does not change the build; without a lock entry the tag is resolved once and
41
+ * recorded (an error with --frozen-lockfile; re-resolved with --update-lock);
42
+ * - a base already in the store (matching manifest and blobs) is used without any network access.
43
+ * A bare name (`nginx:alpine`) means Docker Hub (`docker.io/library/nginx`).
44
+ */
45
+ export async function resolveBase(input, o) {
46
+ const lead = o.lead ?? 'BASE';
47
+ const { ref, registry, display, lockKey } = baseRef(input, o.span, lead);
48
+ const client = makeClient(registry, o.registry ?? {});
49
+ let rootDigest = ref.digest;
50
+ let fresh = false;
51
+ if (rootDigest === undefined) {
52
+ const locked = o.update ? undefined : o.lock?.getImage(lockKey);
53
+ const vendored = locked || o.update || o.frozen ? undefined : o.vendor?.image(lockKey);
54
+ if (locked) {
55
+ rootDigest = locked.digest;
56
+ o.lock?.markImageUsed(lockKey);
57
+ }
58
+ else if (vendored) {
59
+ rootDigest = vendored.digest;
60
+ o.lock?.recordImage(lockKey, { digest: rootDigest });
61
+ }
62
+ else if (o.frozen) {
63
+ throw new SitefileError('E_LOCK_MISSING', `${lead} ${input}: '${lockKey}' is not in the lockfile (--frozen-lockfile); pin it with @sha256:... or run a build without --frozen-lockfile to record it`, o.span);
64
+ }
65
+ else {
66
+ rootDigest = await resolveDigest(o.store, client, ref);
67
+ fresh = true;
68
+ if (o.lock)
69
+ o.lock.recordImage(lockKey, { digest: rootDigest });
70
+ else
71
+ o.warn?.(`${lead} ${input} is not pinned and there is no lockfile: the build is not reproducible (use ${display}@sha256:... or a Sitefile.lock)`);
72
+ }
73
+ }
74
+ // Vendored blobs go into the store first, so the pull below finds them and needs no network.
75
+ if (o.vendor)
76
+ await o.vendor.seedStore(o.store, rootDigest, o.platform ?? 'linux/amd64');
77
+ const pulled = await pullImage(o.store, client, ref, {
78
+ platform: o.platform,
79
+ enforcePlatform: o.enforcePlatform ?? true,
80
+ rootDigest,
81
+ tag: fresh,
82
+ });
83
+ return { name: display, manifestDigest: pulled.id, layers: pulled.manifest.layers, config: pulled.config };
84
+ }
85
+ /**
86
+ * Resolve `FROM image <ref> [AT <dir>]` to the image's files.
87
+ *
88
+ * - A reference with a registry host is resolved exactly like a BASE (see {@link resolveBase}): pinned by
89
+ * `@sha256:` or a Sitefile.lock `images` entry, fetched by digest, honouring --frozen-lockfile,
90
+ * --update-lock, the vendor directory and the registry's auth/TLS/network rules. Any platform is accepted
91
+ * (an index is narrowed to `platform`): only the files are read.
92
+ * - A reference without a registry host (`theme:1`, `theme@sha256:...`, an image ID) names an image in the
93
+ * local store (`sitefile build -t`, `pull`, `load`) and never touches the network or the lockfile.
94
+ *
95
+ * The files are the image's site layer under its `dev.sitefile.root` label (`/` without one), or under `at`.
96
+ * Their origins read `image <name>@<digest>:<path in the layer>`.
97
+ */
98
+ export async function resolveFromImage(input, at, local, o) {
99
+ const lead = o.lead ?? 'FROM image';
100
+ let id;
101
+ let name;
102
+ if (local) {
103
+ name = input.split('@')[0];
104
+ try {
105
+ id = await o.store.resolve(input);
106
+ }
107
+ catch (e) {
108
+ if (e instanceof SitefileError && e.code === 'E_NOT_FOUND') {
109
+ throw new SitefileError('E_NOT_FOUND', `${lead} ${input}: no such image in the local store; build it with -t ${name}, pull it, or name its registry (registry.example/${name})`, o.span, { cause: e });
110
+ }
111
+ throw e;
112
+ }
113
+ }
114
+ else {
115
+ name = baseRef(input, o.span, lead).display;
116
+ id = (await resolveBase(input, { ...o, lead, enforcePlatform: false })).manifestDigest;
117
+ }
118
+ let tree;
119
+ try {
120
+ tree = await o.store.loadTree(id, { origin: `image ${name}@${id}`, ...(at !== undefined ? { root: at } : {}) });
121
+ }
122
+ catch (e) {
123
+ // Name the FROM line in errors about the image's content (not a Sitefile image, corrupt blobs).
124
+ if (e instanceof SitefileError && e.span === undefined && o.span)
125
+ throw new SitefileError(e.code, e.detail, o.span, { cause: e });
126
+ throw e;
127
+ }
128
+ return { tree, digest: id, name };
129
+ }
130
+ /** Read the blobs of `layers` from the store (verifying each digest), for writing a self-contained layout. */
131
+ export async function readLayerBlobs(store, layers) {
132
+ const out = new Map();
133
+ for (const l of layers)
134
+ if (!out.has(l.digest))
135
+ out.set(l.digest, await store.readBlob(l.digest, l.size));
136
+ return out;
137
+ }
138
+ //# sourceMappingURL=base-image.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Read build-arg values from a file, by extension:
3
+ *
4
+ * - `.json`: a flat object, `{"TITLE": "Docs", "PORT": 8080}`
5
+ * - `.yaml` / `.yml`: a flat mapping, `TITLE: Docs`
6
+ * - `.env` (or a file named `.env`, `.env.*`, `*.env`): `KEY=value` lines
7
+ *
8
+ * Values must be strings, numbers or booleans; they become strings and are
9
+ * checked against each ARG's declared type later, like `--build-arg`.
10
+ * `.env` files are parsed, never executed: no `$VAR` or `$(…)` expansion.
11
+ */
12
+ export declare function loadBuildArgFile(file: string): Promise<Record<string, string>>;
13
+ export declare function buildArgFileKind(file: string): 'json' | 'yaml' | 'env';
14
+ /**
15
+ * `.env` syntax: one `KEY=value` per line, optional `export ` prefix, `#`
16
+ * comment lines and blank lines. Unquoted values are trimmed and a ` #` starts
17
+ * a comment; `'single'` quotes are literal; `"double"` quotes understand
18
+ * `\n \t \r \" \\` and may span lines. Nothing is expanded or executed.
19
+ */
20
+ export declare function parseEnvFile(text: string, label: string): Record<string, string>;
21
+ /**
22
+ * The CLI's build args: `--build-arg-file` files in order (later files win), then
23
+ * `--build-arg NAME=value` (or `NAME`, read from the environment), which win over files.
24
+ * Malformed flags are E_USAGE errors.
25
+ */
26
+ export declare function collectBuildArgs(files: readonly string[], flags: readonly string[], env?: NodeJS.ProcessEnv): Promise<Record<string, string>>;