@johnhenry/sitefile 0.0.0-stage → 0.0.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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { crc32, deflateRaw, gzip } from './deflate.js';
|
|
2
|
+
import { SitefileError } from './errors.js';
|
|
3
|
+
/** Read `SOURCE_DATE_EPOCH`; undefined if unset, E_USAGE if malformed. */
|
|
4
|
+
export function sourceDateEpoch(env = process.env) {
|
|
5
|
+
const raw = env.SOURCE_DATE_EPOCH;
|
|
6
|
+
if (raw === undefined || raw === '')
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!/^\d{1,12}$/.test(raw))
|
|
9
|
+
throw new SitefileError('E_USAGE', `SOURCE_DATE_EPOCH must be a non-negative integer, got '${raw}'`);
|
|
10
|
+
return Number(raw);
|
|
11
|
+
}
|
|
12
|
+
const enc = new TextEncoder();
|
|
13
|
+
function entriesOf(tree) {
|
|
14
|
+
return tree.entries().map(([p, n]) => {
|
|
15
|
+
if (!p.startsWith('/') || p.endsWith('/') || p.includes('\0') || p.includes('\\') || p.split('/').some((s, i) => i > 0 && (s === '' || s === '.' || s === '..'))) {
|
|
16
|
+
throw new SitefileError('E_EXPORT', `invalid artifact path '${p}'`);
|
|
17
|
+
}
|
|
18
|
+
const name = p.slice(1);
|
|
19
|
+
return { name, nameBytes: enc.encode(name), bytes: n.bytes, mode: n.mode };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export { crc32 };
|
|
23
|
+
// --------------------------------------------------------------------- zip
|
|
24
|
+
const ZIP_MAX_ENTRIES = 0xffff;
|
|
25
|
+
const ZIP_MAX = 0xffffffff;
|
|
26
|
+
function dosDateTime(epoch) {
|
|
27
|
+
if (epoch === undefined)
|
|
28
|
+
return { time: 0, date: (0 << 9) | (1 << 5) | 1 }; // 1980-01-01 00:00:00
|
|
29
|
+
const d = new Date(Math.max(epoch, 315532800) * 1000); // clamp to 1980-01-01
|
|
30
|
+
const year = Math.min(d.getUTCFullYear(), 2107);
|
|
31
|
+
// Seconds are stored in 2-second units.
|
|
32
|
+
return {
|
|
33
|
+
time: (d.getUTCHours() << 11) | (d.getUTCMinutes() << 5) | (d.getUTCSeconds() >> 1),
|
|
34
|
+
date: ((year - 1980) << 9) | ((d.getUTCMonth() + 1) << 5) | d.getUTCDate(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Zip (no zip64). Deflate when it shrinks the data, otherwise stored. */
|
|
38
|
+
export function writeZip(tree, options = {}) {
|
|
39
|
+
const entries = entriesOf(tree);
|
|
40
|
+
if (entries.length > ZIP_MAX_ENTRIES) {
|
|
41
|
+
throw new SitefileError('E_EXPORT', `zip export supports at most ${ZIP_MAX_ENTRIES} entries (zip64 is not implemented); got ${entries.length}`);
|
|
42
|
+
}
|
|
43
|
+
const { time, date } = dosDateTime(options.epoch);
|
|
44
|
+
const chunks = [];
|
|
45
|
+
const central = [];
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
if (e.nameBytes.length > 0xffff)
|
|
49
|
+
throw new SitefileError('E_EXPORT', `zip entry name is too long (${e.nameBytes.length} bytes): '${e.name.slice(0, 60)}…'`);
|
|
50
|
+
const deflated = deflateRaw(e.bytes);
|
|
51
|
+
const stored = deflated.length >= e.bytes.length;
|
|
52
|
+
const data = stored ? e.bytes : deflated;
|
|
53
|
+
if (e.bytes.length > ZIP_MAX || data.length > ZIP_MAX) {
|
|
54
|
+
throw new SitefileError('E_EXPORT', `zip export does not support files of 4 GiB or more (zip64 is not implemented): '/${e.name}'`);
|
|
55
|
+
}
|
|
56
|
+
if (offset > ZIP_MAX)
|
|
57
|
+
throw new SitefileError('E_EXPORT', 'zip export does not support archives of 4 GiB or more (zip64 is not implemented)');
|
|
58
|
+
const method = stored ? 0 : 8;
|
|
59
|
+
const crc = crc32(e.bytes);
|
|
60
|
+
const lh = Buffer.alloc(30);
|
|
61
|
+
lh.writeUInt32LE(0x04034b50, 0);
|
|
62
|
+
lh.writeUInt16LE(20, 4); // version needed
|
|
63
|
+
lh.writeUInt16LE(0x0800, 6); // UTF-8 names
|
|
64
|
+
lh.writeUInt16LE(method, 8);
|
|
65
|
+
lh.writeUInt16LE(time, 10);
|
|
66
|
+
lh.writeUInt16LE(date, 12);
|
|
67
|
+
lh.writeUInt32LE(crc, 14);
|
|
68
|
+
lh.writeUInt32LE(data.length, 18);
|
|
69
|
+
lh.writeUInt32LE(e.bytes.length, 22);
|
|
70
|
+
lh.writeUInt16LE(e.nameBytes.length, 26);
|
|
71
|
+
lh.writeUInt16LE(0, 28);
|
|
72
|
+
const ch = Buffer.alloc(46);
|
|
73
|
+
ch.writeUInt32LE(0x02014b50, 0);
|
|
74
|
+
ch.writeUInt16LE((3 << 8) | 20, 4); // made by: Unix, spec 2.0
|
|
75
|
+
ch.writeUInt16LE(20, 6);
|
|
76
|
+
ch.writeUInt16LE(0x0800, 8);
|
|
77
|
+
ch.writeUInt16LE(method, 10);
|
|
78
|
+
ch.writeUInt16LE(time, 12);
|
|
79
|
+
ch.writeUInt16LE(date, 14);
|
|
80
|
+
ch.writeUInt32LE(crc, 16);
|
|
81
|
+
ch.writeUInt32LE(data.length, 20);
|
|
82
|
+
ch.writeUInt32LE(e.bytes.length, 24);
|
|
83
|
+
ch.writeUInt16LE(e.nameBytes.length, 28);
|
|
84
|
+
// extra, comment, disk number, internal attrs = 0
|
|
85
|
+
ch.writeUInt32LE(((0o100000 | e.mode) << 16) >>> 0, 38);
|
|
86
|
+
ch.writeUInt32LE(offset, 42);
|
|
87
|
+
chunks.push(lh, e.nameBytes, data);
|
|
88
|
+
central.push(ch, e.nameBytes);
|
|
89
|
+
offset += 30 + e.nameBytes.length + data.length;
|
|
90
|
+
}
|
|
91
|
+
const cdSize = central.reduce((n, c) => n + c.length, 0);
|
|
92
|
+
if (offset + cdSize > ZIP_MAX)
|
|
93
|
+
throw new SitefileError('E_EXPORT', 'zip export does not support archives of 4 GiB or more (zip64 is not implemented)');
|
|
94
|
+
const end = Buffer.alloc(22);
|
|
95
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
96
|
+
end.writeUInt16LE(entries.length, 8);
|
|
97
|
+
end.writeUInt16LE(entries.length, 10);
|
|
98
|
+
end.writeUInt32LE(cdSize, 12);
|
|
99
|
+
end.writeUInt32LE(offset, 16);
|
|
100
|
+
return Buffer.concat([...chunks, ...central, end]);
|
|
101
|
+
}
|
|
102
|
+
// --------------------------------------------------------------------- tar
|
|
103
|
+
const BLOCK = 512;
|
|
104
|
+
const TAR_MAX_SIZE = 8 ** 11 - 1; // 11 octal digits
|
|
105
|
+
function putOctal(h, at, width, value) {
|
|
106
|
+
h.write(value.toString(8).padStart(width - 1, '0') + '\0', at, 'latin1');
|
|
107
|
+
}
|
|
108
|
+
/** Largest prefix of `s` (UTF-8) not longer than `max` bytes, cut on a character boundary. */
|
|
109
|
+
function utf8Head(s, max) {
|
|
110
|
+
const b = enc.encode(s);
|
|
111
|
+
if (b.length <= max)
|
|
112
|
+
return b;
|
|
113
|
+
let end = max;
|
|
114
|
+
while (end > 0 && (b[end] & 0xc0) === 0x80)
|
|
115
|
+
end--;
|
|
116
|
+
return b.subarray(0, end);
|
|
117
|
+
}
|
|
118
|
+
function tarHeader(name, prefix, size, mode, mtime, type) {
|
|
119
|
+
const h = Buffer.alloc(BLOCK);
|
|
120
|
+
h.set(name, 0);
|
|
121
|
+
putOctal(h, 100, 8, mode);
|
|
122
|
+
putOctal(h, 108, 8, 0); // uid
|
|
123
|
+
putOctal(h, 116, 8, 0); // gid
|
|
124
|
+
putOctal(h, 124, 12, size);
|
|
125
|
+
putOctal(h, 136, 12, mtime);
|
|
126
|
+
h.fill(0x20, 148, 156); // checksum placeholder
|
|
127
|
+
h.write(type, 156, 'latin1');
|
|
128
|
+
h.write('ustar\0', 257, 'latin1');
|
|
129
|
+
h.write('00', 263, 'latin1');
|
|
130
|
+
h.set(prefix, 345);
|
|
131
|
+
let sum = 0;
|
|
132
|
+
for (const b of h)
|
|
133
|
+
sum += b;
|
|
134
|
+
h.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 'latin1');
|
|
135
|
+
return h;
|
|
136
|
+
}
|
|
137
|
+
function padTo512(data) {
|
|
138
|
+
const rem = data.length % BLOCK;
|
|
139
|
+
return rem === 0 ? [data] : [data, Buffer.alloc(BLOCK - rem)];
|
|
140
|
+
}
|
|
141
|
+
function paxRecord(key, value) {
|
|
142
|
+
const body = enc.encode(` ${key}=${value}\n`);
|
|
143
|
+
// The length counts its own decimal digits.
|
|
144
|
+
let len = body.length + 1;
|
|
145
|
+
while (String(len).length + body.length !== len)
|
|
146
|
+
len = String(len).length + body.length;
|
|
147
|
+
return Buffer.concat([Buffer.from(String(len), 'latin1'), body]);
|
|
148
|
+
}
|
|
149
|
+
/** Split `name` for the ustar name/prefix fields, or undefined if it cannot fit. */
|
|
150
|
+
function ustarSplit(nameBytes, name) {
|
|
151
|
+
if (nameBytes.length <= 100)
|
|
152
|
+
return { name: nameBytes, prefix: new Uint8Array(0) };
|
|
153
|
+
for (let i = 0; i < nameBytes.length; i++) {
|
|
154
|
+
if (nameBytes[i] !== 0x2f)
|
|
155
|
+
continue;
|
|
156
|
+
const prefix = nameBytes.subarray(0, i);
|
|
157
|
+
const rest = nameBytes.subarray(i + 1);
|
|
158
|
+
if (prefix.length > 155)
|
|
159
|
+
break;
|
|
160
|
+
if (rest.length <= 100 && rest.length > 0)
|
|
161
|
+
return { name: rest, prefix };
|
|
162
|
+
}
|
|
163
|
+
void name;
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/** POSIX ustar; long names use the prefix field, then a pax `path` extended header. */
|
|
167
|
+
export function writeTar(tree, options = {}) {
|
|
168
|
+
const mtime = options.epoch ?? 0;
|
|
169
|
+
const chunks = [];
|
|
170
|
+
let n = 0;
|
|
171
|
+
for (const d of options.dirs ?? [])
|
|
172
|
+
chunks.push(tarHeader(enc.encode(d + '/'), new Uint8Array(0), 0, 0o755, mtime, '5'));
|
|
173
|
+
for (const e of entriesOf(tree)) {
|
|
174
|
+
if (e.bytes.length > TAR_MAX_SIZE) {
|
|
175
|
+
throw new SitefileError('E_EXPORT', `tar export does not support files of 8 GiB or more: '/${e.name}'`);
|
|
176
|
+
}
|
|
177
|
+
let split = ustarSplit(e.nameBytes, e.name);
|
|
178
|
+
if (!split) {
|
|
179
|
+
const rec = paxRecord('path', e.name);
|
|
180
|
+
const pname = enc.encode(`PaxHeaders/${n++}`);
|
|
181
|
+
chunks.push(tarHeader(pname, new Uint8Array(0), rec.length, 0o644, mtime, 'x'), ...padTo512(rec));
|
|
182
|
+
split = { name: utf8Head(e.name, 100), prefix: new Uint8Array(0) };
|
|
183
|
+
}
|
|
184
|
+
chunks.push(tarHeader(split.name, split.prefix, e.bytes.length, e.mode, mtime, '0'), ...padTo512(e.bytes));
|
|
185
|
+
}
|
|
186
|
+
chunks.push(Buffer.alloc(BLOCK * 2));
|
|
187
|
+
return Buffer.concat(chunks);
|
|
188
|
+
}
|
|
189
|
+
/** gzip of the tar, header mtime 0 and no filename, so the stream is reproducible. */
|
|
190
|
+
export function writeTgz(tree, options = {}) {
|
|
191
|
+
return gzipTar(writeTar(tree, options));
|
|
192
|
+
}
|
|
193
|
+
/** Reproducible gzip of an already-written tar (see deflate.ts for the exact format). */
|
|
194
|
+
export function gzipTar(tar) {
|
|
195
|
+
return gzip(tar);
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=archive-writers.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { BuildResult, Pins } from './executor.js';
|
|
2
|
+
import type { Tree } from './tree.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build attestations for OCI site images (`build --provenance` / `--sbom`).
|
|
5
|
+
*
|
|
6
|
+
* Each attestation is an in-toto Statement v1 (one JSON blob) wrapped in an OCI 1.1 artifact
|
|
7
|
+
* manifest whose `subject` is the image manifest:
|
|
8
|
+
*
|
|
9
|
+
* manifest artifactType application/vnd.in-toto+json, config = the empty descriptor ({}),
|
|
10
|
+
* one layer application/vnd.in-toto+json annotated in-toto.io/predicate-type,
|
|
11
|
+
* subject = the image manifest descriptor
|
|
12
|
+
*
|
|
13
|
+
* - provenance: predicate https://slsa.dev/provenance/v1 (the Sitefile digest, build args except
|
|
14
|
+
* secret-looking names, every import with its digest, the BASE image digest, the builder version)
|
|
15
|
+
* - sbom: predicate https://cyclonedx.org/bom, a CycloneDX 1.6 document listing every file in the
|
|
16
|
+
* site layer (path in the image, SHA-256, mode, origin) and the BASE image
|
|
17
|
+
*
|
|
18
|
+
* The image manifest is never touched: an attestation points at it, so the image ID is the same with
|
|
19
|
+
* or without attestations. Everything is canonical JSON with no timestamps unless SOURCE_DATE_EPOCH
|
|
20
|
+
* is set, so the same build gives byte-identical attestations (in both implementations).
|
|
21
|
+
*/
|
|
22
|
+
/** Sitefile version recorded as the builder version (also `sitefile --version`). */
|
|
23
|
+
export declare const SITEFILE_VERSION = "0.1.0";
|
|
24
|
+
export declare const ATTEST: {
|
|
25
|
+
/** artifactType of the attestation manifest and media type of its layer. */
|
|
26
|
+
readonly inToto: 'application/vnd.in-toto+json';
|
|
27
|
+
readonly emptyConfig: 'application/vnd.oci.empty.v1+json';
|
|
28
|
+
readonly predicateAnnotation: 'in-toto.io/predicate-type';
|
|
29
|
+
readonly statement: 'https://in-toto.io/Statement/v1';
|
|
30
|
+
readonly slsa: 'https://slsa.dev/provenance/v1';
|
|
31
|
+
readonly cyclonedx: 'https://cyclonedx.org/bom';
|
|
32
|
+
readonly buildType: 'https://github.com/johnhenry/sitefile/blob/main/docs/CLI.md#attestations';
|
|
33
|
+
readonly builderId: 'https://github.com/johnhenry/sitefile';
|
|
34
|
+
};
|
|
35
|
+
/** The OCI empty descriptor's blob (`{}`). */
|
|
36
|
+
export declare const EMPTY_JSON: NodeJS.NonSharedUint8Array;
|
|
37
|
+
export declare const EMPTY_DIGEST: string;
|
|
38
|
+
export interface AttestRequest {
|
|
39
|
+
readonly provenance: boolean;
|
|
40
|
+
readonly sbom: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** One attestation (or any referrer): its index descriptor and the blobs it needs. */
|
|
43
|
+
export interface Referrer {
|
|
44
|
+
/** Image-index descriptor: mediaType, digest, size, artifactType, annotations. */
|
|
45
|
+
readonly descriptor: ReferrerDescriptor;
|
|
46
|
+
/** digest -> bytes: the manifest, its config and its layers. */
|
|
47
|
+
readonly blobs: ReadonlyArray<readonly [string, Uint8Array]>;
|
|
48
|
+
}
|
|
49
|
+
export interface ReferrerDescriptor {
|
|
50
|
+
readonly mediaType: string;
|
|
51
|
+
readonly digest: string;
|
|
52
|
+
readonly size: number;
|
|
53
|
+
readonly artifactType?: string | undefined;
|
|
54
|
+
readonly annotations?: Readonly<Record<string, string>> | undefined;
|
|
55
|
+
}
|
|
56
|
+
/** What the provenance and SBOM say about a build (deterministic: no absolute paths, no times). */
|
|
57
|
+
export interface BuildFacts {
|
|
58
|
+
/** The Sitefile's path relative to the context (its basename when it lives elsewhere). */
|
|
59
|
+
readonly sitefile: string;
|
|
60
|
+
readonly sitefileDigest: string;
|
|
61
|
+
/** `--build-arg` / `--build-arg-file` values, except those whose names look secret. */
|
|
62
|
+
readonly args: Readonly<Record<string, string>>;
|
|
63
|
+
/** Names of the build args left out because they look secret (API_TOKEN, DB_PASSWORD, ...). */
|
|
64
|
+
readonly redactedArgs: readonly string[];
|
|
65
|
+
readonly platform: string;
|
|
66
|
+
/** Imports in declaration order with the digest the build used (Sitefile.lock's for remote ones). */
|
|
67
|
+
readonly imports: ReadonlyArray<{
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly uri: string;
|
|
70
|
+
readonly resolved?: string | undefined;
|
|
71
|
+
readonly digest: string;
|
|
72
|
+
readonly gitCommit?: string | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
readonly base?: {
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly digest: string;
|
|
77
|
+
} | undefined;
|
|
78
|
+
/** `FROM image` stage bases, in stage order (FROM line, image name, image digest). */
|
|
79
|
+
/** `import`: the qualified name of the imported Sitefile (IMPORT … AS sitefile) whose stage it is. */
|
|
80
|
+
readonly images?: ReadonlyArray<{
|
|
81
|
+
readonly line: number;
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly digest: string;
|
|
84
|
+
readonly import?: string | undefined;
|
|
85
|
+
}> | undefined;
|
|
86
|
+
/** `--strict-pins`: how each remote input was pinned (shown in the provenance). */
|
|
87
|
+
readonly pins?: Pins | undefined;
|
|
88
|
+
/** File origins start with the Sitefile label (relative to the cwd); they are rewritten to `sitefile`. */
|
|
89
|
+
readonly label: string;
|
|
90
|
+
}
|
|
91
|
+
/** The image an attestation is about. */
|
|
92
|
+
export interface AttestSubject {
|
|
93
|
+
readonly digest: string;
|
|
94
|
+
readonly size: number;
|
|
95
|
+
/** The site's files (rooted at `/`) and where they sit in the image. */
|
|
96
|
+
readonly files: Tree;
|
|
97
|
+
readonly root: string;
|
|
98
|
+
/** org.opencontainers.image.title, if the image has one. */
|
|
99
|
+
readonly title?: string | undefined;
|
|
100
|
+
}
|
|
101
|
+
/** Whether a build-arg name looks like it holds a secret (any `_`-separated word is key, token, password, ...). */
|
|
102
|
+
export declare function isSecretArgName(name: string): boolean;
|
|
103
|
+
/** The Sitefile path the build used, as `build` resolves it. */
|
|
104
|
+
export declare function sitefilePathOf(contextDir: string, sitefile: string | undefined): string;
|
|
105
|
+
/** Collect the facts for a finished build. Reads the Sitefile again to digest it. */
|
|
106
|
+
export declare function buildFacts(result: Pick<BuildResult, 'resources' | 'base' | 'pins' | 'images'>, options: {
|
|
107
|
+
readonly contextDir: string;
|
|
108
|
+
readonly sitefile?: string | undefined;
|
|
109
|
+
readonly label?: string | undefined;
|
|
110
|
+
readonly buildArgs?: Readonly<Record<string, string>> | undefined;
|
|
111
|
+
readonly platform?: string | undefined;
|
|
112
|
+
}): Promise<BuildFacts>;
|
|
113
|
+
/** SLSA provenance v1 predicate. */
|
|
114
|
+
export declare function provenancePredicate(facts: BuildFacts, epoch: number | undefined): Record<string, unknown>;
|
|
115
|
+
/** CycloneDX 1.6 SBOM: the image, its BASE and every file of the site layer. */
|
|
116
|
+
export declare function sbomPredicate(subject: AttestSubject, facts: BuildFacts, epoch: number | undefined): Record<string, unknown>;
|
|
117
|
+
/** Wrap a predicate in an in-toto statement about `subject` and an OCI artifact manifest. */
|
|
118
|
+
export declare function attestationReferrer(subject: Pick<AttestSubject, 'digest' | 'size'>, predicateType: string, predicate: unknown): Referrer;
|
|
119
|
+
/** The requested attestations for an image, provenance first. */
|
|
120
|
+
export declare function makeAttestations(subject: AttestSubject, facts: BuildFacts, request: AttestRequest, epoch: number | undefined): Referrer[];
|
|
121
|
+
/** An OCI image index listing referrers (the store's referrers file and the registry fallback tag). */
|
|
122
|
+
export declare function encodeReferrersIndex(descriptors: readonly ReferrerDescriptor[]): Uint8Array;
|
|
123
|
+
/** Parse a referrers index; entries that are not well-formed descriptors are dropped. */
|
|
124
|
+
export declare function decodeReferrersIndex(bytes: Uint8Array): ReferrerDescriptor[];
|
|
125
|
+
/** The referrers fallback tag for a subject digest (`sha256-<hex>`, distribution spec v1.1). */
|
|
126
|
+
export declare function referrersTag(subject: string): string;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256 } from './digest.js';
|
|
4
|
+
import { SitefileError } from './errors.js';
|
|
5
|
+
import { canonicalJson, MEDIA } from './oci.js';
|
|
6
|
+
/**
|
|
7
|
+
* Build attestations for OCI site images (`build --provenance` / `--sbom`).
|
|
8
|
+
*
|
|
9
|
+
* Each attestation is an in-toto Statement v1 (one JSON blob) wrapped in an OCI 1.1 artifact
|
|
10
|
+
* manifest whose `subject` is the image manifest:
|
|
11
|
+
*
|
|
12
|
+
* manifest artifactType application/vnd.in-toto+json, config = the empty descriptor ({}),
|
|
13
|
+
* one layer application/vnd.in-toto+json annotated in-toto.io/predicate-type,
|
|
14
|
+
* subject = the image manifest descriptor
|
|
15
|
+
*
|
|
16
|
+
* - provenance: predicate https://slsa.dev/provenance/v1 (the Sitefile digest, build args except
|
|
17
|
+
* secret-looking names, every import with its digest, the BASE image digest, the builder version)
|
|
18
|
+
* - sbom: predicate https://cyclonedx.org/bom, a CycloneDX 1.6 document listing every file in the
|
|
19
|
+
* site layer (path in the image, SHA-256, mode, origin) and the BASE image
|
|
20
|
+
*
|
|
21
|
+
* The image manifest is never touched: an attestation points at it, so the image ID is the same with
|
|
22
|
+
* or without attestations. Everything is canonical JSON with no timestamps unless SOURCE_DATE_EPOCH
|
|
23
|
+
* is set, so the same build gives byte-identical attestations (in both implementations).
|
|
24
|
+
*/
|
|
25
|
+
/** Sitefile version recorded as the builder version (also `sitefile --version`). */
|
|
26
|
+
export const SITEFILE_VERSION = '0.1.0';
|
|
27
|
+
export const ATTEST = {
|
|
28
|
+
/** artifactType of the attestation manifest and media type of its layer. */
|
|
29
|
+
inToto: 'application/vnd.in-toto+json',
|
|
30
|
+
emptyConfig: 'application/vnd.oci.empty.v1+json',
|
|
31
|
+
predicateAnnotation: 'in-toto.io/predicate-type',
|
|
32
|
+
statement: 'https://in-toto.io/Statement/v1',
|
|
33
|
+
slsa: 'https://slsa.dev/provenance/v1',
|
|
34
|
+
cyclonedx: 'https://cyclonedx.org/bom',
|
|
35
|
+
buildType: 'https://github.com/johnhenry/sitefile/blob/main/docs/CLI.md#attestations',
|
|
36
|
+
builderId: 'https://github.com/johnhenry/sitefile',
|
|
37
|
+
};
|
|
38
|
+
/** The OCI empty descriptor's blob (`{}`). */
|
|
39
|
+
export const EMPTY_JSON = new TextEncoder().encode('{}');
|
|
40
|
+
export const EMPTY_DIGEST = sha256(EMPTY_JSON);
|
|
41
|
+
const SECRET_WORDS = new Set(['apikey', 'auth', 'credential', 'credentials', 'key', 'passwd', 'password', 'pword', 'secret', 'token']);
|
|
42
|
+
/** Whether a build-arg name looks like it holds a secret (any `_`-separated word is key, token, password, ...). */
|
|
43
|
+
export function isSecretArgName(name) {
|
|
44
|
+
return name
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.split('_')
|
|
47
|
+
.some((w) => SECRET_WORDS.has(w));
|
|
48
|
+
}
|
|
49
|
+
/** The Sitefile path the build used, as `build` resolves it. */
|
|
50
|
+
export function sitefilePathOf(contextDir, sitefile) {
|
|
51
|
+
return path.resolve(sitefile ?? path.join(contextDir, 'Sitefile'));
|
|
52
|
+
}
|
|
53
|
+
/** Collect the facts for a finished build. Reads the Sitefile again to digest it. */
|
|
54
|
+
export async function buildFacts(result, options) {
|
|
55
|
+
const file = sitefilePathOf(options.contextDir, options.sitefile);
|
|
56
|
+
let bytes;
|
|
57
|
+
try {
|
|
58
|
+
bytes = new Uint8Array(await readFile(file));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
throw new SitefileError('E_NOT_FOUND', `cannot read Sitefile '${file}'`, undefined, { cause: e });
|
|
62
|
+
}
|
|
63
|
+
const rel = path.relative(path.resolve(options.contextDir), file);
|
|
64
|
+
const inside = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
65
|
+
const sitefile = inside ? rel.split(path.sep).join('/') : path.basename(file);
|
|
66
|
+
const args = {};
|
|
67
|
+
const redactedArgs = [];
|
|
68
|
+
for (const [k, v] of Object.entries(options.buildArgs ?? {})) {
|
|
69
|
+
if (isSecretArgName(k))
|
|
70
|
+
redactedArgs.push(k);
|
|
71
|
+
else
|
|
72
|
+
args[k] = v;
|
|
73
|
+
}
|
|
74
|
+
redactedArgs.sort();
|
|
75
|
+
const imports = [...result.resources].map(([name, r]) => ({ name, uri: r.provenance.uri, resolved: r.provenance.resolvedUrl, digest: r.provenance.digest, gitCommit: r.provenance.gitCommit }));
|
|
76
|
+
return {
|
|
77
|
+
sitefile,
|
|
78
|
+
sitefileDigest: sha256(bytes),
|
|
79
|
+
args,
|
|
80
|
+
redactedArgs,
|
|
81
|
+
platform: options.platform ?? 'linux/amd64',
|
|
82
|
+
imports,
|
|
83
|
+
base: result.base ? { name: result.base.name, digest: result.base.manifestDigest } : undefined,
|
|
84
|
+
images: result.images?.map((i) => ({ line: i.line, name: i.name, digest: i.digest, ...(i.import !== undefined ? { import: i.import } : {}) })),
|
|
85
|
+
pins: result.pins,
|
|
86
|
+
label: options.label ?? (path.relative(process.cwd(), file) || path.basename(file)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const hex = (digest) => digest.replace(/^sha256:/, '');
|
|
90
|
+
function isoOf(epoch) {
|
|
91
|
+
return new Date(epoch * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
92
|
+
}
|
|
93
|
+
/** SLSA provenance v1 predicate. */
|
|
94
|
+
export function provenancePredicate(facts, epoch) {
|
|
95
|
+
const externalParameters = { sitefile: facts.sitefile, args: { ...facts.args }, platform: facts.platform };
|
|
96
|
+
if (facts.redactedArgs.length)
|
|
97
|
+
externalParameters.redactedArgs = [...facts.redactedArgs];
|
|
98
|
+
// --strict-pins: the build refused unpinned inputs; each remote dependency says what pinned it.
|
|
99
|
+
if (facts.pins)
|
|
100
|
+
externalParameters.strictPins = true;
|
|
101
|
+
const deps = [{ name: 'sitefile', uri: facts.sitefile, digest: { sha256: hex(facts.sitefileDigest) } }];
|
|
102
|
+
for (const i of facts.imports) {
|
|
103
|
+
const d = { name: `import:${i.name}`, uri: i.uri, digest: i.gitCommit !== undefined ? { gitCommit: i.gitCommit, sha256: hex(i.digest) } : { sha256: hex(i.digest) } };
|
|
104
|
+
if (i.resolved !== undefined && i.resolved !== i.uri)
|
|
105
|
+
d.downloadLocation = i.resolved;
|
|
106
|
+
const pin = facts.pins?.imports[i.name];
|
|
107
|
+
if (pin !== undefined)
|
|
108
|
+
d.annotations = { 'dev.sitefile.pin': pin };
|
|
109
|
+
deps.push(d);
|
|
110
|
+
}
|
|
111
|
+
for (const i of facts.images ?? []) {
|
|
112
|
+
// A stage of an imported Sitefile is `from:<import>:<line>` (its line is in that Sitefile); --strict-pins checked it there.
|
|
113
|
+
const pin = i.import === undefined ? facts.pins?.images?.[i.line] : undefined;
|
|
114
|
+
deps.push({ name: i.import === undefined ? `from:${i.line}` : `from:${i.import}:${i.line}`, uri: i.name, digest: { sha256: hex(i.digest) }, ...(pin ? { annotations: { 'dev.sitefile.pin': pin } } : {}) });
|
|
115
|
+
}
|
|
116
|
+
if (facts.base) {
|
|
117
|
+
deps.push({ name: 'base', uri: facts.base.name, digest: { sha256: hex(facts.base.digest) }, ...(facts.pins?.base ? { annotations: { 'dev.sitefile.pin': facts.pins.base } } : {}) });
|
|
118
|
+
}
|
|
119
|
+
const runDetails = { builder: { id: ATTEST.builderId, version: { sitefile: SITEFILE_VERSION } } };
|
|
120
|
+
if (epoch !== undefined)
|
|
121
|
+
runDetails.metadata = { startedOn: isoOf(epoch), finishedOn: isoOf(epoch) };
|
|
122
|
+
return { buildDefinition: { buildType: ATTEST.buildType, externalParameters, resolvedDependencies: deps }, runDetails };
|
|
123
|
+
}
|
|
124
|
+
/** CycloneDX 1.6 SBOM: the image, its BASE and every file of the site layer. */
|
|
125
|
+
export function sbomPredicate(subject, facts, epoch) {
|
|
126
|
+
const metadata = {
|
|
127
|
+
tools: { components: [{ type: 'application', name: 'sitefile', version: SITEFILE_VERSION }] },
|
|
128
|
+
component: { type: 'container', 'bom-ref': 'image', name: subject.title ?? 'site', version: subject.digest },
|
|
129
|
+
};
|
|
130
|
+
if (epoch !== undefined)
|
|
131
|
+
metadata.timestamp = isoOf(epoch);
|
|
132
|
+
const components = [];
|
|
133
|
+
if (facts.base)
|
|
134
|
+
components.push({ type: 'container', 'bom-ref': 'base', name: facts.base.name, version: facts.base.digest });
|
|
135
|
+
const prefix = `${facts.label}:`;
|
|
136
|
+
for (const [p, n] of subject.files.entries()) {
|
|
137
|
+
const name = subject.root === '/' ? p : subject.root + p;
|
|
138
|
+
const origin = n.origin.startsWith(prefix) ? `${facts.sitefile}:${n.origin.slice(prefix.length)}` : n.origin;
|
|
139
|
+
components.push({
|
|
140
|
+
type: 'file',
|
|
141
|
+
'bom-ref': `file:${name}`,
|
|
142
|
+
name,
|
|
143
|
+
hashes: [{ alg: 'SHA-256', content: hex(n.digest) }],
|
|
144
|
+
properties: [
|
|
145
|
+
{ name: 'sitefile:mode', value: n.mode.toString(8) },
|
|
146
|
+
{ name: 'sitefile:origin', value: origin },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, metadata, components };
|
|
151
|
+
}
|
|
152
|
+
const enc = new TextEncoder();
|
|
153
|
+
/** Wrap a predicate in an in-toto statement about `subject` and an OCI artifact manifest. */
|
|
154
|
+
export function attestationReferrer(subject, predicateType, predicate) {
|
|
155
|
+
const statement = enc.encode(canonicalJson({ _type: ATTEST.statement, subject: [{ name: '_', digest: { sha256: hex(subject.digest) } }], predicateType, predicate }));
|
|
156
|
+
const layerDigest = sha256(statement);
|
|
157
|
+
const annotations = { [ATTEST.predicateAnnotation]: predicateType };
|
|
158
|
+
const manifest = enc.encode(canonicalJson({
|
|
159
|
+
schemaVersion: 2,
|
|
160
|
+
mediaType: MEDIA.manifest,
|
|
161
|
+
artifactType: ATTEST.inToto,
|
|
162
|
+
config: { mediaType: ATTEST.emptyConfig, digest: EMPTY_DIGEST, size: EMPTY_JSON.byteLength },
|
|
163
|
+
layers: [{ mediaType: ATTEST.inToto, digest: layerDigest, size: statement.byteLength, annotations }],
|
|
164
|
+
subject: { mediaType: MEDIA.manifest, digest: subject.digest, size: subject.size },
|
|
165
|
+
annotations,
|
|
166
|
+
}));
|
|
167
|
+
const digest = sha256(manifest);
|
|
168
|
+
return {
|
|
169
|
+
descriptor: { mediaType: MEDIA.manifest, digest, size: manifest.byteLength, artifactType: ATTEST.inToto, annotations },
|
|
170
|
+
blobs: [
|
|
171
|
+
[digest, manifest],
|
|
172
|
+
[EMPTY_DIGEST, EMPTY_JSON],
|
|
173
|
+
[layerDigest, statement],
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** The requested attestations for an image, provenance first. */
|
|
178
|
+
export function makeAttestations(subject, facts, request, epoch) {
|
|
179
|
+
const out = [];
|
|
180
|
+
if (request.provenance)
|
|
181
|
+
out.push(attestationReferrer(subject, ATTEST.slsa, provenancePredicate(facts, epoch)));
|
|
182
|
+
if (request.sbom)
|
|
183
|
+
out.push(attestationReferrer(subject, ATTEST.cyclonedx, sbomPredicate(subject, facts, epoch)));
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/** An OCI image index listing referrers (the store's referrers file and the registry fallback tag). */
|
|
187
|
+
export function encodeReferrersIndex(descriptors) {
|
|
188
|
+
return enc.encode(canonicalJson({ schemaVersion: 2, mediaType: MEDIA.index, manifests: descriptors }));
|
|
189
|
+
}
|
|
190
|
+
/** Parse a referrers index; entries that are not well-formed descriptors are dropped. */
|
|
191
|
+
export function decodeReferrersIndex(bytes) {
|
|
192
|
+
const j = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
193
|
+
if (j.schemaVersion !== 2 || !Array.isArray(j.manifests))
|
|
194
|
+
throw new Error('not an OCI image index');
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const m of j.manifests) {
|
|
197
|
+
if (typeof m?.digest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(m.digest) || typeof m.size !== 'number' || !Number.isSafeInteger(m.size) || m.size < 0)
|
|
198
|
+
continue;
|
|
199
|
+
let annotations;
|
|
200
|
+
if (typeof m.annotations === 'object' && m.annotations !== null && !Array.isArray(m.annotations)) {
|
|
201
|
+
annotations = {};
|
|
202
|
+
for (const [k, v] of Object.entries(m.annotations))
|
|
203
|
+
if (typeof v === 'string')
|
|
204
|
+
annotations[k] = v;
|
|
205
|
+
}
|
|
206
|
+
out.push({
|
|
207
|
+
mediaType: typeof m.mediaType === 'string' ? m.mediaType : MEDIA.manifest,
|
|
208
|
+
digest: m.digest,
|
|
209
|
+
size: m.size,
|
|
210
|
+
artifactType: typeof m.artifactType === 'string' ? m.artifactType : undefined,
|
|
211
|
+
annotations,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** The referrers fallback tag for a subject digest (`sha256-<hex>`, distribution spec v1.1). */
|
|
217
|
+
export function referrersTag(subject) {
|
|
218
|
+
return subject.replace(':', '-');
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=attest.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sitefile bake`: build several variants of a site from one bake file
|
|
3
|
+
* (`sitefile.bake.yaml`, `.yml` or `.json`), like `docker buildx bake`.
|
|
4
|
+
*
|
|
5
|
+
* The bake file names targets (each one a `sitefile build`: context, Sitefile,
|
|
6
|
+
* build args, arg files, `--target` stage, output, format, tags), optional
|
|
7
|
+
* `matrix` values that expand one target into several, `inherits` and groups.
|
|
8
|
+
* This module only reads and resolves the file into a {@link BakePlan}; the CLI
|
|
9
|
+
* (src/cli/bake.ts) runs the builds. No IO besides reading the bake file.
|
|
10
|
+
*/
|
|
11
|
+
/** The default bake file names, looked up in this order in the current directory. */
|
|
12
|
+
export declare const BAKE_FILES: readonly string[];
|
|
13
|
+
/** One build of a bake plan. Paths are as the CLI passes them to `build` (joined to the bake file's directory). */
|
|
14
|
+
export interface BakeTarget {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly context: string;
|
|
17
|
+
readonly file: string;
|
|
18
|
+
/** Build args in definition order (inherited ones first). */
|
|
19
|
+
readonly args: Readonly<Record<string, string>>;
|
|
20
|
+
readonly argFiles: readonly string[];
|
|
21
|
+
/** `--target <stage>`. */
|
|
22
|
+
readonly target: string | null;
|
|
23
|
+
readonly output: string;
|
|
24
|
+
readonly format: string | null;
|
|
25
|
+
readonly tags: readonly string[];
|
|
26
|
+
/** The matrix values of an expanded target. */
|
|
27
|
+
readonly matrix: Readonly<Record<string, string>> | null;
|
|
28
|
+
}
|
|
29
|
+
export interface BakePlan {
|
|
30
|
+
/** The bake file as given (or found). */
|
|
31
|
+
readonly file: string;
|
|
32
|
+
readonly targets: readonly BakeTarget[];
|
|
33
|
+
}
|
|
34
|
+
/** A target as written (after type checks), before inheritance. */
|
|
35
|
+
interface RawTarget {
|
|
36
|
+
context?: string;
|
|
37
|
+
file?: string;
|
|
38
|
+
args?: Record<string, string>;
|
|
39
|
+
'arg-files'?: string[];
|
|
40
|
+
target?: string;
|
|
41
|
+
output?: string;
|
|
42
|
+
format?: string;
|
|
43
|
+
tags?: string[];
|
|
44
|
+
matrix?: Array<[string, string[]]>;
|
|
45
|
+
inherits: string[];
|
|
46
|
+
}
|
|
47
|
+
interface Definition {
|
|
48
|
+
readonly label: string;
|
|
49
|
+
readonly targets: ReadonlyMap<string, RawTarget>;
|
|
50
|
+
readonly groups: ReadonlyMap<string, readonly string[]>;
|
|
51
|
+
}
|
|
52
|
+
/** Find the bake file: `file` when given, else the first of {@link BAKE_FILES} in the current directory. */
|
|
53
|
+
export declare function findBakeFile(file: string | undefined): Promise<string>;
|
|
54
|
+
/** Read and parse a bake file into its definition (no resolution yet). */
|
|
55
|
+
export declare function loadBakeFile(file: string): Promise<unknown>;
|
|
56
|
+
/** Check the shape of a parsed bake file. */
|
|
57
|
+
export declare function parseBakeDefinition(value: unknown, label: string): Definition;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a bake definition into the targets to build: `selected` names targets
|
|
60
|
+
* (a matrix target's name selects all its variants; a variant's own name selects
|
|
61
|
+
* it) and groups, in order, without duplicates. With none, the `default` group if
|
|
62
|
+
* there is one, else every target in file order. Unknown names are usage errors;
|
|
63
|
+
* targets whose outputs overlap, or that share a tag, are E_CONFLICT.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveBake(def: Definition, file: string, selected: readonly string[]): BakePlan;
|
|
66
|
+
/** Read, check and resolve a bake file. */
|
|
67
|
+
export declare function bakePlan(file: string, selected: readonly string[]): Promise<BakePlan>;
|
|
68
|
+
/** `--print --json`. */
|
|
69
|
+
export declare function bakePlanJson(plan: BakePlan): unknown;
|
|
70
|
+
/** `--print`: one block per target, then a count. */
|
|
71
|
+
export declare function bakePlanText(plan: BakePlan): string;
|
|
72
|
+
/** The `sitefile build` arguments of one target (before the CLI's pass-through flags). */
|
|
73
|
+
export declare function buildArgv(t: BakeTarget): string[];
|
|
74
|
+
export {};
|