@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +475 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +118 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +109 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/outdated.d.ts +5 -0
  37. package/lib/src/cli/outdated.js +94 -0
  38. package/lib/src/cli/registry.d.ts +2 -0
  39. package/lib/src/cli/registry.js +122 -0
  40. package/lib/src/cli/serve-worker.d.ts +2 -0
  41. package/lib/src/cli/serve-worker.js +42 -0
  42. package/lib/src/cli/servers.d.ts +15 -0
  43. package/lib/src/cli/servers.js +451 -0
  44. package/lib/src/cli/vendor.d.ts +2 -0
  45. package/lib/src/cli/vendor.js +92 -0
  46. package/lib/src/cli/watch.d.ts +26 -0
  47. package/lib/src/cli/watch.js +81 -0
  48. package/lib/src/cli.d.ts +12 -0
  49. package/lib/src/cli.js +486 -0
  50. package/lib/src/csv.d.ts +27 -0
  51. package/lib/src/csv.js +117 -0
  52. package/lib/src/deflate.d.ts +76 -0
  53. package/lib/src/deflate.js +604 -0
  54. package/lib/src/digest.d.ts +3 -0
  55. package/lib/src/digest.js +7 -0
  56. package/lib/src/errors.d.ts +34 -0
  57. package/lib/src/errors.js +20 -0
  58. package/lib/src/executor.d.ts +386 -0
  59. package/lib/src/executor.js +2193 -0
  60. package/lib/src/explain.d.ts +64 -0
  61. package/lib/src/explain.js +445 -0
  62. package/lib/src/export-formats.d.ts +16 -0
  63. package/lib/src/export-formats.js +52 -0
  64. package/lib/src/exporter.d.ts +103 -0
  65. package/lib/src/exporter.js +676 -0
  66. package/lib/src/fingerprint.d.ts +40 -0
  67. package/lib/src/fingerprint.js +168 -0
  68. package/lib/src/frontmatter.d.ts +22 -0
  69. package/lib/src/frontmatter.js +45 -0
  70. package/lib/src/index.d.ts +12 -0
  71. package/lib/src/index.js +12 -0
  72. package/lib/src/language/ast.d.ts +353 -0
  73. package/lib/src/language/ast.js +57 -0
  74. package/lib/src/language/parser.d.ts +3 -0
  75. package/lib/src/language/parser.js +612 -0
  76. package/lib/src/language/scanner.d.ts +110 -0
  77. package/lib/src/language/scanner.js +449 -0
  78. package/lib/src/language/validate.d.ts +13 -0
  79. package/lib/src/language/validate.js +367 -0
  80. package/lib/src/links.d.ts +79 -0
  81. package/lib/src/links.js +583 -0
  82. package/lib/src/lint.d.ts +10 -0
  83. package/lib/src/lint.js +223 -0
  84. package/lib/src/markdown.d.ts +29 -0
  85. package/lib/src/markdown.js +612 -0
  86. package/lib/src/media.d.ts +82 -0
  87. package/lib/src/media.js +650 -0
  88. package/lib/src/memo.d.ts +57 -0
  89. package/lib/src/memo.js +93 -0
  90. package/lib/src/oci.d.ts +178 -0
  91. package/lib/src/oci.js +380 -0
  92. package/lib/src/outdated.d.ts +46 -0
  93. package/lib/src/outdated.js +195 -0
  94. package/lib/src/pages.d.ts +10 -0
  95. package/lib/src/pages.js +54 -0
  96. package/lib/src/paths.d.ts +39 -0
  97. package/lib/src/paths.js +101 -0
  98. package/lib/src/planner.d.ts +297 -0
  99. package/lib/src/planner.js +410 -0
  100. package/lib/src/plugins/cache.d.ts +70 -0
  101. package/lib/src/plugins/cache.js +226 -0
  102. package/lib/src/plugins/exec.d.ts +51 -0
  103. package/lib/src/plugins/exec.js +144 -0
  104. package/lib/src/plugins/host.d.ts +75 -0
  105. package/lib/src/plugins/host.js +277 -0
  106. package/lib/src/plugins/wasm.d.ts +23 -0
  107. package/lib/src/plugins/wasm.js +249 -0
  108. package/lib/src/profile.d.ts +42 -0
  109. package/lib/src/profile.js +68 -0
  110. package/lib/src/ref.d.ts +40 -0
  111. package/lib/src/ref.js +79 -0
  112. package/lib/src/registry-auth.d.ts +32 -0
  113. package/lib/src/registry-auth.js +151 -0
  114. package/lib/src/registry-cache.d.ts +71 -0
  115. package/lib/src/registry-cache.js +333 -0
  116. package/lib/src/registry-ops.d.ts +51 -0
  117. package/lib/src/registry-ops.js +248 -0
  118. package/lib/src/registry.d.ts +109 -0
  119. package/lib/src/registry.js +505 -0
  120. package/lib/src/render.d.ts +134 -0
  121. package/lib/src/render.js +576 -0
  122. package/lib/src/resources/archive.d.ts +11 -0
  123. package/lib/src/resources/archive.js +306 -0
  124. package/lib/src/resources/bundle-data.d.ts +16 -0
  125. package/lib/src/resources/bundle-data.js +50 -0
  126. package/lib/src/resources/convert.d.ts +12 -0
  127. package/lib/src/resources/convert.js +63 -0
  128. package/lib/src/resources/documents.d.ts +35 -0
  129. package/lib/src/resources/documents.js +122 -0
  130. package/lib/src/resources/git.d.ts +103 -0
  131. package/lib/src/resources/git.js +720 -0
  132. package/lib/src/resources/https.d.ts +40 -0
  133. package/lib/src/resources/https.js +256 -0
  134. package/lib/src/resources/ignore.d.ts +33 -0
  135. package/lib/src/resources/ignore.js +119 -0
  136. package/lib/src/resources/local.d.ts +64 -0
  137. package/lib/src/resources/local.js +248 -0
  138. package/lib/src/resources/lockfile.d.ts +52 -0
  139. package/lib/src/resources/lockfile.js +172 -0
  140. package/lib/src/resources/netpolicy.d.ts +6 -0
  141. package/lib/src/resources/netpolicy.js +82 -0
  142. package/lib/src/resources/types.d.ts +63 -0
  143. package/lib/src/resources/types.js +13 -0
  144. package/lib/src/resources/vendor.d.ts +94 -0
  145. package/lib/src/resources/vendor.js +296 -0
  146. package/lib/src/resources/wbn-reader.d.ts +44 -0
  147. package/lib/src/resources/wbn-reader.js +302 -0
  148. package/lib/src/schema.d.ts +25 -0
  149. package/lib/src/schema.js +169 -0
  150. package/lib/src/secrets.d.ts +21 -0
  151. package/lib/src/secrets.js +103 -0
  152. package/lib/src/serve-rules.d.ts +29 -0
  153. package/lib/src/serve-rules.js +122 -0
  154. package/lib/src/server.d.ts +45 -0
  155. package/lib/src/server.js +246 -0
  156. package/lib/src/servers.d.ts +71 -0
  157. package/lib/src/servers.js +139 -0
  158. package/lib/src/site.d.ts +60 -0
  159. package/lib/src/site.js +169 -0
  160. package/lib/src/store.d.ts +151 -0
  161. package/lib/src/store.js +596 -0
  162. package/lib/src/tree.d.ts +50 -0
  163. package/lib/src/tree.js +134 -0
  164. package/lib/src/values.d.ts +7 -0
  165. package/lib/src/values.js +37 -0
  166. package/lib/src/vendor.d.ts +19 -0
  167. package/lib/src/vendor.js +145 -0
  168. package/lib/src/watch.d.ts +44 -0
  169. package/lib/src/watch.js +137 -0
  170. package/lib/src/wbn.d.ts +5 -0
  171. package/lib/src/wbn.js +147 -0
  172. package/lib/src/yaml.d.ts +34 -0
  173. package/lib/src/yaml.js +562 -0
  174. package/package.json +58 -4
@@ -0,0 +1,367 @@
1
+ import { SitefileError } from '../errors.js';
2
+ import { FEED_ITEM, IDENT_RE, importedStage, isFragment, isImageBase, isInline, literalText, stageNames } from './ast.js';
3
+ import { parseSitefile } from './parser.js';
4
+ import { fragmentNotValue, freeRefs, parseRenderTemplate } from '../render.js';
5
+ /** A fragment's template: a heredoc body or partial file without its final newline (it is inserted inline). */
6
+ export function fragmentText(text) {
7
+ return text.replace(/\r?\n$/, '');
8
+ }
9
+ /** Parse the template of an html LET (errors point into the Sitefile). */
10
+ export function parseFragmentBody(body, file) {
11
+ return body.kind === 'line' ? parseRenderTemplate(body.text, file, body.span.line, body.span.column) : parseRenderTemplate(fragmentText(body.text), file, body.bodyLine);
12
+ }
13
+ /** Import kinds that can produce a list (and so can be sorted). */
14
+ const SORTABLE = new Set(['markdown', 'json', 'yaml', 'csv']);
15
+ const IMPORT_REF = /^([A-Za-z_][A-Za-z0-9_]*):/;
16
+ export function operandsOf(ins) {
17
+ switch (ins.kind) {
18
+ case 'FROM':
19
+ return ins.base === 'scratch' ? [] : isImageBase(ins.base) ? [ins.base.ref, ...(ins.base.at ? [ins.base.at] : [])] : [ins.base];
20
+ case 'ARG':
21
+ return ins.defaultValue ? [ins.defaultValue] : [];
22
+ case 'LET':
23
+ return isFragment(ins) ? [] : [ins.value];
24
+ case 'IMPORT':
25
+ return [ins.uri, ...(ins.schema ? [ins.schema.path] : []), ...(ins.drafts ? [ins.drafts.value] : []), ...(ins.args ?? []).map((a) => a.value)];
26
+ case 'COPY':
27
+ return isInline(ins.source) ? [ins.dest] : [ins.source, ins.dest];
28
+ case 'RENDER': {
29
+ const page = ins.each?.page;
30
+ const extra = page ? [page.size, ...(page.first ? [page.first] : [])] : [];
31
+ return [...(isInline(ins.template) ? [] : [ins.template]), ins.dest, ...extra];
32
+ }
33
+ case 'PLUGIN':
34
+ return [ins.uri];
35
+ case 'TRANSFORM': {
36
+ const ops = ins.source && !isInline(ins.source) ? [ins.source] : [];
37
+ return [...ops, ins.dest, ...ins.options.map((o) => o.value), ...(ins.args ?? [])];
38
+ }
39
+ case 'TOOL':
40
+ return [];
41
+ case 'SITEMAP':
42
+ return [ins.base, ...(ins.dest ? [ins.dest] : [])];
43
+ case 'FEED':
44
+ return [ins.base, ins.link, ...(ins.dest ? [ins.dest] : []), ...(ins.title ? [ins.title] : [])];
45
+ case 'REDIRECT':
46
+ return [ins.from, ins.to];
47
+ case 'HEADER':
48
+ return [ins.pattern, ins.name, ...ins.value];
49
+ case 'FINGERPRINT':
50
+ return [...ins.patterns, ...(ins.manifest ? [ins.manifest] : [])];
51
+ case 'CHECKLINKS':
52
+ return [];
53
+ case 'EXPORT':
54
+ return [ins.path];
55
+ }
56
+ }
57
+ /** The only placeholders the host replaces inside TOOL arguments; any other `{…}` stays literal (e.g. ffmpeg's `%{pts}`). */
58
+ export const TOOL_PLACEHOLDER_RE = /\{(input|output|option\.[A-Za-z_][A-Za-z0-9_-]*)\}/g;
59
+ function checkToolArgs(ins) {
60
+ if (!ins.args || ins.args.length === 0) {
61
+ throw new SitefileError('E_VALIDATION', `TRANSFORM … WITH ${ins.plugin}: a TOOL needs ARGS (the argument list, e.g. ARGS -i {input} {output})`, ins.span);
62
+ }
63
+ let usesOutput = false;
64
+ for (const arg of ins.args) {
65
+ for (const part of arg.parts) {
66
+ if (part.kind !== 'text')
67
+ continue;
68
+ for (const m of part.value.matchAll(TOOL_PLACEHOLDER_RE)) {
69
+ const name = m[1];
70
+ if (name === 'output')
71
+ usesOutput = true;
72
+ else if (name === 'input') {
73
+ if (!ins.source)
74
+ throw new SitefileError('E_VALIDATION', `{input} needs a TRANSFORM source file`, arg.span);
75
+ }
76
+ else if (!ins.options.some((o) => `option.${o.key}` === name)) {
77
+ throw new SitefileError('E_UNDEFINED', `{${name}}: no OPTIONS ${name.slice(7)}=… on this TRANSFORM`, arg.span);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (!usesOutput)
83
+ throw new SitefileError('E_VALIDATION', `TRANSFORM … WITH ${ins.plugin}: ARGS must contain {output}, the file the tool writes`, ins.span);
84
+ }
85
+ /** Check semantic rules (see docs/LANGUAGE.md, "v0.1 implementation decisions"). */
86
+ export function validate(instructions, file) {
87
+ if (instructions.length === 0)
88
+ throw new SitefileError('E_VALIDATION', 'Sitefile has no instructions', { file, line: 1, column: 1 });
89
+ const vars = new Set();
90
+ const imports = new Set();
91
+ // Stage names: every alias in the file, those already declared, and those whose stage is complete.
92
+ const allStages = stageNames(instructions);
93
+ const declaredStages = new Set();
94
+ const finishedStages = new Set();
95
+ let currentStage;
96
+ const plugins = new Set();
97
+ const tools = new Set();
98
+ /** html LETs and `IMPORT … AS html`: usable only inside RENDER templates. */
99
+ const fragments = new Set();
100
+ /** IMPORTs `AS sitefile`: `<name>+<stage>` names a stage of the imported Sitefile. */
101
+ const sitefiles = new Set();
102
+ /** One namespace for ARG/LET, IMPORT, stage and PLUGIN names. */
103
+ const taken = (name) => vars.has(name) || imports.has(name) || declaredStages.has(name) || plugins.has(name) || tools.has(name) || fragments.has(name);
104
+ // Where each ARG, LET and IMPORT name is first declared (a fragment may only use names declared before it).
105
+ const declaredAt = new Map();
106
+ for (const ins of instructions) {
107
+ if ((ins.kind === 'ARG' || ins.kind === 'LET' || ins.kind === 'IMPORT') && !declaredAt.has(ins.name))
108
+ declaredAt.set(ins.name, ins.span.line);
109
+ }
110
+ /** Not a value a template can read. */
111
+ const notValue = (name) => declaredStages.has(name) || plugins.has(name) || tools.has(name);
112
+ let sawFrom = false;
113
+ let sawExport = false;
114
+ const out = [];
115
+ for (const ins of instructions) {
116
+ // The first instruction other than ARG, with no FROM yet, starts an implicit unnamed `FROM scratch`
117
+ // stage. It has no source line of its own, so it takes that instruction's span.
118
+ // An IMPORT … AS sitefile may come first too, so a file can start FROM one of its stages.
119
+ if (!sawFrom && ins.kind !== 'FROM' && ins.kind !== 'ARG' && !(ins.kind === 'IMPORT' && ins.as === 'sitefile')) {
120
+ out.push({ kind: 'FROM', span: ins.span, base: 'scratch' });
121
+ sawFrom = true;
122
+ }
123
+ out.push(ins);
124
+ if (sawExport)
125
+ throw new SitefileError('E_VALIDATION', 'EXPORT must be the last instruction', ins.span);
126
+ // RENDER … FOR item IN list: the destination may use ${item} / ${item.key}.
127
+ // RENDER/TRANSFORM … FOR item IN list: the destination (and TRANSFORM options) may use ${item.key}.
128
+ // FEED … LINK <path>: the link may use ${item.key} for the current element.
129
+ const loopVar = ins.kind === 'RENDER' || ins.kind === 'TRANSFORM' ? ins.each?.as : ins.kind === 'FEED' ? FEED_ITEM : undefined;
130
+ for (const op of operandsOf(ins)) {
131
+ const isDest = (ins.kind === 'FEED' && op === ins.link) ||
132
+ (ins.kind === 'RENDER' && (op === ins.dest || op === ins.each?.page?.first)) ||
133
+ (ins.kind === 'TRANSFORM' && (op === ins.dest || ins.options.some((o) => o.value === op) || (ins.args?.includes(op) ?? false)));
134
+ for (const part of op.parts) {
135
+ if (part.kind !== 'var')
136
+ continue;
137
+ if (isDest && part.name === loopVar)
138
+ continue;
139
+ if (part.path.length > 0) {
140
+ throw new SitefileError('E_SYNTAX', `dotted access \${${[part.name, ...part.path].join('.')}} is only allowed in RENDER templates and for a RENDER … FOR loop variable`, part.span);
141
+ }
142
+ if (fragments.has(part.name)) {
143
+ throw new SitefileError('E_TYPE', `'${part.name}' is an html fragment; html values can only be used in RENDER templates`, part.span);
144
+ }
145
+ if (vars.has(part.name))
146
+ continue;
147
+ const hint = imports.has(part.name) ? ` ('${part.name}' is an import; use it in a RENDER template)` : '';
148
+ throw new SitefileError('E_UNDEFINED', `undefined variable \${${part.name}}: declare it with ARG or LET on an earlier line${hint}`, part.span);
149
+ }
150
+ }
151
+ if (ins.kind === 'FROM') {
152
+ if (currentStage !== undefined)
153
+ finishedStages.add(currentStage);
154
+ if (ins.base !== 'scratch' && !isImageBase(ins.base)) {
155
+ // A bare word naming a stage must name an earlier one; './name' always means a directory.
156
+ const text = literalText(ins.base);
157
+ const imported = text === undefined ? undefined : importedStage(text);
158
+ // `<import>+<stage>` of an IMPORT … AS sitefile: whether the stage exists is checked when that Sitefile is read.
159
+ if (!(imported && sitefiles.has(imported.import)) && text !== undefined && IDENT_RE.test(text) && allStages.has(text) && !finishedStages.has(text)) {
160
+ throw new SitefileError('E_UNDEFINED', `${ins.keyword === 'STAGE' ? `STAGE ${ins.alias} FROM` : 'FROM'} ${text}: stage '${text}' is not defined before this line (write ./${text} for a local directory)`, ins.base.span);
161
+ }
162
+ }
163
+ if (ins.alias !== undefined) {
164
+ if (taken(ins.alias)) {
165
+ throw new SitefileError('E_DUPLICATE', `'${ins.alias}' is already defined`, ins.span);
166
+ }
167
+ declaredStages.add(ins.alias);
168
+ }
169
+ currentStage = ins.alias;
170
+ sawFrom = true;
171
+ continue;
172
+ }
173
+ switch (ins.kind) {
174
+ case 'ARG':
175
+ case 'LET':
176
+ if (taken(ins.name)) {
177
+ throw new SitefileError('E_DUPLICATE', `'${ins.name}' is already defined`, ins.span);
178
+ }
179
+ if (isFragment(ins)) {
180
+ checkFragment(ins, file, { vars, imports, fragments, notValue, declaredAt });
181
+ fragments.add(ins.name);
182
+ }
183
+ else {
184
+ vars.add(ins.name);
185
+ }
186
+ break;
187
+ case 'IMPORT':
188
+ if (taken(ins.name)) {
189
+ throw new SitefileError('E_DUPLICATE', `'${ins.name}' is already defined`, ins.span);
190
+ }
191
+ if (ins.sort && !SORTABLE.has(ins.as ?? '')) {
192
+ throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: SORT BY applies to lists: AS markdown, json, yaml or csv`, ins.sort.span);
193
+ }
194
+ if (ins.drafts && !SORTABLE.has(ins.as ?? '')) {
195
+ throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: DRAFTS applies to lists: AS markdown, json, yaml or csv`, ins.drafts.span);
196
+ }
197
+ if (ins.schema && !SORTABLE.has(ins.as ?? '')) {
198
+ throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: SCHEMA applies to documents and lists: AS markdown, json, yaml or csv`, ins.schema.span);
199
+ }
200
+ // `AS html`: a file-based fragment, usable only in templates (still an import for `name:` sources).
201
+ if (ins.as === 'html')
202
+ fragments.add(ins.name);
203
+ if (ins.args && ins.as !== 'sitefile') {
204
+ throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: ARG sets a build arg of an imported Sitefile, so it needs AS sitefile`, ins.args[0].span);
205
+ }
206
+ if (ins.as === 'sitefile') {
207
+ if (ins.integrity) {
208
+ throw new SitefileError('E_VALIDATION', `IMPORT ${ins.name}: INTEGRITY does not apply to AS sitefile (pin a Git import by its commit, #<40 hex>[:path])`, ins.span);
209
+ }
210
+ sitefiles.add(ins.name);
211
+ }
212
+ imports.add(ins.name);
213
+ break;
214
+ case 'COPY':
215
+ checkSource(ins.source, imports, ins.from, finishedStages, allStages, sitefiles);
216
+ break;
217
+ case 'RENDER':
218
+ checkSource(ins.template, imports, ins.from, finishedStages, allStages, sitefiles);
219
+ if (ins.each) {
220
+ const { as, list, span } = ins.each;
221
+ if (fragments.has(list))
222
+ throw notAList(as, list, span);
223
+ if (!imports.has(list)) {
224
+ throw new SitefileError('E_UNDEFINED', `FOR ${as} IN ${list}: '${list}' is not an IMPORT declared on an earlier line`, span);
225
+ }
226
+ notSitefile(list, `FOR ${as} IN ${list}`, span, sitefiles);
227
+ if (taken(as) || allStages.has(as)) {
228
+ throw new SitefileError('E_DUPLICATE', `FOR ${as}: '${as}' is already defined; pick another loop variable name`, span);
229
+ }
230
+ }
231
+ break;
232
+ case 'PLUGIN':
233
+ if (taken(ins.name))
234
+ throw new SitefileError('E_DUPLICATE', `'${ins.name}' is already defined`, ins.span);
235
+ plugins.add(ins.name);
236
+ break;
237
+ case 'TOOL':
238
+ if (taken(ins.name))
239
+ throw new SitefileError('E_DUPLICATE', `'${ins.name}' is already defined`, ins.span);
240
+ tools.add(ins.name);
241
+ break;
242
+ case 'TRANSFORM':
243
+ if (tools.has(ins.plugin))
244
+ checkToolArgs(ins);
245
+ else if (!plugins.has(ins.plugin)) {
246
+ throw new SitefileError('E_UNDEFINED', `TRANSFORM … WITH ${ins.plugin}: no PLUGIN or TOOL ${ins.plugin} is declared on an earlier line`, ins.pluginSpan);
247
+ }
248
+ else if (ins.args) {
249
+ throw new SitefileError('E_VALIDATION', `ARGS applies to a TOOL; ${ins.plugin} is a PLUGIN (pass values with OPTIONS)`, ins.span);
250
+ }
251
+ // With no source and no FOR the plugin gets only its OPTIONS (e.g. a badge or a generated file).
252
+ if (ins.source)
253
+ checkSource(ins.source, imports, ins.from, finishedStages, allStages, sitefiles);
254
+ if (ins.each) {
255
+ const { as, list, span } = ins.each;
256
+ if (fragments.has(list))
257
+ throw notAList(as, list, span);
258
+ if (!imports.has(list)) {
259
+ throw new SitefileError('E_UNDEFINED', `FOR ${as} IN ${list}: '${list}' is not an IMPORT declared on an earlier line`, span);
260
+ }
261
+ notSitefile(list, `FOR ${as} IN ${list}`, span, sitefiles);
262
+ if (taken(as))
263
+ throw new SitefileError('E_DUPLICATE', `FOR ${as}: '${as}' is already defined; pick another loop variable name`, span);
264
+ }
265
+ break;
266
+ case 'FEED':
267
+ if (fragments.has(ins.list)) {
268
+ throw new SitefileError('E_TYPE', `FEED ${ins.list}: '${ins.list}' is an html fragment, not a list`, ins.listSpan);
269
+ }
270
+ if (!imports.has(ins.list)) {
271
+ throw new SitefileError('E_UNDEFINED', `FEED ${ins.list}: '${ins.list}' is not an IMPORT declared on an earlier line`, ins.listSpan);
272
+ }
273
+ notSitefile(ins.list, `FEED ${ins.list}`, ins.listSpan, sitefiles);
274
+ break;
275
+ case 'SITEMAP':
276
+ case 'REDIRECT':
277
+ case 'HEADER':
278
+ case 'FINGERPRINT':
279
+ case 'CHECKLINKS':
280
+ break;
281
+ case 'EXPORT':
282
+ sawExport = true;
283
+ break;
284
+ }
285
+ }
286
+ const last = instructions[instructions.length - 1];
287
+ if (!sawExport)
288
+ throw new SitefileError('E_VALIDATION', 'missing EXPORT: the last instruction must be EXPORT', last.span);
289
+ return { file, instructions: out };
290
+ }
291
+ function notAList(as, list, span) {
292
+ return new SitefileError('E_TYPE', `FOR ${as} IN ${list}: '${list}' is an html fragment, not a list`, span);
293
+ }
294
+ /**
295
+ * An html LET is checked where it is declared: its template syntax, and every name it reads that
296
+ * is declared before it. A name declared later is an error (define before use); a name declared
297
+ * nowhere is left to the place it is used (a `RENDER … FOR` loop variable, say).
298
+ */
299
+ function checkFragment(ins, file, s) {
300
+ for (const r of freeRefs(parseFragmentBody(ins.body, file))) {
301
+ if (r.name === ins.name)
302
+ throw new SitefileError('E_TEMPLATE', `html fragment '${ins.name}' uses itself`, r.span);
303
+ if (s.fragments.has(r.name)) {
304
+ if (r.use !== 'var' || r.path.length > 0)
305
+ throw fragmentNotValue(r.name, r.span);
306
+ }
307
+ else if (s.vars.has(r.name)) {
308
+ if ((r.use === 'var' || r.use === 'text') && r.path.length > 0)
309
+ throw new SitefileError('E_TYPE', `'${r.name}' is an ARG/LET value; dotted access only works on JSON imports`, r.span);
310
+ }
311
+ else if (s.imports.has(r.name)) {
312
+ // Its kind (and so whether a template can read it) is known once it is resolved.
313
+ }
314
+ else if (s.notValue(r.name)) {
315
+ throw new SitefileError('E_UNDEFINED', `'${r.name}' is not an ARG, LET or IMPORT`, r.span);
316
+ }
317
+ else {
318
+ const line = s.declaredAt.get(r.name);
319
+ if (line !== undefined) {
320
+ throw new SitefileError('E_UNDEFINED', `'${r.name}' is declared on line ${line}, after LET ${ins.name}: html; a fragment can only use names declared before it`, r.span);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ /** A FOR or FEED list must be a value import, not a Sitefile imported AS sitefile. */
326
+ function notSitefile(name, what, span, sitefiles) {
327
+ if (sitefiles.has(name))
328
+ throw new SitefileError('E_TYPE', `${what}: '${name}' is a Sitefile import, not a list`, span);
329
+ }
330
+ function checkSource(op, imports, from, finished, all, sitefiles) {
331
+ if (isInline(op))
332
+ return; // content is in the Sitefile itself
333
+ const imported = from ? importedStage(from.name) : undefined;
334
+ if (from && imported) {
335
+ // `--from=<import>+<stage>`: a stage of an imported Sitefile (whether it exists is checked when that Sitefile is read).
336
+ if (!sitefiles.has(imported.import)) {
337
+ throw new SitefileError('E_UNDEFINED', `--from=${from.name}: '${imported.import}' is not an IMPORT … AS sitefile declared on an earlier line`, from.span);
338
+ }
339
+ return;
340
+ }
341
+ if (from) {
342
+ // The source is a path inside the stage, not an import ref or local path.
343
+ if (!finished.has(from.name)) {
344
+ const why = all.has(from.name) ? 'is not complete before this line (a stage cannot read itself or a later stage)' : 'is not defined';
345
+ throw new SitefileError('E_UNDEFINED', `--from=${from.name}: stage '${from.name}' ${why}`, from.span);
346
+ }
347
+ return;
348
+ }
349
+ const text = literalText(op);
350
+ if (text === undefined)
351
+ return;
352
+ if (text.includes('://')) {
353
+ throw new SitefileError('E_VALIDATION', 'URL sources are not allowed here: use IMPORT then COPY', op.span);
354
+ }
355
+ const m = IMPORT_REF.exec(text);
356
+ if (m && !imports.has(m[1])) {
357
+ throw new SitefileError('E_UNDEFINED', `unknown import '${m[1]}' (declare it with IMPORT on an earlier line, or write ./${text} for a local path)`, op.span);
358
+ }
359
+ if (m && sitefiles.has(m[1])) {
360
+ throw new SitefileError('E_TYPE', `'${m[1]}' is a Sitefile import; read one of its stages with --from=${m[1]}+<stage>`, op.span);
361
+ }
362
+ }
363
+ /** Parse and validate a Sitefile. */
364
+ export function parseProgram(source, file = 'Sitefile') {
365
+ return validate(parseSitefile(source, file), file);
366
+ }
367
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1,79 @@
1
+ /** One URL found in a file. */
2
+ export interface Reference {
3
+ /** Offsets of the URL as written in the scanned text (`raw` is `text.slice(start, end)`). */
4
+ readonly start: number;
5
+ readonly end: number;
6
+ /** The URL as written. */
7
+ readonly raw: string;
8
+ /** The URL with character references (HTML) or escapes (CSS) decoded and ASCII whitespace trimmed. */
9
+ readonly url: string;
10
+ /** 1-based line of `start`. */
11
+ readonly line: number;
12
+ /** Written as CSS (a CSS file, a <style> element or a `style` attribute), so CSS escapes apply. */
13
+ readonly css: boolean;
14
+ }
15
+ /** Decoded text, and for each of its offsets (and its end) the offset in the source it came from. */
16
+ interface Mapped {
17
+ readonly text: string;
18
+ readonly map: readonly number[];
19
+ }
20
+ /** Decode the character references this scanner knows (see the top of the file); others stay as written. */
21
+ export declare function decodeHtml(s: string): Mapped;
22
+ export declare const decodeHtmlText: (s: string) => string;
23
+ /** Decode CSS escapes: `\` + newline is removed, `\` + 1-6 hex digits (and one following whitespace) is a code point, `\` + anything else is that character. */
24
+ export declare function decodeCss(s: string): Mapped;
25
+ export declare const decodeCssText: (s: string) => string;
26
+ /** The URLs a CSS file references, in order. */
27
+ export declare function scanCss(text: string): Reference[];
28
+ /** The URLs an HTML file references, in order (including those of its CSS). */
29
+ export declare function scanHtml(text: string): Reference[];
30
+ /** Whether a path is read by {@link scanFile}, and how. */
31
+ export declare function referenceKind(path: string): 'html' | 'css' | undefined;
32
+ /** A file's text and references; undefined when it is not HTML or CSS, or not valid UTF-8. */
33
+ export declare function scanFile(path: string, bytes: Uint8Array): {
34
+ text: string;
35
+ refs: Reference[];
36
+ } | undefined;
37
+ /** What a reference points at. */
38
+ export type Target =
39
+ /** Nothing to check: empty, a fragment or query of the same document, or a non-http(s) scheme (mailto:, data:, …). */
40
+ {
41
+ readonly kind: 'none';
42
+ }
43
+ /** An http(s) URL or a protocol-relative `//host/…`. */
44
+ | {
45
+ readonly kind: 'external';
46
+ readonly url: string;
47
+ }
48
+ /**
49
+ * A file of the site: `path` is absolute, normalized and NFC (`..` stops at the root), percent-decoded;
50
+ * `dir` when the URL ends with `/` (or `.`/`..`), so it names a directory.
51
+ */
52
+ | {
53
+ readonly kind: 'internal';
54
+ readonly path: string;
55
+ readonly dir: boolean;
56
+ };
57
+ /** Percent-decode one path segment; a segment whose decoding is not valid UTF-8 stays as written. */
58
+ export declare function percentDecode(seg: string): string;
59
+ /**
60
+ * Resolve a reference's URL against the path of the file that contains it. `root` is the directory the
61
+ * site is served from (the EXPORT path): an absolute URL path (`/img/a.png`) names a file below it, and
62
+ * `..` never climbs above it (as in a browser), for references in files below it; elsewhere `..` stops at `/`.
63
+ */
64
+ export declare function resolveReference(url: string, from: string, root?: string): Target;
65
+ /** Percent-encode a path, keeping `/` and the URL-unreserved characters `A-Z a-z 0-9 - . _ ~`. */
66
+ export declare function encodePath(p: string): string;
67
+ /** A relative URL from the file `from` to the file `to` (both absolute tree paths), percent-encoded. */
68
+ export declare function relativeUrl(from: string, to: string): string;
69
+ /**
70
+ * The reference `raw` (written in `from`, resolved to `oldPath`) pointing at `newPath` instead, where only the
71
+ * file name changed by the insertion of `.<hash>`. The hash is inserted into the name as written, before the
72
+ * dot of its extension (or after the name), so the directories, the query, the fragment and the way it is
73
+ * written (escapes, character references, percent-encoding) are kept. When the name can't be edited that way
74
+ * (its extension's dot is percent-encoded, say), the path becomes a relative URL from `from` to `newPath`.
75
+ */
76
+ export declare function renameReference(raw: string, css: boolean, from: string, oldPath: string, newPath: string): string;
77
+ /** `text` with each reference whose target `rename` maps to a new path rewritten; undefined when nothing changed. */
78
+ export declare function rewriteReferences(text: string, refs: readonly Reference[], from: string, root: string, rename: (path: string) => string | undefined): string | undefined;
79
+ export {};