@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.
- package/CHANGELOG.md +152 -0
- package/LICENSE +21 -0
- package/README.md +513 -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 +120 -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 +119 -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/network.d.ts +34 -0
- package/lib/src/cli/network.js +56 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +99 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +127 -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 +467 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +97 -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 +522 -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 +56 -0
- package/lib/src/errors.js +29 -0
- package/lib/src/executor.d.ts +393 -0
- package/lib/src/executor.js +2201 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +23 -0
- package/lib/src/export-formats.js +63 -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 +13 -0
- package/lib/src/index.js +13 -0
- package/lib/src/language/ast.d.ts +360 -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 +96 -0
- package/lib/src/plugins/cache.js +325 -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 +115 -0
- package/lib/src/registry.js +524 -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 +74 -0
- package/lib/src/resources/https.js +413 -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/network.d.ts +70 -0
- package/lib/src/resources/network.js +304 -0
- package/lib/src/resources/types.d.ts +69 -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 +123 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +85 -0
- package/lib/src/servers.js +225 -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 +26 -0
- package/lib/src/wbn.js +241 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +59 -4
package/lib/src/tree.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SitefileError } from './errors.js';
|
|
2
|
+
import { sha256 } from './digest.js';
|
|
3
|
+
import { foldCase, nfc } from './paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* A path and its ancestors, as [path, foldCase(path)] pairs from the
|
|
6
|
+
* shortest ancestor to the path itself. Folding is per segment, so prefix
|
|
7
|
+
* lengths never depend on how folding changes a string's length.
|
|
8
|
+
*/
|
|
9
|
+
function prefixes(path) {
|
|
10
|
+
const out = [];
|
|
11
|
+
let raw = '';
|
|
12
|
+
let folded = '';
|
|
13
|
+
for (const seg of path.slice(1).split('/')) {
|
|
14
|
+
raw += '/' + seg;
|
|
15
|
+
folded += '/' + foldCase(seg);
|
|
16
|
+
out.push([raw, folded]);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export function makeFile(bytes, origin, mode = 0o644) {
|
|
21
|
+
return { bytes, mode: mode & 0o111 ? 0o755 : 0o644, digest: sha256(bytes), origin };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Immutable snapshot of a filesystem tree. Keys are normalized absolute
|
|
25
|
+
* virtual paths of files; directories are implicit. Every operation returns
|
|
26
|
+
* a new Tree.
|
|
27
|
+
*/
|
|
28
|
+
export class Tree {
|
|
29
|
+
files;
|
|
30
|
+
dirs;
|
|
31
|
+
folded;
|
|
32
|
+
static empty = new Tree(new Map(), new Set(), new Map());
|
|
33
|
+
constructor(files, dirs,
|
|
34
|
+
/** foldCase(path) -> the one spelling of it in this tree (files and directories). */
|
|
35
|
+
folded) {
|
|
36
|
+
this.files = files;
|
|
37
|
+
this.dirs = dirs;
|
|
38
|
+
this.folded = folded;
|
|
39
|
+
}
|
|
40
|
+
get size() {
|
|
41
|
+
return this.files.size;
|
|
42
|
+
}
|
|
43
|
+
get(path) {
|
|
44
|
+
return this.files.get(path);
|
|
45
|
+
}
|
|
46
|
+
isFile(path) {
|
|
47
|
+
return this.files.has(path);
|
|
48
|
+
}
|
|
49
|
+
/** True for `/` and any implicit directory. */
|
|
50
|
+
isDirectory(path) {
|
|
51
|
+
return path === '/' || this.dirs.has(path);
|
|
52
|
+
}
|
|
53
|
+
/** Sorted [path, node] pairs. */
|
|
54
|
+
entries() {
|
|
55
|
+
return [...this.files.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add files. Paths are normalized to NFC. Writing identical content (digest
|
|
59
|
+
* and mode) is a no-op that keeps the first owner. Different content fails
|
|
60
|
+
* with E_CONFLICT unless the policy is 'overwrite'. A file can never replace
|
|
61
|
+
* a directory or vice versa. A path (or one of its directories) that equals
|
|
62
|
+
* an existing one under simple case folding but is spelled differently
|
|
63
|
+
* fails with E_PATH_COLLISION whatever the policy: case-insensitive file
|
|
64
|
+
* systems cannot hold both.
|
|
65
|
+
*/
|
|
66
|
+
withFiles(additions, policy, span) {
|
|
67
|
+
const files = new Map(this.files);
|
|
68
|
+
const dirs = new Set(this.dirs);
|
|
69
|
+
const folded = new Map(this.folded);
|
|
70
|
+
for (const [rawPath, node] of additions) {
|
|
71
|
+
const path = nfc(rawPath);
|
|
72
|
+
if (path === '/' || dirs.has(path)) {
|
|
73
|
+
throw new SitefileError('E_CONFLICT', `cannot write file '${path}': it is a directory`, span);
|
|
74
|
+
}
|
|
75
|
+
const chain = prefixes(path);
|
|
76
|
+
const ancestors = chain.slice(0, -1);
|
|
77
|
+
for (const [a] of ancestors) {
|
|
78
|
+
const f = files.get(a);
|
|
79
|
+
if (f) {
|
|
80
|
+
throw new SitefileError('E_CONFLICT', `cannot write '${path}': '${a}' is a file (written by ${f.origin})`, span);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [p, key] of chain) {
|
|
84
|
+
const prior = folded.get(key);
|
|
85
|
+
if (prior && prior.path !== p) {
|
|
86
|
+
const by = `${prior.dir ? 'created' : 'written'} by ${prior.origin}`;
|
|
87
|
+
throw new SitefileError('E_PATH_COLLISION', p === path
|
|
88
|
+
? `'${path}' differs only in case from '${prior.path}' (${by}); case-insensitive file systems cannot hold both`
|
|
89
|
+
: `cannot write '${path}': '${p}' differs only in case from '${prior.path}' (${by}); case-insensitive file systems cannot hold both`, span);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const existing = files.get(path);
|
|
93
|
+
if (existing) {
|
|
94
|
+
if (existing.digest === node.digest && existing.mode === node.mode)
|
|
95
|
+
continue;
|
|
96
|
+
if (policy === 'error') {
|
|
97
|
+
throw new SitefileError('E_CONFLICT', `'${path}' was already written with different content by ${existing.origin}; use --overwrite to replace it`, span);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
files.set(path, node);
|
|
101
|
+
folded.set(chain[chain.length - 1][1], { path, origin: node.origin, dir: false });
|
|
102
|
+
for (const [a, key] of ancestors) {
|
|
103
|
+
if (dirs.has(a))
|
|
104
|
+
continue;
|
|
105
|
+
dirs.add(a);
|
|
106
|
+
folded.set(key, { path: a, origin: node.origin, dir: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return new Tree(files, dirs, folded);
|
|
110
|
+
}
|
|
111
|
+
/** The files under a directory, re-rooted at `/`. */
|
|
112
|
+
subtree(dir) {
|
|
113
|
+
if (dir === '/')
|
|
114
|
+
return this;
|
|
115
|
+
const prefix = dir + '/';
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const [p, n] of this.files)
|
|
118
|
+
if (p.startsWith(prefix))
|
|
119
|
+
out.push([p.slice(dir.length), n]);
|
|
120
|
+
return Tree.empty.withFiles(out, 'error');
|
|
121
|
+
}
|
|
122
|
+
/** The files of this tree placed under `dir`. */
|
|
123
|
+
mountedAt(dir) {
|
|
124
|
+
return this.entries().map(([p, n]) => [dir === '/' ? p : dir + p, n]);
|
|
125
|
+
}
|
|
126
|
+
/** Content digest of the whole tree (paths, modes and file digests). */
|
|
127
|
+
digest() {
|
|
128
|
+
return sha256(this.entries().map(([p, n]) => `${p}\0${n.mode.toString(8)}\0${n.digest}\n`).join(''));
|
|
129
|
+
}
|
|
130
|
+
static fromFiles(files) {
|
|
131
|
+
return Tree.empty.withFiles(files, 'error');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SourceSpan } from './errors.js';
|
|
2
|
+
import type { Operand, ValueType } from './language/ast.js';
|
|
3
|
+
export type Scalar = string | number | boolean;
|
|
4
|
+
/** Convert a raw string to a typed scalar, or fail with E_TYPE. */
|
|
5
|
+
export declare function coerce(raw: string, type: ValueType, span: SourceSpan | undefined, what: string): Scalar;
|
|
6
|
+
/** Interpolate an instruction operand with ARG/LET bindings. */
|
|
7
|
+
export declare function evaluateOperand(op: Operand, env: ReadonlyMap<string, Scalar>): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SitefileError } from './errors.js';
|
|
2
|
+
const NUMBER_RE = /^-?(?:\d+)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
/** Convert a raw string to a typed scalar, or fail with E_TYPE. */
|
|
4
|
+
export function coerce(raw, type, span, what) {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case 'string':
|
|
7
|
+
return raw;
|
|
8
|
+
case 'number': {
|
|
9
|
+
if (!NUMBER_RE.test(raw.trim()))
|
|
10
|
+
throw new SitefileError('E_TYPE', `${what} expects a number, got '${raw}'`, span);
|
|
11
|
+
return Number(raw);
|
|
12
|
+
}
|
|
13
|
+
case 'boolean': {
|
|
14
|
+
if (raw === 'true')
|
|
15
|
+
return true;
|
|
16
|
+
if (raw === 'false')
|
|
17
|
+
return false;
|
|
18
|
+
throw new SitefileError('E_TYPE', `${what} expects true or false, got '${raw}'`, span);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Interpolate an instruction operand with ARG/LET bindings. */
|
|
23
|
+
export function evaluateOperand(op, env) {
|
|
24
|
+
let out = '';
|
|
25
|
+
for (const part of op.parts) {
|
|
26
|
+
if (part.kind === 'text') {
|
|
27
|
+
out += part.value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const v = env.get(part.name);
|
|
31
|
+
if (v === undefined)
|
|
32
|
+
throw new SitefileError('E_UNDEFINED', `undefined variable '${part.name}'`, part.span);
|
|
33
|
+
out += String(v);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=values.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type BuildOptions } from './executor.js';
|
|
2
|
+
export interface VendorResult {
|
|
3
|
+
/** Real path of the vendor directory written. */
|
|
4
|
+
readonly dir: string;
|
|
5
|
+
readonly resources: number;
|
|
6
|
+
readonly images: number;
|
|
7
|
+
readonly blobs: number;
|
|
8
|
+
/** Total size of the blobs. */
|
|
9
|
+
readonly bytes: number;
|
|
10
|
+
readonly lockfileWritten: boolean;
|
|
11
|
+
readonly warnings: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `sitefile vendor` (issue #96): fetch every remote IMPORT and the BASE image exactly as a build would
|
|
15
|
+
* (same network policy, INTEGRITY and lockfile checks, and Sitefile.lock is updated the same way), then
|
|
16
|
+
* replace the vendor directory (`options.vendorDir`, default `vendor/sitefile`) with their bytes under
|
|
17
|
+
* content-addressed names and a manifest. Always fetches: an existing vendor directory is not read.
|
|
18
|
+
*/
|
|
19
|
+
export declare function vendor(options: BuildOptions): Promise<VendorResult>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveBase } from './base-image.js';
|
|
4
|
+
import { sha256 } from './digest.js';
|
|
5
|
+
import { SitefileError } from './errors.js';
|
|
6
|
+
import { fetchGitImport, fetchRemoteImport, gitSelectionBytes, loadProgram, readImportedSitefile, rootSitefileSource, warnUnusedSecrets } from './executor.js';
|
|
7
|
+
import { redactError, redactSecrets } from './secrets.js';
|
|
8
|
+
import { parseManifest } from './oci.js';
|
|
9
|
+
import { parseRef, withDefaultRegistry } from './ref.js';
|
|
10
|
+
import { importedStageRefs, plan } from './planner.js';
|
|
11
|
+
import { Lockfile } from './resources/lockfile.js';
|
|
12
|
+
import { DEFAULT_FETCH_POLICY } from './resources/types.js';
|
|
13
|
+
import { DEFAULT_VENDOR_DIR, serializeVendorManifest, vendorDirRel, writeVendorDir } from './resources/vendor.js';
|
|
14
|
+
import { DEFAULT_ARCHIVE_LIMITS } from './resources/types.js';
|
|
15
|
+
import { Store } from './store.js';
|
|
16
|
+
/**
|
|
17
|
+
* `sitefile vendor` (issue #96): fetch every remote IMPORT and the BASE image exactly as a build would
|
|
18
|
+
* (same network policy, INTEGRITY and lockfile checks, and Sitefile.lock is updated the same way), then
|
|
19
|
+
* replace the vendor directory (`options.vendorDir`, default `vendor/sitefile`) with their bytes under
|
|
20
|
+
* content-addressed names and a manifest. Always fetches: an existing vendor directory is not read.
|
|
21
|
+
*/
|
|
22
|
+
export async function vendor(options) {
|
|
23
|
+
const secrets = options.secrets;
|
|
24
|
+
if (!secrets?.size)
|
|
25
|
+
return vendorUnredacted(options);
|
|
26
|
+
const onWarning = options.onWarning;
|
|
27
|
+
try {
|
|
28
|
+
const r = await vendorUnredacted({ ...options, ...(onWarning ? { onWarning: (m) => onWarning(redactSecrets(m, secrets)) } : {}) });
|
|
29
|
+
return { ...r, warnings: r.warnings.map((w) => redactSecrets(w, secrets)) };
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
throw redactError(e, secrets);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function vendorUnredacted(options) {
|
|
36
|
+
const { program, sitefilePath, label } = await loadProgram(options);
|
|
37
|
+
const p = plan(program, options.buildArgs ?? {});
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const warn = (m) => {
|
|
40
|
+
warnings.push(m);
|
|
41
|
+
options.onWarning?.(m);
|
|
42
|
+
};
|
|
43
|
+
p.warnings.forEach(warn);
|
|
44
|
+
const dir = vendorDirRel(options.vendorDir ?? DEFAULT_VENDOR_DIR);
|
|
45
|
+
const contextReal = await realpath(options.contextDir).catch((e) => {
|
|
46
|
+
throw new SitefileError('E_NOT_FOUND', `build context '${options.contextDir}' does not exist`, undefined, { cause: e });
|
|
47
|
+
});
|
|
48
|
+
const policy = { ...DEFAULT_FETCH_POLICY, ...options.fetchPolicy };
|
|
49
|
+
const lockPath = options.lockfile === false ? undefined : (options.lockfile ?? path.join(path.dirname(sitefilePath), 'Sitefile.lock'));
|
|
50
|
+
const lock = lockPath ? await Lockfile.load(lockPath) : undefined;
|
|
51
|
+
const frozen = !!options.frozenLockfile;
|
|
52
|
+
const update = !!options.updateLock;
|
|
53
|
+
warnUnusedSecrets(p, options.secrets, warn);
|
|
54
|
+
const env = { policy, lock, label, warn, frozen, update, archiveLimits: { ...DEFAULT_ARCHIVE_LIMITS, ...options.archiveLimits }, ...(options.secrets ? { secrets: options.secrets } : {}) };
|
|
55
|
+
const blobs = new Map();
|
|
56
|
+
const resources = {};
|
|
57
|
+
const git = {};
|
|
58
|
+
/** A Git import's content, vendored (a Sitefile imported from Git is read from it too). */
|
|
59
|
+
const vendorGit = async (spec, loc, e) => {
|
|
60
|
+
const got = await fetchGitImport(spec, loc, e);
|
|
61
|
+
const bytes = gitSelectionBytes(got.selection);
|
|
62
|
+
const digest = sha256(bytes);
|
|
63
|
+
blobs.set(digest, bytes);
|
|
64
|
+
git[loc.url] = { commit: got.commit, kind: got.selection.kind, integrity: digest, size: bytes.byteLength, ...(got.selection.kind === 'file' ? { mode: got.selection.mode === 0o755 ? '755' : '644' } : {}) };
|
|
65
|
+
return got;
|
|
66
|
+
};
|
|
67
|
+
/** Every remote input of `sp` (a Sitefile read from `source`), then those of the Sitefiles it imports AS sitefile. */
|
|
68
|
+
const vendorImports = async (sp, source) => {
|
|
69
|
+
const e = { ...env, label: source.label };
|
|
70
|
+
for (const spec of sp.imports) {
|
|
71
|
+
if (spec.as === 'sitefile') {
|
|
72
|
+
const sub = await readImportedSitefile(spec, source, importedStageRefs(sp.stages), (s, loc) => vendorGit(s, loc, e));
|
|
73
|
+
sub.plan.warnings.forEach(warn);
|
|
74
|
+
await vendorImports(sub.plan, sub.source);
|
|
75
|
+
nestedImages.push(...sub.plan.stages);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (spec.location.kind === 'git') {
|
|
79
|
+
await vendorGit(spec, spec.location, e);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (spec.location.kind !== 'remote')
|
|
83
|
+
continue;
|
|
84
|
+
const url = spec.location.url;
|
|
85
|
+
const { bytes, finalUrl } = await fetchRemoteImport(spec, url, e);
|
|
86
|
+
const digest = sha256(bytes);
|
|
87
|
+
blobs.set(digest, bytes);
|
|
88
|
+
resources[url] = { integrity: digest, size: bytes.byteLength, resolved: finalUrl };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** The stages of imported Sitefiles: their registry FROM images are vendored after this Sitefile's. */
|
|
92
|
+
const nestedImages = [];
|
|
93
|
+
await vendorImports(p, await rootSitefileSource(options, label, sitefilePath));
|
|
94
|
+
const images = {};
|
|
95
|
+
const platform = options.platform ?? 'linux/amd64';
|
|
96
|
+
let store;
|
|
97
|
+
/** Resolve an image as a build would and vendor what an offline pull of it needs. */
|
|
98
|
+
const vendorImage = async (input, lead, enforcePlatform, span) => {
|
|
99
|
+
store ??= options.store ?? (await Store.open());
|
|
100
|
+
const s = store;
|
|
101
|
+
const base = await resolveBase(input, {
|
|
102
|
+
store: s,
|
|
103
|
+
platform,
|
|
104
|
+
lock,
|
|
105
|
+
frozen,
|
|
106
|
+
update,
|
|
107
|
+
registry: { ...options.registry, allowPrivateNetwork: options.registry?.allowPrivateNetwork ?? policy.allowPrivateNetwork, network: options.registry?.network ?? policy.network },
|
|
108
|
+
warn,
|
|
109
|
+
lead,
|
|
110
|
+
enforcePlatform,
|
|
111
|
+
...(span ? { span } : {}),
|
|
112
|
+
});
|
|
113
|
+
// resolveBase has validated the reference; this is its lockfile key.
|
|
114
|
+
const ref = withDefaultRegistry(parseRef(input));
|
|
115
|
+
const registry = ref.registry;
|
|
116
|
+
const lockKey = `${registry}/${ref.name}:${ref.tag ?? 'latest'}`;
|
|
117
|
+
const root = ref.digest ?? lock?.getImage(lockKey)?.digest ?? base.manifestDigest;
|
|
118
|
+
const add = async (digest, size) => {
|
|
119
|
+
const bytes = await s.readBlob(digest, size);
|
|
120
|
+
blobs.set(digest, bytes);
|
|
121
|
+
return bytes;
|
|
122
|
+
};
|
|
123
|
+
await add(root);
|
|
124
|
+
const m = parseManifest(await add(base.manifestDigest));
|
|
125
|
+
for (const d of [m.config, ...m.layers])
|
|
126
|
+
await add(d.digest, d.size);
|
|
127
|
+
const key = ref.digest !== undefined ? `${registry}/${ref.name}${ref.tag !== undefined ? `:${ref.tag}` : ''}@${ref.digest}` : lockKey;
|
|
128
|
+
images[key] = { digest: root, platform, manifest: base.manifestDigest };
|
|
129
|
+
};
|
|
130
|
+
// FROM image stage bases with a registry (local-store images are not vendored), then the BASE.
|
|
131
|
+
for (const st of [...p.stages, ...nestedImages])
|
|
132
|
+
if (st.base.kind === 'image' && !st.base.local)
|
|
133
|
+
await vendorImage(st.base.ref, `${st.base.lead} image`, false, st.base.span);
|
|
134
|
+
const baseInput = options.baseImage ?? (p.exportFormat === 'oci' ? p.exportBase : undefined);
|
|
135
|
+
if (baseInput !== undefined)
|
|
136
|
+
await vendorImage(baseInput, 'BASE', true);
|
|
137
|
+
const text = serializeVendorManifest({ version: 1, resources, images, git });
|
|
138
|
+
const written = await writeVendorDir(contextReal, dir, text, blobs);
|
|
139
|
+
const lockfileWritten = lock && !frozen ? await lock.save() : false;
|
|
140
|
+
let bytes = 0;
|
|
141
|
+
for (const b of blobs.values())
|
|
142
|
+
bytes += b.byteLength;
|
|
143
|
+
return { dir: written, resources: Object.keys(resources).length + Object.keys(git).length, images: Object.keys(images).length, blobs: blobs.size, bytes, lockfileWritten, warnings };
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=vendor.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Quiet period after the last change before a rebuild starts. */
|
|
2
|
+
export declare const DEBOUNCE_MS = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Runs `build` once changes settle (no new change for `debounceMs`). Changes that
|
|
5
|
+
* arrive while a build runs are collected and coalesced into one follow-up build.
|
|
6
|
+
* `build` receives the paths reported since the previous build started.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RebuildScheduler {
|
|
9
|
+
private readonly build;
|
|
10
|
+
private readonly debounceMs;
|
|
11
|
+
private timer;
|
|
12
|
+
private running;
|
|
13
|
+
private changes;
|
|
14
|
+
private closed;
|
|
15
|
+
constructor(build: (changed: readonly string[]) => Promise<void>, debounceMs?: number);
|
|
16
|
+
/** Record a change; (re)starts the debounce unless a build is running. */
|
|
17
|
+
notify(changed: string): void;
|
|
18
|
+
/** Build now (the initial build), then any follow-up. Resolves when idle. */
|
|
19
|
+
run(): Promise<void>;
|
|
20
|
+
/** True while a build runs or one is scheduled. */
|
|
21
|
+
get busy(): boolean;
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
|
24
|
+
/** `p` is `dir` or inside it. */
|
|
25
|
+
export declare function isWithin(p: string, dir: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Paths a watcher never reacts to: anything under `.git`, the exporter's temp and
|
|
28
|
+
* backup siblings (`.<name>.sitefile-tmp-*`, `.<name>.sitefile-old-*`), and any of
|
|
29
|
+
* `ignored` (the output, the store, Sitefile.lock), checked at call time so the
|
|
30
|
+
* set may grow between builds.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isIgnoredChange(abs: string, roots: readonly string[], ignored: Iterable<string>): boolean;
|
|
33
|
+
/** realpath of `p`, else of its parent joined with its name, else `p` resolved. */
|
|
34
|
+
export declare function resolveReal(p: string): Promise<string>;
|
|
35
|
+
export interface WatchSpec {
|
|
36
|
+
/** Directories watched recursively (the build context). Absolute, real paths. */
|
|
37
|
+
readonly dirs: readonly string[];
|
|
38
|
+
/** Single files (the Sitefile, --build-arg-file files) outside `dirs`; their directory is watched for their name. */
|
|
39
|
+
readonly files: readonly string[];
|
|
40
|
+
/** Called with the absolute path of each change (the directory itself when the OS gives no name). */
|
|
41
|
+
readonly onChange: (abs: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/** Start fs.watch watchers for `spec`; returns a function that closes them. */
|
|
44
|
+
export declare function watchPaths(spec: WatchSpec): () => void;
|
package/lib/src/watch.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { watch as fsWatch, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { TEMP_PREFIX_PATTERN } from './exporter.js';
|
|
4
|
+
/** Quiet period after the last change before a rebuild starts. */
|
|
5
|
+
export const DEBOUNCE_MS = 100;
|
|
6
|
+
/**
|
|
7
|
+
* Runs `build` once changes settle (no new change for `debounceMs`). Changes that
|
|
8
|
+
* arrive while a build runs are collected and coalesced into one follow-up build.
|
|
9
|
+
* `build` receives the paths reported since the previous build started.
|
|
10
|
+
*/
|
|
11
|
+
export class RebuildScheduler {
|
|
12
|
+
build;
|
|
13
|
+
debounceMs;
|
|
14
|
+
timer;
|
|
15
|
+
running;
|
|
16
|
+
changes = new Set();
|
|
17
|
+
closed = false;
|
|
18
|
+
constructor(build, debounceMs = DEBOUNCE_MS) {
|
|
19
|
+
this.build = build;
|
|
20
|
+
this.debounceMs = debounceMs;
|
|
21
|
+
}
|
|
22
|
+
/** Record a change; (re)starts the debounce unless a build is running. */
|
|
23
|
+
notify(changed) {
|
|
24
|
+
if (this.closed)
|
|
25
|
+
return;
|
|
26
|
+
this.changes.add(changed);
|
|
27
|
+
if (this.running)
|
|
28
|
+
return; // picked up when the running build finishes
|
|
29
|
+
clearTimeout(this.timer);
|
|
30
|
+
this.timer = setTimeout(() => void this.run(), this.debounceMs);
|
|
31
|
+
}
|
|
32
|
+
/** Build now (the initial build), then any follow-up. Resolves when idle. */
|
|
33
|
+
run() {
|
|
34
|
+
if (this.running)
|
|
35
|
+
return this.running;
|
|
36
|
+
clearTimeout(this.timer);
|
|
37
|
+
this.timer = undefined;
|
|
38
|
+
const batch = [...this.changes];
|
|
39
|
+
this.changes.clear();
|
|
40
|
+
this.running = this.build(batch)
|
|
41
|
+
.catch((e) => void process.stderr.write(`internal error: ${e?.stack ?? String(e)}\n`))
|
|
42
|
+
.finally(() => {
|
|
43
|
+
this.running = undefined;
|
|
44
|
+
if (this.changes.size && !this.closed) {
|
|
45
|
+
clearTimeout(this.timer);
|
|
46
|
+
this.timer = setTimeout(() => void this.run(), this.debounceMs);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return this.running;
|
|
50
|
+
}
|
|
51
|
+
/** True while a build runs or one is scheduled. */
|
|
52
|
+
get busy() {
|
|
53
|
+
return this.running !== undefined || this.timer !== undefined;
|
|
54
|
+
}
|
|
55
|
+
close() {
|
|
56
|
+
this.closed = true;
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** `p` is `dir` or inside it. */
|
|
62
|
+
export function isWithin(p, dir) {
|
|
63
|
+
const rel = path.relative(dir, p);
|
|
64
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Paths a watcher never reacts to: anything under `.git`, the exporter's temp and
|
|
68
|
+
* backup siblings (`.<name>.sitefile-tmp-*`, `.<name>.sitefile-old-*`), and any of
|
|
69
|
+
* `ignored` (the output, the store, Sitefile.lock), checked at call time so the
|
|
70
|
+
* set may grow between builds.
|
|
71
|
+
*/
|
|
72
|
+
export function isIgnoredChange(abs, roots, ignored) {
|
|
73
|
+
for (const q of ignored)
|
|
74
|
+
if (isWithin(abs, q))
|
|
75
|
+
return true;
|
|
76
|
+
const root = roots.find((r) => isWithin(abs, r));
|
|
77
|
+
if (root === undefined)
|
|
78
|
+
return false;
|
|
79
|
+
return path
|
|
80
|
+
.relative(root, abs)
|
|
81
|
+
.split(path.sep)
|
|
82
|
+
.some((seg) => seg === '.git' || TEMP_PREFIX_PATTERN.test(seg));
|
|
83
|
+
}
|
|
84
|
+
/** realpath of `p`, else of its parent joined with its name, else `p` resolved. */
|
|
85
|
+
export async function resolveReal(p) {
|
|
86
|
+
const abs = path.resolve(p);
|
|
87
|
+
try {
|
|
88
|
+
return await fs.realpath(abs);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
try {
|
|
92
|
+
return path.join(await fs.realpath(path.dirname(abs)), path.basename(abs));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return abs;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Start fs.watch watchers for `spec`; returns a function that closes them. */
|
|
100
|
+
export function watchPaths(spec) {
|
|
101
|
+
const watchers = [];
|
|
102
|
+
const add = (dir, recursive, names) => {
|
|
103
|
+
const w = fsWatch(dir, { recursive, persistent: true }, (_event, filename) => {
|
|
104
|
+
const name = filename == null ? undefined : filename.toString();
|
|
105
|
+
if (names && (name === undefined || !names.has(path.basename(name))))
|
|
106
|
+
return;
|
|
107
|
+
spec.onChange(name === undefined ? dir : path.join(dir, name));
|
|
108
|
+
});
|
|
109
|
+
// A watched directory that disappears (or a platform error) must not crash the build loop.
|
|
110
|
+
w.on('error', () => undefined);
|
|
111
|
+
watchers.push(w);
|
|
112
|
+
};
|
|
113
|
+
for (const d of spec.dirs)
|
|
114
|
+
add(d, true);
|
|
115
|
+
const byDir = new Map();
|
|
116
|
+
for (const f of spec.files) {
|
|
117
|
+
if (spec.dirs.some((d) => isWithin(f, d)))
|
|
118
|
+
continue;
|
|
119
|
+
const dir = path.dirname(f);
|
|
120
|
+
const set = byDir.get(dir) ?? new Set();
|
|
121
|
+
set.add(path.basename(f));
|
|
122
|
+
byDir.set(dir, set);
|
|
123
|
+
}
|
|
124
|
+
for (const [dir, names] of byDir) {
|
|
125
|
+
try {
|
|
126
|
+
add(dir, false, names);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// the directory of a missing file: the build reports the missing file
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return () => {
|
|
133
|
+
for (const w of watchers)
|
|
134
|
+
w.close();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=watch.js.map
|
package/lib/src/wbn.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Tree } from './tree.js';
|
|
2
|
+
export declare function mimeTypeFor(path: string): string;
|
|
3
|
+
/** An absolute https URL ending in `/`, without credentials, query or fragment. Returns its normalized form. */
|
|
4
|
+
export declare function validateBaseUrl(raw: string | undefined): string;
|
|
5
|
+
export declare function writeWebBundle(tree: Tree, baseUrl: string | undefined): Uint8Array;
|
|
6
|
+
/**
|
|
7
|
+
* The responses section and its URLs (tree paths, percent-encoded by the caller), as `sitefile
|
|
8
|
+
* serve` would answer them:
|
|
9
|
+
*
|
|
10
|
+
* - Each file is a 200 with `content-type` and the `/_headers` blocks whose pattern matches its
|
|
11
|
+
* URL path (`headersFor`; a `content-type` there replaces the inferred one). A directory URL
|
|
12
|
+
* (`/docs/` for `/docs/index.html`) matches with its trailing slash, so it gets its own
|
|
13
|
+
* response when its headers differ from the file URL's.
|
|
14
|
+
* - An exact-path `/_redirects` rule with a 3xx status becomes a response with that `:status`, a
|
|
15
|
+
* `location` of the rule's target as written, and an empty body: at its path when no file
|
|
16
|
+
* would be served there, and over the file when forced (`301!`). Like `serve`, the first rule
|
|
17
|
+
* matching the path decides; when that is a splat or a 200/404 rewrite, the bundle leaves the
|
|
18
|
+
* path to `/_redirects`. Splat rules and rewrites are not baked in.
|
|
19
|
+
*
|
|
20
|
+
* `/_headers` and `/_redirects` stay in the bundle as files. Responses are deduplicated (both URLs
|
|
21
|
+
* of an index page share one when their headers agree; identical redirects share one).
|
|
22
|
+
*/
|
|
23
|
+
export declare function bundleResponses(tree: Tree): {
|
|
24
|
+
responses: Uint8Array[];
|
|
25
|
+
urls: Array<[string, number]>;
|
|
26
|
+
};
|