@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,64 @@
1
+ import { type Operation, type Plan } from './planner.js';
2
+ import type { BuildResult } from './executor.js';
3
+ /** One line describing an operation: its keyword, source and destination (FOR destinations are not evaluated). */
4
+ export declare function operationLabel(op: Operation): string;
5
+ export type GraphNodeKind = 'import' | 'image' | 'plugin' | 'tool' | 'local' | 'operation' | 'stage' | 'site' | 'export';
6
+ export type GraphEdgeKind = 'base' | 'input' | 'next' | 'export';
7
+ export interface GraphNode {
8
+ readonly id: string;
9
+ readonly kind: GraphNodeKind;
10
+ readonly label: string;
11
+ readonly line?: number;
12
+ }
13
+ export interface GraphEdge {
14
+ readonly from: string;
15
+ readonly to: string;
16
+ readonly kind: GraphEdgeKind;
17
+ }
18
+ export interface Graph {
19
+ readonly file: string;
20
+ readonly target?: string;
21
+ readonly nodes: readonly GraphNode[];
22
+ readonly edges: readonly GraphEdge[];
23
+ }
24
+ /**
25
+ * The plan as a graph. Nodes: imports, plugins, tools, local paths read, operations (`op:<line>`), stages (their
26
+ * final trees), the EXPORT site files and the export. Edges point from what is read to what reads it: `base` (a
27
+ * stage's starting tree to its first operation), `next` (an operation's tree to the following operation, the
28
+ * last one to its stage), `input` (sources, plugins, tools and FOR/FEED lists) and `export`.
29
+ */
30
+ export declare function planGraph(p: Plan): Graph;
31
+ /** `id label` per node, then one ` <- from (kind)` line per incoming edge, then a count. */
32
+ export declare function graphText(g: Graph): string;
33
+ /** Graphviz DOT (`sitefile graph --dot | dot -Tsvg > graph.svg`). */
34
+ export declare function graphDot(g: Graph): string;
35
+ export interface WhyEvent {
36
+ /** `Sitefile:12`: the instruction's line. */
37
+ readonly location: string;
38
+ /** The instruction, as `graph` labels it. */
39
+ readonly instruction: string;
40
+ /** Stage name, or `#n` for an unnamed stage. */
41
+ readonly stage: string;
42
+ /** The path in that stage's tree. */
43
+ readonly path: string;
44
+ readonly action: 'write' | 'overwrite';
45
+ readonly digest: string;
46
+ /** What the content came from: `./src/a.html`, `stage build:/out/a.html`, `import site:/a.html`, `<<EOF`, `plugin x`, `list posts`. */
47
+ readonly inputs: readonly string[];
48
+ }
49
+ export interface WhyReport {
50
+ readonly path: string;
51
+ readonly mode: string;
52
+ readonly size: number;
53
+ readonly digest: string;
54
+ readonly origin: string;
55
+ /** Oldest first; the last event wrote the file as exported. */
56
+ readonly history: readonly WhyEvent[];
57
+ }
58
+ /**
59
+ * Explain one exported file: every write to it (and to the files it was copied from in earlier stages) that
60
+ * changed it, oldest first. Writes of identical content are no-ops in the tree (the first writer stays the
61
+ * owner), so they are not listed. `result` must come from a build with `recordSteps`.
62
+ */
63
+ export declare function why(result: BuildResult, raw: string, contextDir: string): WhyReport;
64
+ export declare function whyText(r: WhyReport): string;
@@ -0,0 +1,445 @@
1
+ import { statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { SitefileError } from './errors.js';
4
+ import { joinVirtual, normalizeVirtual } from './paths.js';
5
+ import { importedStageRefs } from './planner.js';
6
+ import { importedStage } from './language/ast.js';
7
+ // `sitefile graph` and `sitefile why` (issue #94): the dependency graph of a plan, and the history of one
8
+ // output file. Both outputs are part of the CLI contract and identical in the Rust port (rust/src/explain.rs).
9
+ // ---------------------------------------------------------------- labels
10
+ /** A context-relative path as written in a Sitefile: `./src`, `./` for the context root. */
11
+ function localLabel(p) {
12
+ return './' + p;
13
+ }
14
+ /** A source as an operation label shows it. */
15
+ function sourceLabel(ref) {
16
+ switch (ref.kind) {
17
+ case 'inline':
18
+ return `<<${ref.delimiter}`;
19
+ case 'stage':
20
+ return `--from=${ref.name} ${ref.path}`;
21
+ case 'local':
22
+ return localLabel(ref.path);
23
+ case 'import':
24
+ return ref.path === '/' ? `${ref.name}:` : `${ref.name}:${ref.path}`;
25
+ }
26
+ }
27
+ /** One line describing an operation: its keyword, source and destination (FOR destinations are not evaluated). */
28
+ export function operationLabel(op) {
29
+ switch (op.kind) {
30
+ case 'COPY':
31
+ return `COPY ${sourceLabel(op.source)} ${op.dest}${op.destIsDir && op.dest !== '/' ? '/' : ''}`;
32
+ case 'RENDER':
33
+ return op.each
34
+ ? `RENDER ${sourceLabel(op.template)} FOR ${op.each.as} IN ${op.each.list}${op.each.by !== undefined ? ` BY ${op.each.by}` : ''}${op.each.page ? ` PAGE ${op.each.page.size}` : ''}`
35
+ : `RENDER ${sourceLabel(op.template)} TO ${op.dest}`;
36
+ case 'TRANSFORM':
37
+ return `TRANSFORM ${op.plugin}${op.source ? ` ${sourceLabel(op.source)}` : ''}${op.each ? ` FOR ${op.each.as} IN ${op.each.list}` : ''}`;
38
+ case 'SITEMAP':
39
+ return `SITEMAP ${op.dest}`;
40
+ case 'FEED':
41
+ return `FEED ${op.list} ${op.dest}`;
42
+ case 'FINGERPRINT':
43
+ return `FINGERPRINT ${op.patterns.join(' ')}`;
44
+ case 'CHECKLINKS':
45
+ return `CHECKLINKS${op.warn ? ' --warn' : ''}${op.external ? ' --external' : ''}`;
46
+ }
47
+ }
48
+ function stageLabel(stage) {
49
+ const b = stage.base;
50
+ const base = b.kind === 'scratch' ? 'scratch' : b.kind === 'local' ? localLabel(b.path) : b.kind === 'image' ? `image ${b.ref}${b.at !== undefined ? ` AT ${b.at}` : ''}` : b.name;
51
+ return `FROM ${base}${stage.name !== undefined ? ` AS ${stage.name}` : ''}`;
52
+ }
53
+ /** `build`, or `#2` for the second stage when it has no name. */
54
+ function stageDisplay(stage, index) {
55
+ return stage.name ?? `#${index + 1}`;
56
+ }
57
+ function stageId(stage, index) {
58
+ return `stage:${stageDisplay(stage, index)}`;
59
+ }
60
+ /**
61
+ * The plan as a graph. Nodes: imports, plugins, tools, local paths read, operations (`op:<line>`), stages (their
62
+ * final trees), the EXPORT site files and the export. Edges point from what is read to what reads it: `base` (a
63
+ * stage's starting tree to its first operation), `next` (an operation's tree to the following operation, the
64
+ * last one to its stage), `input` (sources, plugins, tools and FOR/FEED lists) and `export`.
65
+ */
66
+ export function planGraph(p) {
67
+ const nodes = [];
68
+ const locals = [];
69
+ const edges = [];
70
+ const seen = new Set();
71
+ const edge = (from, to, kind) => {
72
+ const key = `${from}\0${to}\0${kind}`;
73
+ if (seen.has(key))
74
+ return;
75
+ seen.add(key);
76
+ edges.push({ from, to, kind });
77
+ };
78
+ const local = (rel) => {
79
+ const id = `local:${localLabel(rel)}`;
80
+ if (!locals.some((n) => n.id === id))
81
+ locals.push({ id, kind: 'local', label: localLabel(rel) });
82
+ return id;
83
+ };
84
+ const imports = new Set(p.imports.map((i) => i.name));
85
+ const tools = new Set(p.tools.map((t) => t.name));
86
+ const stageIds = new Map();
87
+ const sourceNode = (ref) => {
88
+ switch (ref.kind) {
89
+ case 'inline':
90
+ return undefined;
91
+ case 'stage':
92
+ return stageIds.get(ref.name);
93
+ case 'local':
94
+ return local(ref.path);
95
+ case 'import':
96
+ return `import:${ref.name}`;
97
+ }
98
+ };
99
+ const list = (name) => (imports.has(name) ? `import:${name}` : undefined);
100
+ // An IMPORT … SCHEMA reads its schema file.
101
+ for (const i of p.imports)
102
+ if (i.schema)
103
+ edge(local(i.schema.path), `import:${i.name}`, 'input');
104
+ // Stages of imported Sitefiles (IMPORT … AS sitefile) are read like stages: one node each, fed by the import.
105
+ const importedStages = [];
106
+ for (const ref of importedStageRefs(p.stages)) {
107
+ const id = `stage:${ref.name}`;
108
+ if (stageIds.has(ref.name))
109
+ continue;
110
+ const line = p.imports.find((i) => i.name === ref.import)?.span.line;
111
+ importedStages.push({ id, kind: 'stage', label: `stage ${ref.stage} of IMPORT ${ref.import}`, ...(line !== undefined ? { line } : {}) });
112
+ stageIds.set(ref.name, id);
113
+ edge(`import:${ref.import}`, id, 'input');
114
+ }
115
+ const staged = [];
116
+ const images = [];
117
+ p.stages.forEach((stage, si) => {
118
+ const id = stageId(stage, si);
119
+ const ids = stage.operations.map((op) => `op:${op.span.line}`);
120
+ const first = ids[0] ?? id;
121
+ if (stage.base.kind === 'local')
122
+ edge(local(stage.base.path), first, 'base');
123
+ else if (stage.base.kind === 'image') {
124
+ const imageId = `image:${stage.base.ref}`;
125
+ if (!images.some((n) => n.id === imageId))
126
+ images.push({ id: imageId, kind: 'image', label: `image ${stage.base.ref}`, line: stage.span.line });
127
+ edge(imageId, first, 'base');
128
+ }
129
+ else if (stage.base.kind === 'stage') {
130
+ const from = stageIds.get(stage.base.name);
131
+ if (from)
132
+ edge(from, first, 'base');
133
+ }
134
+ stage.operations.forEach((op, i) => {
135
+ const opId = ids[i];
136
+ staged.push({ id: opId, kind: 'operation', label: operationLabel(op), line: op.span.line });
137
+ const inputs = [];
138
+ if (op.kind === 'COPY')
139
+ inputs.push(sourceNode(op.source));
140
+ if (op.kind === 'RENDER')
141
+ inputs.push(sourceNode(op.template), op.each ? list(op.each.list) : undefined);
142
+ if (op.kind === 'TRANSFORM') {
143
+ inputs.push(op.source ? sourceNode(op.source) : undefined, `${tools.has(op.plugin) ? 'tool' : 'plugin'}:${op.plugin}`, op.each ? list(op.each.list) : undefined);
144
+ }
145
+ if (op.kind === 'FEED')
146
+ inputs.push(list(op.list));
147
+ for (const from of inputs)
148
+ if (from !== undefined)
149
+ edge(from, opId, 'input');
150
+ edge(opId, ids[i + 1] ?? id, 'next');
151
+ });
152
+ staged.push({ id, kind: 'stage', label: stageLabel(stage), line: stage.span.line });
153
+ if (stage.name !== undefined)
154
+ stageIds.set(stage.name, id);
155
+ });
156
+ const last = p.stages[p.stages.length - 1];
157
+ edge(stageId(last, p.stages.length - 1), 'export', 'export');
158
+ const site = [];
159
+ for (const [name, rules, kind] of [
160
+ ['_redirects', p.redirects, 'REDIRECT'],
161
+ ['_headers', p.headers, 'HEADER'],
162
+ ]) {
163
+ if (!rules.length)
164
+ continue;
165
+ const id = `site:${name}`;
166
+ site.push({ id, kind: 'site', label: `${joinVirtual(p.exportPath, name)} (${rules.length} ${kind} line${rules.length === 1 ? '' : 's'})`, line: rules[0].span.line });
167
+ edge(id, 'export', 'export');
168
+ }
169
+ nodes.push(...p.imports.map((i) => ({ id: `import:${i.name}`, kind: 'import', label: `IMPORT ${i.name} ${i.uri}${i.as ? ` AS ${i.as}` : ''}`, line: i.span.line })), ...p.plugins.map((pl) => ({ id: `plugin:${pl.name}`, kind: 'plugin', label: `PLUGIN ${pl.name} ${localLabel(pl.path)}`, line: pl.span.line })), ...p.tools.map((t) => ({ id: `tool:${t.name}`, kind: 'tool', label: `TOOL ${t.name}`, line: t.span.line })), ...images, ...importedStages, ...locals, ...staged, ...site, { id: 'export', kind: 'export', label: `EXPORT ${p.exportPath} AS ${p.exportFormat}${p.target !== undefined ? ` (--target ${p.target})` : ''}` });
170
+ return { file: p.file, ...(p.target !== undefined ? { target: p.target } : {}), nodes, edges };
171
+ }
172
+ /** `id label` per node, then one ` <- from (kind)` line per incoming edge, then a count. */
173
+ export function graphText(g) {
174
+ let out = '';
175
+ for (const n of g.nodes) {
176
+ out += `${n.id} ${n.label}\n`;
177
+ for (const e of g.edges)
178
+ if (e.to === n.id)
179
+ out += ` <- ${e.from} (${e.kind})\n`;
180
+ }
181
+ return out + `${g.nodes.length} nodes, ${g.edges.length} edges\n`;
182
+ }
183
+ const DOT_SHAPES = {
184
+ import: 'cylinder',
185
+ image: 'tab',
186
+ plugin: 'component',
187
+ tool: 'component',
188
+ local: 'folder',
189
+ operation: 'box',
190
+ stage: 'box3d',
191
+ site: 'note',
192
+ export: 'doubleoctagon',
193
+ };
194
+ function dotString(s) {
195
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
196
+ }
197
+ /** Graphviz DOT (`sitefile graph --dot | dot -Tsvg > graph.svg`). */
198
+ export function graphDot(g) {
199
+ let out = 'digraph sitefile {\n rankdir=LR;\n node [shape=box];\n';
200
+ for (const n of g.nodes)
201
+ out += ` ${dotString(n.id)} [label=${dotString(n.label)}, shape=${DOT_SHAPES[n.kind]}];\n`;
202
+ for (const e of g.edges) {
203
+ const style = e.kind === 'input' ? ' [style=dashed]' : e.kind === 'next' ? '' : ' [style=bold]';
204
+ out += ` ${dotString(e.from)} -> ${dotString(e.to)}${style};\n`;
205
+ }
206
+ return out + '}\n';
207
+ }
208
+ function same(a, b) {
209
+ return a === b || (a !== undefined && b !== undefined && a.origin === b.origin && a.digest === b.digest && a.mode === b.mode);
210
+ }
211
+ /** `rel` of `full` below directory `dir` (`/x` for `/d/x` under `/d`). */
212
+ function below(dir, full) {
213
+ return dir === '/' ? full : full.slice(dir.length);
214
+ }
215
+ /** `base` + `rel` where `base` is a virtual path (`/` for the root) and `rel` starts with `/`. */
216
+ function under(base, rel) {
217
+ return base === '/' ? rel : base + rel;
218
+ }
219
+ /**
220
+ * Explain one exported file: every write to it (and to the files it was copied from in earlier stages) that
221
+ * changed it, oldest first. Writes of identical content are no-ops in the tree (the first writer stays the
222
+ * owner), so they are not listed. `result` must come from a build with `recordSteps`.
223
+ */
224
+ export function why(result, raw, contextDir) {
225
+ const p = result.plan;
226
+ const steps = result.steps;
227
+ if (!steps)
228
+ throw new Error('why() needs a build with recordSteps');
229
+ const want = normalizeVirtual(raw.startsWith('/') ? raw : '/' + raw, undefined, 'path').path;
230
+ const node = result.output.get(want);
231
+ if (!node) {
232
+ if (result.output.isDirectory(want))
233
+ throw new SitefileError('E_NOT_FOUND', `'${want}' is a directory in the build output; name a file`);
234
+ throw new SitefileError('E_NOT_FOUND', `'${want}' is not in the build output`);
235
+ }
236
+ const root = {
237
+ plan: p,
238
+ steps,
239
+ prefix: '',
240
+ resource: (name) => result.resources.get(name),
241
+ isDir: (rel) => {
242
+ try {
243
+ return statSync(path.join(contextDir, rel)).isDirectory();
244
+ }
245
+ catch {
246
+ return false; // gone since the build: show the path as written
247
+ }
248
+ },
249
+ imported: result.imported ?? new Map(),
250
+ ...(result.renames ? { renames: result.renames } : {}),
251
+ };
252
+ const scopes = new Map();
253
+ /** The scope of a Sitefile imported by `scope` (its own messages, steps and context). */
254
+ const scopeOf = (unit, prefix) => {
255
+ let s = scopes.get(unit);
256
+ if (!s) {
257
+ s = {
258
+ plan: unit.plan,
259
+ steps: unit.steps ?? [],
260
+ prefix,
261
+ resource: (name) => unit.resources.get(name),
262
+ isDir: (rel) => unit.isDirectory(rel),
263
+ imported: unit.imported,
264
+ renames: unit.renames,
265
+ };
266
+ scopes.set(unit, s);
267
+ }
268
+ return s;
269
+ };
270
+ /** The scope and index of the stage `name` (`build`, or `theme+assets` for a stage of an imported Sitefile). */
271
+ const locate = (scope, name) => {
272
+ const imp = importedStage(name);
273
+ const unit = imp ? scope.imported.get(imp.import) : undefined;
274
+ if (imp && unit) {
275
+ const s = scopeOf(unit, `${scope.prefix}${imp.import}+`);
276
+ return { scope: s, index: unit.plan.stages.findIndex((st) => st.name === imp.stage) };
277
+ }
278
+ return { scope, index: scope.plan.stages.findIndex((st) => st.name === name) };
279
+ };
280
+ const finalOf = (scope, i) => scope.steps[i][scope.steps[i].length - 1];
281
+ /** Display and (for a stage) the file a source contributes to `file` written at `dest`. */
282
+ const copied = (scope, ref, dest, file) => {
283
+ const rel = file === dest ? undefined : below(dest, file);
284
+ switch (ref.kind) {
285
+ case 'inline':
286
+ return { input: `<<${ref.delimiter}` };
287
+ case 'local': {
288
+ const isDir = scope.isDir(ref.path);
289
+ const rest = isDir && rel !== undefined ? (ref.path === '' ? rel.slice(1) : ref.path + rel) : ref.path;
290
+ return { input: localLabel(rest) };
291
+ }
292
+ case 'import': {
293
+ const res = scope.resource(ref.name);
294
+ if (res?.kind !== 'tree')
295
+ return { input: ref.path === '/' ? `import ${ref.name}` : `import ${ref.name}:${ref.path}` };
296
+ const inner = res.tree.isFile(ref.path) || rel === undefined ? ref.path : under(ref.path, rel);
297
+ return { input: `import ${ref.name}:${inner}` };
298
+ }
299
+ case 'stage': {
300
+ const loc = locate(scope, ref.name);
301
+ const tree = finalOf(loc.scope, loc.index);
302
+ const inner = tree.isFile(ref.path) || rel === undefined ? ref.path : under(ref.path, rel);
303
+ return { input: `stage ${ref.name}:${inner}`, ...(tree.isFile(inner) ? { from: { scope: loc.scope, stage: loc.index, path: inner } } : {}) };
304
+ }
305
+ }
306
+ };
307
+ const readLabel = (ref) => {
308
+ switch (ref.kind) {
309
+ case 'inline':
310
+ return `<<${ref.delimiter}`;
311
+ case 'local':
312
+ return localLabel(ref.path);
313
+ case 'import':
314
+ return ref.path === '/' ? `import ${ref.name}` : `import ${ref.name}:${ref.path}`;
315
+ case 'stage':
316
+ return `stage ${ref.name}:${ref.path}`;
317
+ }
318
+ };
319
+ /** The path `file` had before operation `upTo` of stage `si` when a FINGERPRINT there renamed it, and where. */
320
+ const renamedAt = (scope, si, file, upTo) => {
321
+ const ops = scope.plan.stages[si].operations;
322
+ for (let i = upTo - 1; i >= 0; i--) {
323
+ const op = ops[i];
324
+ if (op.kind !== 'FINGERPRINT')
325
+ continue;
326
+ const hit = scope.renames?.get(op.span.line)?.find(([, to]) => to === file);
327
+ if (hit)
328
+ return { index: i, from: hit[0] };
329
+ }
330
+ return undefined;
331
+ };
332
+ const history = (scope, si, file, upTo = scope.plan.stages[si].operations.length) => {
333
+ const sp = scope.plan;
334
+ const at = (line) => `${sp.file}:${line}`;
335
+ const stage = sp.stages[si];
336
+ const trees = scope.steps[si];
337
+ const display = scope.prefix + stageDisplay(stage, si);
338
+ // A renamed file: the history of its old path up to the FINGERPRINT, then the rename.
339
+ const rename = renamedAt(scope, si, file, upTo);
340
+ if (rename) {
341
+ const op = stage.operations[rename.index];
342
+ const node = trees[rename.index + 1].get(file);
343
+ const events = history(scope, si, rename.from, rename.index);
344
+ events.push({ location: at(op.span.line), instruction: operationLabel(op), stage: display, path: file, action: 'write', digest: node.digest, inputs: [`${rename.from} (renamed)`] });
345
+ return [...events, ...later(scope, si, file, rename.index + 1, upTo, node)];
346
+ }
347
+ const events = [];
348
+ const prev = trees[0].get(file);
349
+ if (prev) {
350
+ if (stage.base.kind === 'stage') {
351
+ const loc = locate(scope, stage.base.name);
352
+ events.push(...history(loc.scope, loc.index, file));
353
+ }
354
+ else if (stage.base.kind === 'local') {
355
+ const rest = stage.base.path === '' ? file.slice(1) : stage.base.path + file;
356
+ events.push({ location: at(stage.span.line), instruction: stageLabel(stage), stage: display, path: file, action: 'write', digest: prev.digest, inputs: [localLabel(rest)] });
357
+ }
358
+ else if (stage.base.kind === 'image') {
359
+ // The file's origin names the image, its digest and the path inside the layer.
360
+ events.push({ location: at(stage.span.line), instruction: stageLabel(stage), stage: display, path: file, action: 'write', digest: prev.digest, inputs: [prev.origin] });
361
+ }
362
+ }
363
+ return [...events, ...later(scope, si, file, 0, upTo, prev)];
364
+ };
365
+ /** The events of operations `from` to `upTo` (exclusive) of stage `si` that changed `file` (`prev`: before them). */
366
+ const later = (scope, si, file, from, upTo, prev) => {
367
+ const sp = scope.plan;
368
+ const at = (line) => `${sp.file}:${line}`;
369
+ const tools = new Set(sp.tools.map((t) => t.name));
370
+ const stage = sp.stages[si];
371
+ const trees = scope.steps[si];
372
+ const display = scope.prefix + stageDisplay(stage, si);
373
+ const events = [];
374
+ stage.operations.slice(from, upTo).forEach((op, k) => {
375
+ const i = from + k;
376
+ const next = trees[i + 1].get(file);
377
+ if (!next || same(prev, next)) {
378
+ prev = next;
379
+ return;
380
+ }
381
+ const inputs = [];
382
+ if (op.kind === 'COPY') {
383
+ const c = copied(scope, op.source, op.dest, file);
384
+ if (c.from)
385
+ events.push(...history(c.from.scope, c.from.stage, c.from.path));
386
+ inputs.push(c.input);
387
+ }
388
+ else if (op.kind === 'RENDER') {
389
+ inputs.push(readLabel(op.template));
390
+ if (op.each)
391
+ inputs.push(`list ${op.each.list}`);
392
+ }
393
+ else if (op.kind === 'TRANSFORM') {
394
+ // A directory TRANSFORM names the input file in each output's origin.
395
+ const perFile = `${at(op.span.line)} TRANSFORM `;
396
+ if (op.source)
397
+ inputs.push(next.origin.startsWith(perFile) ? next.origin.slice(perFile.length) : readLabel(op.source));
398
+ inputs.push(`${tools.has(op.plugin) ? 'tool' : 'plugin'} ${op.plugin}`);
399
+ if (op.each)
400
+ inputs.push(`list ${op.each.list}`);
401
+ }
402
+ else if (op.kind === 'SITEMAP')
403
+ inputs.push('the .html files in the tree');
404
+ else if (op.kind === 'CHECKLINKS')
405
+ return;
406
+ else if (op.kind === 'FINGERPRINT')
407
+ inputs.push(op.manifest === file ? 'the files it renamed' : 'references to renamed files');
408
+ else
409
+ inputs.push(`list ${op.list}`);
410
+ events.push({ location: at(op.span.line), instruction: operationLabel(op), stage: display, path: file, action: prev ? 'overwrite' : 'write', digest: next.digest, inputs });
411
+ prev = next;
412
+ });
413
+ return events;
414
+ };
415
+ const at = (line) => `${p.file}:${line}`;
416
+ const lastIndex = p.stages.length - 1;
417
+ const full = under(p.exportPath, want);
418
+ const events = history(root, lastIndex, full);
419
+ // The EXPORT site files are added after the last stage.
420
+ const before = finalOf(root, lastIndex).get(full);
421
+ const after = result.tree.get(full);
422
+ if (after && !same(before, after)) {
423
+ const [rules, kind] = full === joinVirtual(p.exportPath, '_redirects') ? [p.redirects, 'REDIRECT'] : [p.headers, 'HEADER'];
424
+ events.push({
425
+ location: at(rules[0].span.line),
426
+ instruction: kind,
427
+ stage: stageDisplay(p.stages[lastIndex], lastIndex),
428
+ path: full,
429
+ action: before ? 'overwrite' : 'write',
430
+ digest: after.digest,
431
+ inputs: rules.map((r) => `${at(r.span.line)} ${kind}`),
432
+ });
433
+ }
434
+ return { path: want, mode: node.mode.toString(8), size: node.bytes.byteLength, digest: node.digest, origin: node.origin, history: events };
435
+ }
436
+ export function whyText(r) {
437
+ let out = `${r.path} ${r.mode} ${r.size} bytes ${r.digest}\n`;
438
+ r.history.forEach((e, i) => {
439
+ out += ` ${i + 1}. ${e.location} ${e.instruction} [stage ${e.stage}] ${e.action === 'write' ? 'wrote' : 'overwrote'} ${e.path}\n`;
440
+ for (const input of e.inputs)
441
+ out += ` from ${input}\n`;
442
+ });
443
+ return out;
444
+ }
445
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1,16 @@
1
+ import { type WriterOptions } from './archive-writers.js';
2
+ import type { ExportFormat } from './language/ast.js';
3
+ import type { Tree } from './tree.js';
4
+ export type ArchiveFormat = Exclude<ExportFormat, 'directory' | 'oci'>;
5
+ /** Single-file formats encoded by {@link encodeTree}. `oci` is handled separately (layout directory or OCI archive). */
6
+ export declare function isArchiveFormat(f: ExportFormat): f is ArchiveFormat;
7
+ /** Default output path relative to the context: `dist`, `dist.zip`, `dist.tar`, `dist.tar.gz`, `dist.wbn`. */
8
+ export declare function defaultOutName(format: ExportFormat): string;
9
+ export interface EncodeOptions extends WriterOptions {
10
+ /** Required for `wbn`: absolute https URL ending in `/`. */
11
+ readonly baseUrl?: string | undefined;
12
+ }
13
+ /** Serialize a tree in an archive format. Deterministic: see archive-writers.ts and wbn.ts. */
14
+ export declare function encodeTree(tree: Tree, format: ArchiveFormat, options?: EncodeOptions): Uint8Array;
15
+ /** The export format implied by an output file name, if it has an archive extension. */
16
+ export declare function formatFromExtension(out: string): ExportFormat | undefined;
@@ -0,0 +1,52 @@
1
+ import { writeTar, writeTgz, writeZip } from './archive-writers.js';
2
+ import { writeWebBundle } from './wbn.js';
3
+ /** Single-file formats encoded by {@link encodeTree}. `oci` is handled separately (layout directory or OCI archive). */
4
+ export function isArchiveFormat(f) {
5
+ return f !== 'directory' && f !== 'oci';
6
+ }
7
+ /** Default output path relative to the context: `dist`, `dist.zip`, `dist.tar`, `dist.tar.gz`, `dist.wbn`. */
8
+ export function defaultOutName(format) {
9
+ switch (format) {
10
+ case 'directory':
11
+ return 'dist';
12
+ case 'zip':
13
+ return 'dist.zip';
14
+ case 'tar':
15
+ return 'dist.tar';
16
+ case 'tgz':
17
+ return 'dist.tar.gz';
18
+ case 'wbn':
19
+ return 'dist.wbn';
20
+ case 'oci':
21
+ return 'dist.oci.tar';
22
+ }
23
+ }
24
+ /** Serialize a tree in an archive format. Deterministic: see archive-writers.ts and wbn.ts. */
25
+ export function encodeTree(tree, format, options = {}) {
26
+ switch (format) {
27
+ case 'zip':
28
+ return writeZip(tree, options);
29
+ case 'tar':
30
+ return writeTar(tree, options);
31
+ case 'tgz':
32
+ return writeTgz(tree, options);
33
+ case 'wbn':
34
+ return writeWebBundle(tree, options.baseUrl);
35
+ }
36
+ }
37
+ /** The export format implied by an output file name, if it has an archive extension. */
38
+ export function formatFromExtension(out) {
39
+ const name = out.toLowerCase();
40
+ if (name.endsWith('.oci.tar'))
41
+ return 'oci';
42
+ if (name.endsWith('.tar.gz') || name.endsWith('.tgz'))
43
+ return 'tgz';
44
+ if (name.endsWith('.tar'))
45
+ return 'tar';
46
+ if (name.endsWith('.zip'))
47
+ return 'zip';
48
+ if (name.endsWith('.wbn'))
49
+ return 'wbn';
50
+ return undefined;
51
+ }
52
+ //# sourceMappingURL=export-formats.js.map