@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,410 @@
|
|
|
1
|
+
import { SitefileError } from './errors.js';
|
|
2
|
+
import { importedStage, isFragment, isImageBase, isInline, stageNames } from './language/ast.js';
|
|
3
|
+
import { normalizeLocal, normalizeVirtual } from './paths.js';
|
|
4
|
+
import { parseRef } from './ref.js';
|
|
5
|
+
import { checkPattern } from './fingerprint.js';
|
|
6
|
+
import { checkBaseUrl, checkHeaderName, checkHeaderValue, checkRedirectTarget, checkSitePath } from './site.js';
|
|
7
|
+
import { coerce, evaluateOperand } from './values.js';
|
|
8
|
+
import { isGitUri, parseGitUri } from './resources/git.js';
|
|
9
|
+
import { parseFragmentBody } from './language/validate.js';
|
|
10
|
+
const URL_RE = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
|
|
11
|
+
const IMPORT_REF_RE = /^([A-Za-z_][A-Za-z0-9_]*):(.*)$/s;
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate a program. `argSource` names where a build arg came from in messages (`--build-arg NAME` for the
|
|
14
|
+
* CLI's; an imported Sitefile's come from the `ARG` clauses of its IMPORT).
|
|
15
|
+
*/
|
|
16
|
+
export function plan(program, buildArgs = {}, argSource = (n) => `--build-arg ${n}`) {
|
|
17
|
+
const vars = new Map();
|
|
18
|
+
const fragments = new Map();
|
|
19
|
+
const imports = new Map();
|
|
20
|
+
const stages = [];
|
|
21
|
+
const plugins = [];
|
|
22
|
+
const tools = [];
|
|
23
|
+
const redirects = [];
|
|
24
|
+
const headers = [];
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const declaredArgs = new Set();
|
|
27
|
+
const allStageNames = stageNames(program.instructions);
|
|
28
|
+
// validate() guarantees a FROM precedes every operation.
|
|
29
|
+
const operations = () => stages[stages.length - 1].operations;
|
|
30
|
+
let exportPath;
|
|
31
|
+
let exportFormat = 'directory';
|
|
32
|
+
let exportBase;
|
|
33
|
+
let exportRoot;
|
|
34
|
+
let exportCompression;
|
|
35
|
+
const bind = (name, raw, type, span, what) => {
|
|
36
|
+
vars.set(name, coerce(raw, type, span, what));
|
|
37
|
+
};
|
|
38
|
+
for (const ins of program.instructions) {
|
|
39
|
+
switch (ins.kind) {
|
|
40
|
+
case 'FROM':
|
|
41
|
+
stages.push({
|
|
42
|
+
...(ins.alias !== undefined && { name: ins.alias }),
|
|
43
|
+
base: ins.base === 'scratch'
|
|
44
|
+
? { kind: 'scratch' }
|
|
45
|
+
: isImageBase(ins.base)
|
|
46
|
+
? imageBase(ins.base, vars, ins.keyword === 'STAGE' ? `STAGE ${ins.alias} FROM` : 'FROM')
|
|
47
|
+
: stageBase(ins.base, vars, stages, allStageNames, imports, ins.keyword === 'STAGE' ? `STAGE ${ins.alias} FROM` : 'FROM'),
|
|
48
|
+
operations: [],
|
|
49
|
+
span: ins.span,
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
case 'ARG': {
|
|
53
|
+
declaredArgs.add(ins.name);
|
|
54
|
+
const given = buildArgs[ins.name];
|
|
55
|
+
if (given !== undefined)
|
|
56
|
+
bind(ins.name, given, ins.type, ins.span, argSource(ins.name));
|
|
57
|
+
else if (ins.defaultValue)
|
|
58
|
+
bind(ins.name, evaluateOperand(ins.defaultValue, vars), ins.type, ins.defaultValue.span, `ARG ${ins.name}`);
|
|
59
|
+
else
|
|
60
|
+
throw new SitefileError('E_MISSING_ARG', `ARG ${ins.name} has no default; pass --build-arg ${ins.name}=<value>`, ins.span);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'LET':
|
|
64
|
+
if (isFragment(ins)) {
|
|
65
|
+
fragments.set(ins.name, { name: ins.name, text: ins.body.text, nodes: parseFragmentBody(ins.body, program.file), span: ins.span });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
bind(ins.name, evaluateOperand(ins.value, vars), ins.type, ins.value.span, `LET ${ins.name}`);
|
|
69
|
+
break;
|
|
70
|
+
case 'IMPORT': {
|
|
71
|
+
const uri = evaluateOperand(ins.uri, vars);
|
|
72
|
+
const spec = {
|
|
73
|
+
name: ins.name,
|
|
74
|
+
uri,
|
|
75
|
+
location: classifyUri(uri, ins.uri.span),
|
|
76
|
+
...(ins.as ? { as: ins.as } : {}),
|
|
77
|
+
...(ins.integrity ? { integrity: ins.integrity } : {}),
|
|
78
|
+
...(ins.sort ? { sort: { key: ins.sort.key, descending: ins.sort.descending } } : {}),
|
|
79
|
+
...(ins.schema ? { schema: schemaSpec(ins.name, ins.schema.path, vars, ins.schema.span) } : {}),
|
|
80
|
+
...(ins.drafts ? { drafts: draftsValue(ins.name, ins.drafts.value, vars) } : {}),
|
|
81
|
+
...(ins.secrets ? { secrets: ins.secrets.map((s) => ({ id: s.id, header: s.header, bearer: s.bearer })) } : {}),
|
|
82
|
+
...(ins.args ? { args: ins.args.map((a) => ({ name: a.name, value: evaluateOperand(a.value, vars), span: a.span })) } : {}),
|
|
83
|
+
span: ins.span,
|
|
84
|
+
};
|
|
85
|
+
if (spec.as === 'sitefile' && spec.location.kind === 'remote') {
|
|
86
|
+
throw new SitefileError('E_PROTOCOL', `IMPORT ${ins.name}: AS sitefile reads a context directory or a git+https:// repository, not a URL`, ins.uri.span);
|
|
87
|
+
}
|
|
88
|
+
if (spec.location.kind === 'local' && ins.secrets) {
|
|
89
|
+
throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: SECRET applies to remote imports (https:// or git+https://), not local paths`, ins.secrets[0].span);
|
|
90
|
+
}
|
|
91
|
+
if (spec.location.kind === 'git' && spec.integrity) {
|
|
92
|
+
throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: INTEGRITY does not apply to Git imports; pin a commit instead (#<40 hex>[:path])`, ins.span);
|
|
93
|
+
}
|
|
94
|
+
if (spec.location.kind === 'local' && spec.integrity) {
|
|
95
|
+
warnings.push(`${program.file}:${ins.span.line}: INTEGRITY on a local import is checked but not recorded in the lockfile`);
|
|
96
|
+
}
|
|
97
|
+
imports.set(ins.name, spec);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'COPY': {
|
|
101
|
+
const dest = normalizeVirtual(evaluateOperand(ins.dest, vars), ins.dest.span, 'COPY destination');
|
|
102
|
+
if (isInline(ins.source) && dest.trailingSlash) {
|
|
103
|
+
throw new SitefileError('E_PATH', `COPY of an inline body needs a file destination, not a directory`, ins.dest.span);
|
|
104
|
+
}
|
|
105
|
+
operations().push({
|
|
106
|
+
kind: 'COPY',
|
|
107
|
+
source: sourceRef(ins.source, vars, imports, ins.from),
|
|
108
|
+
dest: dest.path,
|
|
109
|
+
destIsDir: dest.trailingSlash,
|
|
110
|
+
overwrite: ins.overwrite,
|
|
111
|
+
...(ins.exclude ? { exclude: ins.exclude } : {}),
|
|
112
|
+
...(ins.chmod !== undefined ? { chmod: ins.chmod } : {}),
|
|
113
|
+
...(ins.parents ? { parents: true } : {}),
|
|
114
|
+
span: ins.span,
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'RENDER': {
|
|
119
|
+
if (ins.each) {
|
|
120
|
+
operations().push({
|
|
121
|
+
kind: 'RENDER',
|
|
122
|
+
template: sourceRef(ins.template, vars, imports, ins.from),
|
|
123
|
+
dest: '',
|
|
124
|
+
each: {
|
|
125
|
+
as: ins.each.as,
|
|
126
|
+
list: ins.each.list,
|
|
127
|
+
dest: ins.dest,
|
|
128
|
+
...(ins.each.by ? { by: ins.each.by.key } : {}),
|
|
129
|
+
...(ins.each.page
|
|
130
|
+
? { page: { size: pageSize(ins.each.page.size, vars), ...(ins.each.page.first ? { first: ins.each.page.first } : {}) } }
|
|
131
|
+
: {}),
|
|
132
|
+
},
|
|
133
|
+
escape: ins.escape,
|
|
134
|
+
overwrite: ins.overwrite,
|
|
135
|
+
...(ins.inlineSvg ? { inlineSvg: true } : {}),
|
|
136
|
+
span: ins.span,
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const raw = evaluateOperand(ins.dest, vars);
|
|
141
|
+
const dest = normalizeVirtual(raw, ins.dest.span, 'RENDER destination');
|
|
142
|
+
if (dest.trailingSlash) {
|
|
143
|
+
throw new SitefileError('E_PATH', `RENDER destination '${raw}' must be a file path, not a directory`, ins.dest.span);
|
|
144
|
+
}
|
|
145
|
+
operations().push({
|
|
146
|
+
kind: 'RENDER',
|
|
147
|
+
template: sourceRef(ins.template, vars, imports, ins.from),
|
|
148
|
+
dest: dest.path,
|
|
149
|
+
escape: ins.escape,
|
|
150
|
+
overwrite: ins.overwrite,
|
|
151
|
+
...(ins.inlineSvg ? { inlineSvg: true } : {}),
|
|
152
|
+
span: ins.span,
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'PLUGIN': {
|
|
157
|
+
const raw = evaluateOperand(ins.uri, vars);
|
|
158
|
+
if (URL_RE.test(raw)) {
|
|
159
|
+
throw new SitefileError('E_PROTOCOL', `PLUGIN ${ins.name}: plugins must be local files in the build context, not URLs`, ins.uri.span);
|
|
160
|
+
}
|
|
161
|
+
const rel = normalizeLocal(raw, ins.uri.span).path;
|
|
162
|
+
if (!/\.(mjs|wasm)$/.test(rel)) {
|
|
163
|
+
throw new SitefileError('E_VALIDATION', `PLUGIN ${ins.name}: '${raw}' must be an ES module (.mjs) or a WebAssembly module (.wasm)`, ins.uri.span);
|
|
164
|
+
}
|
|
165
|
+
plugins.push({ name: ins.name, path: rel, ...(ins.integrity ? { integrity: ins.integrity } : {}), span: ins.span });
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'TOOL':
|
|
169
|
+
tools.push({ name: ins.name, span: ins.span });
|
|
170
|
+
break;
|
|
171
|
+
case 'TRANSFORM':
|
|
172
|
+
operations().push({
|
|
173
|
+
kind: 'TRANSFORM',
|
|
174
|
+
...(ins.source ? { source: sourceRef(ins.source, vars, imports, ins.from) } : {}),
|
|
175
|
+
plugin: ins.plugin,
|
|
176
|
+
dest: ins.dest,
|
|
177
|
+
options: ins.options.map((o) => ({ key: o.key, value: o.value })),
|
|
178
|
+
...(ins.each ? { each: { as: ins.each.as, list: ins.each.list } } : {}),
|
|
179
|
+
...(ins.args ? { args: ins.args } : {}),
|
|
180
|
+
...(ins.include ? { include: ins.include } : {}),
|
|
181
|
+
...(ins.exclude ? { exclude: ins.exclude } : {}),
|
|
182
|
+
overwrite: ins.overwrite,
|
|
183
|
+
span: ins.span,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
case 'SITEMAP': {
|
|
187
|
+
const base = checkBaseUrl(evaluateOperand(ins.base, vars), 'SITEMAP BASE', ins.base.span);
|
|
188
|
+
operations().push({ kind: 'SITEMAP', base, dest: fileDest(ins.dest, vars, '/sitemap.xml', 'SITEMAP'), overwrite: ins.overwrite, span: ins.span });
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'FINGERPRINT': {
|
|
192
|
+
const patterns = ins.patterns.map((o) => checkPattern(evaluateOperand(o, vars), o.span));
|
|
193
|
+
const manifest = ins.manifest ? fileDest(ins.manifest, vars, '', 'FINGERPRINT MANIFEST') : undefined;
|
|
194
|
+
// `root` is the EXPORT path, filled in below (EXPORT is the last instruction).
|
|
195
|
+
operations().push({ kind: 'FINGERPRINT', patterns, ...(manifest !== undefined ? { manifest } : {}), root: '/', overwrite: ins.overwrite, span: ins.span });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'CHECKLINKS':
|
|
199
|
+
// `root` and `redirects` are filled in below, once every line is planned.
|
|
200
|
+
operations().push({ kind: 'CHECKLINKS', warn: ins.warn, external: ins.external, root: '/', redirects: [], span: ins.span });
|
|
201
|
+
break;
|
|
202
|
+
case 'FEED': {
|
|
203
|
+
const base = checkBaseUrl(evaluateOperand(ins.base, vars), 'FEED BASE', ins.base.span);
|
|
204
|
+
operations().push({
|
|
205
|
+
kind: 'FEED',
|
|
206
|
+
list: ins.list,
|
|
207
|
+
base,
|
|
208
|
+
link: ins.link,
|
|
209
|
+
dest: fileDest(ins.dest, vars, '/feed.xml', 'FEED'),
|
|
210
|
+
title: ins.title ? evaluateOperand(ins.title, vars) : base,
|
|
211
|
+
overwrite: ins.overwrite,
|
|
212
|
+
span: ins.span,
|
|
213
|
+
});
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'REDIRECT':
|
|
217
|
+
redirects.push({
|
|
218
|
+
from: checkSitePath(evaluateOperand(ins.from, vars), 'REDIRECT source', ins.from.span),
|
|
219
|
+
to: checkRedirectTarget(evaluateOperand(ins.to, vars), ins.to.span),
|
|
220
|
+
status: ins.status,
|
|
221
|
+
overwrite: ins.overwrite,
|
|
222
|
+
span: ins.span,
|
|
223
|
+
});
|
|
224
|
+
break;
|
|
225
|
+
case 'HEADER': {
|
|
226
|
+
const name = checkHeaderName(evaluateOperand(ins.name, vars), ins.name.span);
|
|
227
|
+
headers.push({
|
|
228
|
+
pattern: checkSitePath(evaluateOperand(ins.pattern, vars), 'HEADER path pattern', ins.pattern.span),
|
|
229
|
+
name,
|
|
230
|
+
value: checkHeaderValue(ins.value.map((v) => evaluateOperand(v, vars)).join(' '), name, ins.value[0].span),
|
|
231
|
+
overwrite: ins.overwrite,
|
|
232
|
+
span: ins.span,
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case 'EXPORT':
|
|
237
|
+
exportPath = normalizeVirtual(evaluateOperand(ins.path, vars), ins.path.span, 'EXPORT path').path;
|
|
238
|
+
exportFormat = ins.format;
|
|
239
|
+
exportBase = ins.base ? evaluateOperand(ins.base, vars) : undefined;
|
|
240
|
+
if (ins.at && ins.format !== 'oci')
|
|
241
|
+
throw new SitefileError('E_VALIDATION', 'EXPORT ... AT applies only to AS oci', ins.at.span);
|
|
242
|
+
exportRoot = ins.at ? normalizeVirtual(evaluateOperand(ins.at, vars), ins.at.span, 'EXPORT AT path').path : undefined;
|
|
243
|
+
if (ins.compression && ins.format !== 'oci')
|
|
244
|
+
throw new SitefileError('E_VALIDATION', 'EXPORT ... COMPRESSION applies only to AS oci', ins.compression.span);
|
|
245
|
+
exportCompression = ins.compression?.value;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const st of stages) {
|
|
250
|
+
st.operations.forEach((op, i) => {
|
|
251
|
+
if (op.kind === 'FINGERPRINT')
|
|
252
|
+
st.operations[i] = { ...op, root: exportPath ?? '/' };
|
|
253
|
+
if (op.kind === 'CHECKLINKS')
|
|
254
|
+
st.operations[i] = { ...op, root: exportPath ?? '/', redirects: redirects.map((r) => r.from) };
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// An unnamed stage can be neither a FROM base nor a COPY --from source; only the last one is useful (EXPORT reads it).
|
|
258
|
+
for (const st of stages.slice(0, -1)) {
|
|
259
|
+
if (st.name === undefined) {
|
|
260
|
+
warnings.push(`${program.file}:${st.span.line}: unnamed stage is not the last one, so its output can never be used; name it with FROM … AS <name>`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const name of Object.keys(buildArgs)) {
|
|
264
|
+
if (!declaredArgs.has(name))
|
|
265
|
+
warnings.push(`--build-arg ${name} does not match any ARG and was ignored`);
|
|
266
|
+
}
|
|
267
|
+
if (stages.length === 0 || exportPath === undefined) {
|
|
268
|
+
// validate() guarantees both; this keeps the types honest.
|
|
269
|
+
throw new SitefileError('E_VALIDATION', 'a Sitefile needs FROM and EXPORT');
|
|
270
|
+
}
|
|
271
|
+
return { file: program.file, stages, vars, fragments, imports: [...imports.values()], plugins, tools, exportPath, exportFormat, exportBase, exportRoot, exportCompression, redirects, headers, warnings };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* `--target <name>`: keep the stages up to and including the named one, and export that stage's whole tree
|
|
275
|
+
* (`/`) instead of the EXPORT path. The EXPORT's format and its options (AS, BASE, AT, COMPRESSION) still apply;
|
|
276
|
+
* the `/_redirects` and `/_headers` files EXPORT writes are not (they belong to the EXPORT path). An unknown
|
|
277
|
+
* name is a usage error that lists the named stages.
|
|
278
|
+
*/
|
|
279
|
+
export function targetPlan(p, name) {
|
|
280
|
+
const index = p.stages.findIndex((s) => s.name === name);
|
|
281
|
+
if (index === -1) {
|
|
282
|
+
const names = p.stages.flatMap((s) => (s.name === undefined ? [] : [s.name]));
|
|
283
|
+
throw new SitefileError('E_USAGE', names.length
|
|
284
|
+
? `--target '${name}' does not name a stage (stages: ${names.join(', ')})`
|
|
285
|
+
: `--target '${name}' does not name a stage (the Sitefile has no named stages; name one with FROM … AS <name>)`);
|
|
286
|
+
}
|
|
287
|
+
return { ...p, stages: p.stages.slice(0, index + 1), exportPath: '/', redirects: [], headers: [], target: name };
|
|
288
|
+
}
|
|
289
|
+
/** `IMPORT … SCHEMA <path>`: a local `.json`, `.yaml` or `.yml` file in the build context. */
|
|
290
|
+
function schemaSpec(name, op, vars, span) {
|
|
291
|
+
const uri = evaluateOperand(op, vars);
|
|
292
|
+
if (URL_RE.test(uri)) {
|
|
293
|
+
throw new SitefileError('E_PROTOCOL', `IMPORT ${name}: SCHEMA must be a local file in the build context, not a URL`, op.span);
|
|
294
|
+
}
|
|
295
|
+
const rel = normalizeLocal(uri, op.span).path;
|
|
296
|
+
if (!/\.(json|ya?ml)$/i.test(rel)) {
|
|
297
|
+
throw new SitefileError('E_VALIDATION', `IMPORT ${name}: SCHEMA '${uri}' must be a .json, .yaml or .yml file`, op.span);
|
|
298
|
+
}
|
|
299
|
+
return { uri, path: rel, span };
|
|
300
|
+
}
|
|
301
|
+
/** `IMPORT … DRAFTS <value>`: `true` keeps drafts, `false` leaves them out. */
|
|
302
|
+
function draftsValue(name, op, vars) {
|
|
303
|
+
const raw = evaluateOperand(op, vars);
|
|
304
|
+
if (raw === 'true' || raw === 'false')
|
|
305
|
+
return raw === 'true';
|
|
306
|
+
throw new SitefileError('E_VALIDATION', `IMPORT ${name}: DRAFTS must be true or false, got '${raw}'`, op.span);
|
|
307
|
+
}
|
|
308
|
+
/** `RENDER … PAGE <n>`: a whole number of items per page, at least 1. */
|
|
309
|
+
function pageSize(op, vars) {
|
|
310
|
+
const raw = evaluateOperand(op, vars);
|
|
311
|
+
if (!/^[1-9][0-9]{0,8}$/.test(raw)) {
|
|
312
|
+
throw new SitefileError('E_VALIDATION', `RENDER … PAGE: '${raw}' is not a page size (a whole number from 1 to 999999999)`, op.span);
|
|
313
|
+
}
|
|
314
|
+
return Number(raw);
|
|
315
|
+
}
|
|
316
|
+
/** `TO <dest>` of SITEMAP/FEED or `MANIFEST <dest>` of FINGERPRINT (a file path), or the default. */
|
|
317
|
+
function fileDest(op, vars, fallback, what) {
|
|
318
|
+
if (!op)
|
|
319
|
+
return fallback;
|
|
320
|
+
const raw = evaluateOperand(op, vars);
|
|
321
|
+
const dest = normalizeVirtual(raw, op.span, `${what} destination`);
|
|
322
|
+
if (dest.trailingSlash)
|
|
323
|
+
throw new SitefileError('E_PATH', `${what} destination '${raw}' must be a file path, not a directory`, op.span);
|
|
324
|
+
return dest.path;
|
|
325
|
+
}
|
|
326
|
+
function classifyUri(uri, span) {
|
|
327
|
+
if (isGitUri(uri))
|
|
328
|
+
return { kind: 'git', ...parseGitUri(uri, span) };
|
|
329
|
+
const m = URL_RE.exec(uri);
|
|
330
|
+
if (m) {
|
|
331
|
+
const scheme = m[1].toLowerCase();
|
|
332
|
+
if (scheme === 'https' || scheme === 'http')
|
|
333
|
+
return { kind: 'remote', url: uri };
|
|
334
|
+
if (scheme === 'file') {
|
|
335
|
+
throw new SitefileError('E_PROTOCOL', `file: URLs are not allowed; use a path relative to the build context`, span);
|
|
336
|
+
}
|
|
337
|
+
throw new SitefileError('E_PROTOCOL', `unsupported protocol '${scheme}:' (supported: https, git+https, relative paths)`, span);
|
|
338
|
+
}
|
|
339
|
+
return { kind: 'local', path: normalizeLocal(uri, span).path };
|
|
340
|
+
}
|
|
341
|
+
/** `image <ref> [AT <dir>]`: the reference must parse; a reference without a registry host names a local-store image. */
|
|
342
|
+
function imageBase(b, vars, lead) {
|
|
343
|
+
const ref = evaluateOperand(b.ref, vars);
|
|
344
|
+
let local;
|
|
345
|
+
try {
|
|
346
|
+
local = parseRef(ref).registry === undefined;
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
if (e instanceof SitefileError)
|
|
350
|
+
throw new SitefileError('E_VALIDATION', `${lead} image: ${e.detail}`, b.ref.span, { cause: e });
|
|
351
|
+
throw e;
|
|
352
|
+
}
|
|
353
|
+
const at = b.at ? normalizeVirtual(evaluateOperand(b.at, vars), b.at.span, 'image directory').path : undefined;
|
|
354
|
+
return { kind: 'image', ref, local, ...(at !== undefined ? { at } : {}), lead, span: b.ref.span };
|
|
355
|
+
}
|
|
356
|
+
/** A bare identifier naming an earlier stage is a stage; anything else is a context-relative directory. */
|
|
357
|
+
function stageBase(op, vars, earlier, all, imports, lead = 'FROM') {
|
|
358
|
+
const raw = evaluateOperand(op, vars);
|
|
359
|
+
if (earlier.some((s) => s.name === raw))
|
|
360
|
+
return { kind: 'stage', name: raw };
|
|
361
|
+
// `<import>+<stage>`: a stage of a Sitefile imported AS sitefile on an earlier line.
|
|
362
|
+
const imported = importedStage(raw);
|
|
363
|
+
if (imported && imports.get(imported.import)?.as === 'sitefile')
|
|
364
|
+
return { kind: 'stage', name: raw };
|
|
365
|
+
if (all.has(raw)) {
|
|
366
|
+
throw new SitefileError('E_UNDEFINED', `${lead} ${raw}: stage '${raw}' is not defined before this line (write ./${raw} for a local directory)`, op.span);
|
|
367
|
+
}
|
|
368
|
+
return { kind: 'local', path: normalizeLocal(raw, op.span).path };
|
|
369
|
+
}
|
|
370
|
+
function sourceRef(op, vars, imports, from) {
|
|
371
|
+
if (isInline(op))
|
|
372
|
+
return { kind: 'inline', text: op.text, delimiter: op.delimiter, bodyLine: op.bodyLine, span: op.span };
|
|
373
|
+
const raw = evaluateOperand(op, vars);
|
|
374
|
+
if (from)
|
|
375
|
+
return { kind: 'stage', name: from.name, path: normalizeVirtual(raw, op.span, `path inside stage '${from.name}'`).path, span: op.span };
|
|
376
|
+
if (URL_RE.test(raw)) {
|
|
377
|
+
throw new SitefileError('E_VALIDATION', `cannot read '${raw}' directly; IMPORT it under a name, then COPY name:`, op.span);
|
|
378
|
+
}
|
|
379
|
+
const m = IMPORT_REF_RE.exec(raw);
|
|
380
|
+
if (m) {
|
|
381
|
+
const name = m[1];
|
|
382
|
+
const inner = m[2];
|
|
383
|
+
if (!imports.has(name)) {
|
|
384
|
+
throw new SitefileError('E_UNDEFINED', `'${name}:' does not name an IMPORT; write './${raw}' for a local file`, op.span);
|
|
385
|
+
}
|
|
386
|
+
const path = inner === '' ? '/' : normalizeVirtual(inner, op.span, `path inside import '${name}'`).path;
|
|
387
|
+
return { kind: 'import', name, path, span: op.span };
|
|
388
|
+
}
|
|
389
|
+
return { kind: 'local', path: normalizeLocal(raw, op.span).path, span: op.span };
|
|
390
|
+
}
|
|
391
|
+
/** Every reference to a stage of an imported Sitefile, in source order (stage bases, then `--from` sources, per stage). */
|
|
392
|
+
export function importedStageRefs(stages) {
|
|
393
|
+
const out = [];
|
|
394
|
+
const add = (name, span) => {
|
|
395
|
+
const s = importedStage(name);
|
|
396
|
+
if (s)
|
|
397
|
+
out.push({ name, import: s.import, stage: s.stage, span });
|
|
398
|
+
};
|
|
399
|
+
for (const st of stages) {
|
|
400
|
+
if (st.base.kind === 'stage')
|
|
401
|
+
add(st.base.name, st.span);
|
|
402
|
+
for (const op of st.operations) {
|
|
403
|
+
const src = op.kind === 'COPY' ? op.source : op.kind === 'RENDER' ? op.template : op.kind === 'TRANSFORM' ? op.source : undefined;
|
|
404
|
+
if (src?.kind === 'stage')
|
|
405
|
+
add(src.name, op.span);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
//# sourceMappingURL=planner.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { PluginResult } from './host.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content-addressed cache of TRANSFORM results (see docs/PLUGINS.md, "Caching").
|
|
4
|
+
*
|
|
5
|
+
* <dir>/entries/<key>.json manifest: which blobs make up the result
|
|
6
|
+
* <dir>/blobs/<hex> result bytes, named by their SHA-256
|
|
7
|
+
*
|
|
8
|
+
* The key is the SHA-256 of a canonical JSON document describing everything the
|
|
9
|
+
* result can depend on. Reads verify every blob's digest and size; a damaged
|
|
10
|
+
* entry is removed and counts as a miss, so the next put rewrites it.
|
|
11
|
+
*/
|
|
12
|
+
/** Bump to invalidate every existing entry when the key or entry format changes. */
|
|
13
|
+
export declare const CACHE_VERSION = 1;
|
|
14
|
+
/** What an entry records: the result kind and `[path, blob hex, size]` per file (a `file` result has one, with path ''). */
|
|
15
|
+
export interface CacheEntry {
|
|
16
|
+
readonly kind: 'file' | 'files';
|
|
17
|
+
readonly files: ReadonlyArray<readonly [string, string, number]>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A second-level cache consulted on a local miss (`--cache-from`, see src/registry-cache.ts), and told
|
|
21
|
+
* about every entry this build used or wrote (`--cache-to`).
|
|
22
|
+
*/
|
|
23
|
+
export interface RemoteCache {
|
|
24
|
+
/** Fetch the entry for `key` and its blobs (written verified to `blobPath(hex)`), or undefined. Never throws. */
|
|
25
|
+
fetch(key: string, blobPath: (hex: string) => string): Promise<CacheEntry | undefined>;
|
|
26
|
+
used(key: string, entry: CacheEntry): void;
|
|
27
|
+
}
|
|
28
|
+
export interface CacheStats {
|
|
29
|
+
hits: number;
|
|
30
|
+
misses: number;
|
|
31
|
+
writes: number;
|
|
32
|
+
/** Entries found damaged (and discarded). */
|
|
33
|
+
corrupt: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CacheInfo {
|
|
36
|
+
readonly dir: string;
|
|
37
|
+
readonly entries: number;
|
|
38
|
+
readonly blobs: number;
|
|
39
|
+
readonly bytes: number;
|
|
40
|
+
}
|
|
41
|
+
/** JSON with sorted object keys, so equal values always serialize equally. */
|
|
42
|
+
export declare function canonicalJson(value: unknown): string;
|
|
43
|
+
export declare function cacheKey(parts: object): string;
|
|
44
|
+
export declare class TransformCache {
|
|
45
|
+
readonly dir: string;
|
|
46
|
+
private readonly remote?;
|
|
47
|
+
readonly stats: CacheStats;
|
|
48
|
+
constructor(dir: string, remote?: RemoteCache | undefined);
|
|
49
|
+
private entryPath;
|
|
50
|
+
private blobPath;
|
|
51
|
+
get(key: string): Promise<PluginResult | undefined>;
|
|
52
|
+
private load;
|
|
53
|
+
put(key: string, result: PluginResult): Promise<void>;
|
|
54
|
+
private writeAtomic;
|
|
55
|
+
info(): Promise<CacheInfo>;
|
|
56
|
+
/**
|
|
57
|
+
* Remove entries not used for `olderThanMs` (0 removes all), then every blob
|
|
58
|
+
* no remaining entry references. Returns what was removed.
|
|
59
|
+
*/
|
|
60
|
+
prune(olderThanMs?: number, now?: number): Promise<{
|
|
61
|
+
entries: number;
|
|
62
|
+
blobs: number;
|
|
63
|
+
bytes: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/** Digest of every regular file under `dir` (a plugin can read all of them, so any may change its output). */
|
|
67
|
+
export declare function digestFolder(dir: string, limits?: {
|
|
68
|
+
files: number;
|
|
69
|
+
bytes: number;
|
|
70
|
+
}): Promise<string | undefined>;
|