@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
package/lib/src/cli.js ADDED
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import path from 'node:path';
4
+ import { SitefileError, isSitefileError } from './errors.js';
5
+ import { build, buildToDirectory, checkPlanPins, lintBuild, planBuild } from './executor.js';
6
+ import { LINT_RULES } from './lint.js';
7
+ import { defaultOutName, formatFromExtension } from './export-formats.js';
8
+ import { collectBuildArgs } from './build-args.js';
9
+ import { insecureRegistriesFromEnv, makeClient } from './base-image.js';
10
+ import { cacheLimitFromEnv } from './cache-limit.js';
11
+ import { parseCacheRef, RegistryCache } from './registry-cache.js';
12
+ import { parseLabels } from './oci.js';
13
+ import { EXPORT_FORMATS, parseExportFormat, parseOciCompression } from './language/ast.js';
14
+ import { DEFAULT_FETCH_POLICY } from './resources/types.js';
15
+ import { parseLocalRef, refKey } from './ref.js';
16
+ import { EXTRA_HELP, isExtraCommand, runExtraCommand } from './cli/index.js';
17
+ import { evictAfterBuild, transformCacheDir } from './cli/cache.js';
18
+ import { storeBuildAsImage, validateImageTags } from './cli/build-image.js';
19
+ import { SITEFILE_VERSION } from './attest.js';
20
+ import { watchLoop } from './cli/watch.js';
21
+ import { Profiler } from './profile.js';
22
+ import { watchMemo } from './memo.js';
23
+ import { promises as fs } from 'node:fs';
24
+ import { graphDot, graphText, planGraph, why, whyText } from './explain.js';
25
+ import { isGitContext, materializeGitContext } from './cli/git-context.js';
26
+ import { loadSecrets } from './secrets.js';
27
+ const VERSION = SITEFILE_VERSION;
28
+ const HELP = `sitefile ${VERSION} — Docker-inspired static artifact builder
29
+
30
+ Usage:
31
+ sitefile build [context] [options] build and export to --out (default: <context>/dist, or dist.<ext>)
32
+ sitefile validate [context] [options] parse, validate and evaluate variables (no IO)
33
+ sitefile inspect [context] [options] build in memory and list the exported files
34
+ sitefile graph [context] [options] print the dependency graph of imports, stages and operations
35
+ (text, --json or --dot; no IO, like validate)
36
+ sitefile why <path> [context] [opts] build in memory and show which instructions wrote an exported
37
+ file, from what inputs, and what overwrote it
38
+ The context may be a Git URL, https://host/repo.git#ref:dir (fetched to a temporary directory;
39
+ the default --out is then ./dist; see docs/CLI.md).
40
+
41
+ Options:
42
+ -f, --file <path> Sitefile path (default: <context>/Sitefile)
43
+ --target <stage> stop after the named stage and export its whole tree instead of
44
+ the EXPORT path (EXPORT's format still applies; no _redirects/_headers)
45
+ -o, --out <path> export destination (build only): a directory for 'directory',
46
+ a file for zip, tar, tgz, wbn
47
+ -t, --tag <name[:tag]> also store the build as a local image (build only, repeatable);
48
+ with -t and no -o nothing is written to the context
49
+ --format <fmt> directory | zip | tar | tgz (alias tar.gz) | wbn | oci (build only).
50
+ oci writes an OCI archive for -o *.tar (e.g. site.oci.tar), else an
51
+ OCI image layout directory.
52
+ Default: the --out extension (.zip .tar .tar.gz .tgz .wbn .oci.tar),
53
+ else the Sitefile's EXPORT ... AS <fmt>
54
+ --oci-root <path> directory inside an oci image layer (default /srv/www; overrides EXPORT ... AT)
55
+ --oci-compression <c> oci site layer: gzip (default) or none (plain tar; overrides EXPORT ... COMPRESSION)
56
+ --label <key=value> add an OCI image config label (repeatable)
57
+ --git-labels add org.opencontainers.image.source/revision from the git checkout
58
+ (off by default: they change the image ID on every commit)
59
+ --provenance attach a SLSA provenance attestation to the oci image (an in-toto
60
+ statement: Sitefile digest, build args, imports, BASE, builder)
61
+ --sbom attach an SBOM attestation (CycloneDX: every file with its SHA-256);
62
+ both are OCI 1.1 referrers of the image (its ID does not change),
63
+ written into the oci export and kept with -t images for push
64
+ --platform <os/arch> oci image platform (default linux/amd64); with a BASE, the platform pulled
65
+ --base <image> build the oci image on <image> (runnable; overrides EXPORT ... AS oci BASE);
66
+ a bare name means Docker Hub. Pin it: nginx:alpine@sha256:...
67
+ --insecure-registry <h> allow plain http to registry host[:port] (repeatable; localhost always may)
68
+ --base-url <url> https URL ending in '/' the files are mounted at (wbn only);
69
+ overrides EXPORT ... BASE
70
+ --build-arg <NAME=value> set an ARG (repeatable); --build-arg NAME reads $NAME
71
+ --secret id=<id>,env=<VAR> | id=<id>,src=<file>
72
+ a build secret for IMPORT ... SECRET <id> AS HEADER <Name> | AS BEARER
73
+ (repeatable); sent only to that import's origin, never recorded
74
+ --plugin-timeout <ms> per-call time limit for TRANSFORM plugins and tools (default 60000)
75
+ --allow-exec <name[=path]> allow a TOOL (external program) to run, resolved on PATH or at
76
+ the absolute path (repeatable); programs run with your privileges
77
+ --allow-plugin <name> allow a JavaScript PLUGIN to run (repeatable; wasm plugins need no flag)
78
+ --allow-plugins allow every JavaScript PLUGIN in the Sitefile
79
+ --no-cache do not read or write the TRANSFORM result cache
80
+ ($SITEFILE_HOME/cache/transform; see 'sitefile cache --help')
81
+ --cache-from <ref> also look up TRANSFORM results in this registry cache (build only,
82
+ repeatable, first match wins); results are fetched only on a hit
83
+ --cache-to <ref> after a successful build, push the TRANSFORM results it used to this
84
+ registry cache (build only, once); registry problems only warn
85
+ --build-arg-file <path> read ARGs from a .json, .yaml/.yml or .env file (repeatable;
86
+ later files win, --build-arg wins over files)
87
+ --force replace a non-empty --out outside the context
88
+ --frozen-lockfile fail instead of changing Sitefile.lock
89
+ --update-lock re-record unpinned URLs whose content changed
90
+ --strict-pins fail when a remote IMPORT has no INTEGRITY and no lockfile entry, or the
91
+ BASE image is neither @sha256:-pinned nor locked (build, validate, inspect)
92
+ --lint-disable <rules> turn lint rules off (validate; comma-separated rule ids, or all; repeatable)
93
+ --no-lockfile do not read or write Sitefile.lock
94
+ --vendor-dir <dir> read remote imports and BASE blobs from this vendor directory (relative
95
+ to the context; default vendor/sitefile when it exists; see 'sitefile vendor')
96
+ --allow-http permit plain http:// imports
97
+ --allow-private-network permit loopback/private network addresses
98
+ --allow-hardlinks permit hard-linked files in the context (you assert they are safe)
99
+ --max-download-bytes <n> per-resource size limit (default ${DEFAULT_FETCH_POLICY.maxBytes})
100
+ --timeout <ms> per-resource fetch timeout (default ${DEFAULT_FETCH_POLICY.timeoutMs})
101
+ --json machine-readable output (validate, inspect, graph, why)
102
+ --dot Graphviz DOT output (graph only)
103
+ --watch build, then rebuild when the context, the Sitefile or a
104
+ --build-arg-file changes (build only; Ctrl-C stops)
105
+ --profile <file> write a timing trace of the build (Chrome trace event JSON:
106
+ chrome://tracing, ui.perfetto.dev); --watch rewrites it per build
107
+ -h, --help show this help
108
+ -v, --version show the version
109
+ ${EXTRA_HELP}`;
110
+ /**
111
+ * Run the CLI. `run.once` runs a --watch command a single time (the watch loop's
112
+ * builds); `run.wrote` then receives the export destination, and `run.memo` carries
113
+ * results between those builds (incremental rebuilds).
114
+ */
115
+ export async function main(argv, run = {}) {
116
+ if (isExtraCommand(argv[0]))
117
+ return runExtraCommand(argv);
118
+ let parsed;
119
+ try {
120
+ parsed = parseArgs({
121
+ args: [...argv],
122
+ allowPositionals: true,
123
+ options: {
124
+ file: { type: 'string', short: 'f' },
125
+ target: { type: 'string' },
126
+ out: { type: 'string', short: 'o' },
127
+ tag: { type: 'string', short: 't', multiple: true },
128
+ format: { type: 'string' },
129
+ 'base-url': { type: 'string' },
130
+ 'oci-root': { type: 'string' },
131
+ 'oci-compression': { type: 'string' },
132
+ label: { type: 'string', multiple: true },
133
+ 'git-labels': { type: 'boolean' },
134
+ provenance: { type: 'boolean' },
135
+ sbom: { type: 'boolean' },
136
+ platform: { type: 'string' },
137
+ base: { type: 'string' },
138
+ 'insecure-registry': { type: 'string', multiple: true },
139
+ 'build-arg': { type: 'string', multiple: true },
140
+ 'build-arg-file': { type: 'string', multiple: true },
141
+ secret: { type: 'string', multiple: true },
142
+ 'plugin-timeout': { type: 'string' },
143
+ 'allow-exec': { type: 'string', multiple: true },
144
+ 'allow-plugin': { type: 'string', multiple: true },
145
+ 'allow-plugins': { type: 'boolean' },
146
+ 'no-cache': { type: 'boolean' },
147
+ 'cache-from': { type: 'string', multiple: true },
148
+ 'cache-to': { type: 'string', multiple: true },
149
+ force: { type: 'boolean' },
150
+ 'frozen-lockfile': { type: 'boolean' },
151
+ 'update-lock': { type: 'boolean' },
152
+ 'strict-pins': { type: 'boolean' },
153
+ 'lint-disable': { type: 'string', multiple: true },
154
+ 'no-lockfile': { type: 'boolean' },
155
+ 'vendor-dir': { type: 'string' },
156
+ 'allow-http': { type: 'boolean' },
157
+ 'allow-private-network': { type: 'boolean' },
158
+ 'allow-hardlinks': { type: 'boolean' },
159
+ 'max-download-bytes': { type: 'string' },
160
+ timeout: { type: 'string' },
161
+ json: { type: 'boolean' },
162
+ dot: { type: 'boolean' },
163
+ watch: { type: 'boolean' },
164
+ profile: { type: 'string' },
165
+ help: { type: 'boolean', short: 'h' },
166
+ version: { type: 'boolean', short: 'v' },
167
+ },
168
+ });
169
+ }
170
+ catch (e) {
171
+ return usageError(e.message);
172
+ }
173
+ const { values: o, positionals } = parsed;
174
+ if (o.version) {
175
+ process.stdout.write(`${VERSION}\n`);
176
+ return 0;
177
+ }
178
+ const [command, ...rest] = positionals;
179
+ if (o.help || !command) {
180
+ process.stdout.write(HELP);
181
+ return o.help ? 0 : 2;
182
+ }
183
+ // why <path> [context]
184
+ const whyPath = command === 'why' ? rest.shift() : undefined;
185
+ if (command === 'why' && whyPath === undefined)
186
+ return usageError('why expects a path in the build output: sitefile why <path> [context]');
187
+ const [contextArg = '.', ...extra] = rest;
188
+ if (extra.length)
189
+ return usageError(`unexpected arguments: ${extra.join(' ')}`);
190
+ const gitContext = isGitContext(contextArg);
191
+ if (gitContext && o.watch)
192
+ return usageError('--watch needs a local context directory, not a Git URL');
193
+ if (gitContext && o.file !== undefined && (path.isAbsolute(o.file) || o.file.split(/[\\/]/).includes('..'))) {
194
+ return usageError('with a Git URL context, -f/--file is a path inside the repository');
195
+ }
196
+ if (o.dot && command !== 'graph')
197
+ return usageError('--dot is only for graph');
198
+ if (o.dot && o.json)
199
+ return usageError('--dot and --json conflict');
200
+ if (o['frozen-lockfile'] && o['update-lock'])
201
+ return usageError('--frozen-lockfile and --update-lock conflict');
202
+ if (o['strict-pins'] && o['update-lock'])
203
+ return usageError('--strict-pins and --update-lock conflict');
204
+ const lintDisabled = (o['lint-disable'] ?? []).flatMap((v) => v.split(',').map((s) => s.trim()));
205
+ if (o['lint-disable'] && command !== 'validate')
206
+ return usageError('--lint-disable is only for validate');
207
+ for (const id of lintDisabled) {
208
+ if (id !== 'all' && !LINT_RULES.includes(id)) {
209
+ return usageError(`--lint-disable: unknown lint rule '${id}' (rules: ${LINT_RULES.join(', ')}, or all)`);
210
+ }
211
+ }
212
+ const cacheFrom = o['cache-from'] ?? [];
213
+ const cacheTo = o['cache-to'] ?? [];
214
+ if (cacheFrom.length || cacheTo.length) {
215
+ if (command !== 'build')
216
+ return usageError('--cache-from and --cache-to are only for build');
217
+ if (o.watch)
218
+ return usageError('--watch cannot be combined with --cache-from or --cache-to');
219
+ if (o['no-cache'])
220
+ return usageError('--no-cache cannot be combined with --cache-from or --cache-to');
221
+ if (cacheTo.length > 1)
222
+ return usageError('--cache-to can be given only once');
223
+ }
224
+ if (o.watch && !run.once) {
225
+ if (command !== 'build')
226
+ return usageError(`--watch is only for build (and serve --watch), not ${command}`);
227
+ if ((o.tag ?? []).length)
228
+ return usageError('--watch cannot be combined with -t');
229
+ try {
230
+ const memo = watchMemo();
231
+ return await watchLoop({
232
+ context: contextArg,
233
+ ...(o.file ? { sitefile: o.file } : {}),
234
+ argFiles: o['build-arg-file'] ?? [],
235
+ build: async () => {
236
+ const wrote = [];
237
+ return { code: await main(argv, { once: true, wrote, memo }), wrote };
238
+ },
239
+ });
240
+ }
241
+ catch (e) {
242
+ if (!isSitefileError(e))
243
+ throw e;
244
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
245
+ return 1;
246
+ }
247
+ }
248
+ if (o.profile === '')
249
+ return usageError('--profile expects a file path');
250
+ // --profile: a trace of this command (build, validate, inspect, graph, why), written when it ends.
251
+ const profiler = o.profile !== undefined ? new Profiler(command) : undefined;
252
+ if (o.profile !== undefined)
253
+ run.wrote?.push(o.profile);
254
+ const commandSpan = profiler?.begin(command, 'command');
255
+ let failed = false;
256
+ let fetched;
257
+ try {
258
+ const fetchPolicy = {
259
+ allowHttp: !!o['allow-http'],
260
+ allowPrivateNetwork: !!o['allow-private-network'],
261
+ ...(o['max-download-bytes'] ? { maxBytes: positiveInt(o['max-download-bytes'], '--max-download-bytes') } : {}),
262
+ ...(o.timeout ? { timeoutMs: positiveInt(o.timeout, '--timeout') } : {}),
263
+ };
264
+ if (gitContext) {
265
+ // A Git URL context: fetched into a temporary directory, built from there, then removed.
266
+ fetched = await materializeGitContext(contextArg, { ...DEFAULT_FETCH_POLICY, ...fetchPolicy });
267
+ process.stderr.write(`context: ${contextArg} at commit ${fetched.commit}\n`);
268
+ }
269
+ const context = fetched?.dir ?? contextArg;
270
+ const file = fetched && o.file !== undefined ? path.join(context, o.file) : o.file;
271
+ const remoteCache = cacheFrom.length || cacheTo.length
272
+ ? new RegistryCache({
273
+ from: cacheFrom.map((r) => parseCacheRef('--cache-from', r)),
274
+ to: cacheTo[0] !== undefined ? parseCacheRef('--cache-to', cacheTo[0]) : undefined,
275
+ client: (registry) => makeClient(registry, { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []), allowPrivateNetwork: !!o['allow-private-network'] }),
276
+ limit: cacheLimitFromEnv(),
277
+ warn: (m) => process.stderr.write(`warning: ${m}\n`),
278
+ notice: (m) => process.stderr.write(`${m}\n`),
279
+ })
280
+ : undefined;
281
+ // Precedence: Sitefile defaults < --build-arg-file (later files win) < --build-arg.
282
+ const buildArgs = await collectBuildArgs(o['build-arg-file'] ?? [], o['build-arg'] ?? []);
283
+ const secrets = await loadSecrets(o.secret ?? []);
284
+ let formatOverride;
285
+ if (o.format !== undefined) {
286
+ formatOverride = parseExportFormat(o.format);
287
+ if (!formatOverride)
288
+ return usageError(`--format expects one of ${EXPORT_FORMATS.join(', ')} (or tar.gz), got '${o.format}'`);
289
+ }
290
+ let ociCompression;
291
+ if (o['oci-compression'] !== undefined) {
292
+ ociCompression = parseOciCompression(o['oci-compression']);
293
+ if (!ociCompression)
294
+ return usageError(`--oci-compression expects gzip or none, got '${o['oci-compression']}'`);
295
+ }
296
+ // No --format: an archive extension on --out picks the format, so `-o site.zip`
297
+ // writes a zip instead of a directory named site.zip.
298
+ if (!formatOverride && o.out)
299
+ formatOverride = formatFromExtension(o.out);
300
+ // The default destination depends on the format, which the Sitefile may set.
301
+ const sitefileFormat = command === 'build' && !o.out && !formatOverride ? (await planBuild({ contextDir: context, ...(file ? { sitefile: file } : {}), buildArgs })).exportFormat : undefined;
302
+ // A Git context's default destination is in the current directory, not the temporary checkout.
303
+ const out = o.out ?? path.join(fetched ? '.' : context, defaultOutName(formatOverride ?? sitefileFormat ?? 'directory'));
304
+ run.wrote?.push(out);
305
+ const options = {
306
+ contextDir: context,
307
+ ...(file ? { sitefile: file } : {}),
308
+ ...(fetched ? { label: o.file ?? 'Sitefile' } : {}),
309
+ ...(o.target !== undefined ? { target: o.target } : {}),
310
+ buildArgs,
311
+ ...(secrets.size ? { secrets } : {}),
312
+ outDir: out,
313
+ fetchPolicy,
314
+ ...(o['no-lockfile'] ? { lockfile: false } : {}),
315
+ ...(o['vendor-dir'] !== undefined ? { vendorDir: o['vendor-dir'] } : {}),
316
+ ...(o['plugin-timeout']
317
+ ? { pluginPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') }, toolPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') } }
318
+ : {}),
319
+ ...(o['allow-exec'] ? { allowExec: o['allow-exec'] } : {}),
320
+ ...(o['allow-plugin'] ? { allowPlugin: o['allow-plugin'] } : {}),
321
+ allowAllPlugins: !!o['allow-plugins'],
322
+ ...(o['no-cache'] ? {} : { cacheDir: transformCacheDir() }),
323
+ ...(remoteCache ? { remoteCache } : {}),
324
+ onNotice: (m) => process.stderr.write(`${m}\n`),
325
+ allowHardlinks: !!o['allow-hardlinks'],
326
+ ...(o.platform !== undefined ? { platform: o.platform } : {}),
327
+ ...(o.base !== undefined ? { baseImage: o.base } : {}),
328
+ // inspect never pulls; a non-oci export (with no image tag) has no use for a BASE
329
+ ...(command === 'inspect' || command === 'why' || (command === 'build' && formatOverride !== undefined && formatOverride !== 'oci' && !(o.tag ?? []).length) ? { skipBase: true } : {}),
330
+ registry: { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []) },
331
+ frozenLockfile: !!o['frozen-lockfile'],
332
+ updateLock: !!o['update-lock'],
333
+ strictPins: !!o['strict-pins'],
334
+ onWarning: (m) => process.stderr.write(`warning: ${m}\n`),
335
+ ...(profiler ? { profiler, alsoWrites: [o.profile] } : {}),
336
+ ...(run.memo ? { memo: run.memo } : {}),
337
+ };
338
+ switch (command) {
339
+ case 'build': {
340
+ const tags = o.tag ?? [];
341
+ validateImageTags(tags);
342
+ const labels = parseLabels(o.label ?? []);
343
+ const attest = { provenance: !!o.provenance, sbom: !!o.sbom };
344
+ if ((attest.provenance || attest.sbom) && !tags.length && (formatOverride ?? sitefileFormat ?? (await planBuild(options)).exportFormat) !== 'oci') {
345
+ return usageError('--provenance and --sbom attach attestations to an oci image: export one (--format oci, -o *.oci.tar, EXPORT ... AS oci) or store one with -t');
346
+ }
347
+ const imageInfo = {
348
+ contextDir: context,
349
+ ociRoot: o['oci-root'],
350
+ ociCompression,
351
+ platform: o.platform,
352
+ labels,
353
+ gitLabels: !!o['git-labels'],
354
+ attest,
355
+ build: { contextDir: context, ...(file ? { sitefile: file } : {}), ...(fetched ? { label: o.file ?? 'Sitefile' } : {}), buildArgs },
356
+ };
357
+ await remoteCache?.load();
358
+ if (tags.length && !o.out) {
359
+ // -t without -o: store the image only; nothing is written to the context.
360
+ await storeBuildAsImage(await build(options), tags, imageInfo);
361
+ await remoteCache?.push(transformCacheDir());
362
+ return 0;
363
+ }
364
+ const r = await buildToDirectory({
365
+ ...options,
366
+ outDir: out,
367
+ force: !!o.force,
368
+ ...(formatOverride ? { format: formatOverride } : {}),
369
+ ...(o['base-url'] !== undefined ? { baseUrl: o['base-url'] } : {}),
370
+ ...(o['oci-root'] !== undefined ? { ociRoot: o['oci-root'] } : {}),
371
+ ...(ociCompression !== undefined ? { ociCompression } : {}),
372
+ ...(o.platform !== undefined ? { platform: o.platform } : {}),
373
+ imageLabels: labels,
374
+ gitLabels: !!o['git-labels'],
375
+ ...(tags[0] ? { ociRefName: refKey(parseLocalRef(tags[0])) } : {}),
376
+ attest,
377
+ });
378
+ const where = path.relative(process.cwd(), r.outDir) || '.';
379
+ const n = `${r.output.size} file${r.output.size === 1 ? '' : 's'}`;
380
+ process.stdout.write(r.format === 'directory'
381
+ ? `Exported ${n} to ${where} (${r.output.digest()})\n`
382
+ : r.format === 'oci'
383
+ ? `Exported ${n} to ${where} (oci image ${r.manifestDigest}${r.bytes !== undefined ? `, ${r.bytes} bytes` : ''})\n`
384
+ : `Exported ${n} to ${where} (${r.format === 'tgz' ? 'tar.gz' : r.format}, ${r.bytes} bytes, ${r.fileDigest})\n`);
385
+ // A Git context's lockfile lives in the temporary checkout: it is used, never written back.
386
+ if (r.lockfileWritten && !fetched)
387
+ process.stdout.write('Updated Sitefile.lock\n');
388
+ if (tags.length)
389
+ await storeBuildAsImage(r, tags, imageInfo);
390
+ await remoteCache?.push(transformCacheDir());
391
+ return 0;
392
+ }
393
+ case 'validate': {
394
+ const span = profiler?.begin('parse', 'build');
395
+ const p = await planBuild(options);
396
+ span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
397
+ p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
398
+ (await lintBuild(options, lintDisabled)).forEach((m) => process.stderr.write(`warning: ${m}\n`));
399
+ if (o['strict-pins'])
400
+ await checkPlanPins(options, p);
401
+ if (o.json) {
402
+ process.stdout.write(JSON.stringify({ file: p.file, vars: Object.fromEntries(p.vars), imports: p.imports, stages: p.stages, export: p.exportPath, ...(p.target !== undefined ? { target: p.target } : {}) }, null, 2) + '\n');
403
+ }
404
+ else {
405
+ const opCount = p.stages.reduce((n, st) => n + st.operations.length, 0);
406
+ process.stdout.write(`${p.file}: OK (${p.imports.length} import${p.imports.length === 1 ? '' : 's'}, ${opCount} operation${opCount === 1 ? '' : 's'}${p.stages.length > 1 ? `, ${p.stages.length} stages` : ''}${p.target !== undefined ? `, target ${p.target}` : ''})\n`);
407
+ }
408
+ return 0;
409
+ }
410
+ case 'inspect': {
411
+ const r = await build({ ...options, frozenLockfile: true });
412
+ const files = r.output.entries().map(([p, n]) => ({ path: p, mode: n.mode.toString(8), size: n.bytes.byteLength, digest: n.digest, origin: n.origin }));
413
+ if (o.json) {
414
+ process.stdout.write(JSON.stringify({ digest: r.output.digest(), files }, null, 2) + '\n');
415
+ }
416
+ else {
417
+ for (const f of files)
418
+ process.stdout.write(`${f.mode} ${String(f.size).padStart(8)} ${f.path} (${f.origin})\n`);
419
+ process.stdout.write(`${files.length} files, ${r.output.digest()}\n`);
420
+ }
421
+ return 0;
422
+ }
423
+ case 'graph': {
424
+ const span = profiler?.begin('parse', 'build');
425
+ const p = await planBuild(options);
426
+ span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
427
+ p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
428
+ if (o['strict-pins'])
429
+ await checkPlanPins(options, p);
430
+ const g = planGraph(p);
431
+ process.stdout.write(o.json ? JSON.stringify(g, null, 2) + '\n' : o.dot ? graphDot(g) : graphText(g));
432
+ return 0;
433
+ }
434
+ case 'why': {
435
+ const r = await build({ ...options, frozenLockfile: true, recordSteps: true });
436
+ const report = why(r, whyPath, context);
437
+ process.stdout.write(o.json ? JSON.stringify(report, null, 2) + '\n' : whyText(report));
438
+ return 0;
439
+ }
440
+ default:
441
+ return usageError(`unknown command '${command}'`);
442
+ }
443
+ }
444
+ catch (e) {
445
+ failed = true;
446
+ if (isSitefileError(e)) {
447
+ if (e.code === 'E_USAGE')
448
+ return usageError(e.detail);
449
+ process.stderr.write(`error[${e.code}]: ${e.message}\n`);
450
+ return 1;
451
+ }
452
+ throw e;
453
+ }
454
+ finally {
455
+ // Keep $SITEFILE_HOME under $SITEFILE_CACHE_MAX (never fails the command).
456
+ if (command === 'build' || command === 'inspect' || command === 'why')
457
+ await evictAfterBuild();
458
+ await fetched?.dispose();
459
+ commandSpan?.end();
460
+ if (profiler && !(await fs.writeFile(o.profile, profiler.toJSON()).then(() => true, () => false))) {
461
+ // A failed build already reported its error; otherwise this is the command's error.
462
+ if (!failed) {
463
+ process.stderr.write(`error[E_EXPORT]: cannot write the --profile file '${o.profile}'\n`);
464
+ // eslint-disable-next-line no-unsafe-finally
465
+ return 1;
466
+ }
467
+ }
468
+ }
469
+ }
470
+ function positiveInt(raw, flag) {
471
+ const n = Number(raw);
472
+ if (!Number.isSafeInteger(n) || n <= 0)
473
+ throw new SitefileError('E_USAGE', `${flag} expects a positive integer`);
474
+ return n;
475
+ }
476
+ function usageError(message) {
477
+ process.stderr.write(`error: ${message}\nRun 'sitefile --help' for usage.\n`);
478
+ return 2;
479
+ }
480
+ main(process.argv.slice(2)).then((code) => {
481
+ process.exitCode = code;
482
+ }, (e) => {
483
+ process.stderr.write(`internal error: ${e?.stack ?? String(e)}\n`);
484
+ process.exitCode = 70;
485
+ });
486
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * RFC 4180 CSV parser (no dependencies).
3
+ *
4
+ * - Fields may be double-quoted; inside quotes `""` is a literal quote and
5
+ * delimiters / newlines are data. Records end at LF or CRLF; a trailing
6
+ * newline is optional; a leading UTF-8 BOM is stripped.
7
+ * - Completely empty lines are skipped.
8
+ * - A quote inside an unquoted field, text after a closing quote, or an
9
+ * unterminated quote is E_DATA with line and column.
10
+ * - `header: true` (default): the first record names the columns (non-empty,
11
+ * unique) and every record must have the same number of fields. Values are
12
+ * always strings. With `header: false`, records are returned as string
13
+ * arrays and may have differing lengths.
14
+ * - Rows are ordinary objects; a `__proto__` column is stored as plain data.
15
+ */
16
+ export interface CsvOptions {
17
+ header?: boolean;
18
+ delimiter?: ',' | ';' | '\t';
19
+ }
20
+ export declare function parseCsv(text: string, file: string, options?: CsvOptions & {
21
+ header?: true;
22
+ }): Array<Record<string, string>>;
23
+ export declare function parseCsv(text: string, file: string, options: CsvOptions & {
24
+ header: false;
25
+ }): string[][];
26
+ export declare function parseCsv(text: string, file: string, options?: CsvOptions): Array<Record<string, string>> | string[][];
27
+ export declare function parseCsvRecords(text: string, file: string): Array<Record<string, string>>;
package/lib/src/csv.js ADDED
@@ -0,0 +1,117 @@
1
+ import { SitefileError } from './errors.js';
2
+ export function parseCsv(text, file, options = {}) {
3
+ const header = options.header ?? true;
4
+ const delim = options.delimiter ?? ',';
5
+ const rows = tokenize(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text, file, delim);
6
+ if (!header)
7
+ return rows.map((r) => r.fields);
8
+ const head = rows[0];
9
+ if (!head)
10
+ return [];
11
+ const seen = new Set();
12
+ head.fields.forEach((name, i) => {
13
+ const span = { file, line: head.line, column: head.cols[i] };
14
+ if (name === '')
15
+ throw new SitefileError('E_DATA', `empty CSV column name in header (column ${i + 1})`, span);
16
+ if (seen.has(name))
17
+ throw new SitefileError('E_DATA', `duplicate CSV column name '${name}'`, span);
18
+ seen.add(name);
19
+ });
20
+ return rows.slice(1).map((row) => {
21
+ if (row.fields.length !== head.fields.length) {
22
+ throw new SitefileError('E_DATA', `expected ${head.fields.length} field${head.fields.length === 1 ? '' : 's'} but found ${row.fields.length}`, { file, line: row.line, column: 1 });
23
+ }
24
+ // fromEntries defines own data properties, so `__proto__` is safe.
25
+ return Object.fromEntries(head.fields.map((name, i) => [name, row.fields[i]]));
26
+ });
27
+ }
28
+ export function parseCsvRecords(text, file) {
29
+ return parseCsv(text, file, { header: true });
30
+ }
31
+ function tokenize(text, file, delim) {
32
+ const n = text.length;
33
+ const rows = [];
34
+ let i = 0;
35
+ let line = 1;
36
+ let lineStart = 0;
37
+ const fail = (detail, l, index, ls) => {
38
+ throw new SitefileError('E_DATA', detail, { file, line: l, column: index - ls + 1 });
39
+ };
40
+ while (i < n) {
41
+ // Skip fully empty lines.
42
+ if (text[i] === '\n') {
43
+ i++;
44
+ line++;
45
+ lineStart = i;
46
+ continue;
47
+ }
48
+ if (text[i] === '\r' && text[i + 1] === '\n') {
49
+ i += 2;
50
+ line++;
51
+ lineStart = i;
52
+ continue;
53
+ }
54
+ const row = { fields: [], cols: [], line };
55
+ for (;;) {
56
+ row.cols.push(i - lineStart + 1);
57
+ let field = '';
58
+ if (text[i] === '"') {
59
+ const qLine = line;
60
+ const qIndex = i;
61
+ const qStart = lineStart;
62
+ i++;
63
+ for (;;) {
64
+ if (i >= n)
65
+ return fail('unterminated quoted field', qLine, qIndex, qStart);
66
+ const c = text[i];
67
+ if (c === '"') {
68
+ if (text[i + 1] === '"') {
69
+ field += '"';
70
+ i += 2;
71
+ continue;
72
+ }
73
+ i++;
74
+ break;
75
+ }
76
+ if (c === '\n') {
77
+ line++;
78
+ lineStart = i + 1;
79
+ }
80
+ field += c;
81
+ i++;
82
+ }
83
+ const after = text[i];
84
+ const atEnd = i >= n || after === delim || after === '\n' || (after === '\r' && text[i + 1] === '\n');
85
+ if (!atEnd)
86
+ return fail('unexpected character after closing quote', line, i, lineStart);
87
+ }
88
+ else {
89
+ const s = i;
90
+ while (i < n) {
91
+ const c = text[i];
92
+ if (c === delim || c === '\n' || (c === '\r' && text[i + 1] === '\n'))
93
+ break;
94
+ if (c === '"')
95
+ return fail('quote inside an unquoted field', line, i, lineStart);
96
+ i++;
97
+ }
98
+ field = text.slice(s, i);
99
+ }
100
+ row.fields.push(field);
101
+ if (i >= n)
102
+ break;
103
+ if (text[i] === delim) {
104
+ i++;
105
+ continue;
106
+ }
107
+ // Record terminator.
108
+ i += text[i] === '\r' ? 2 : 1;
109
+ line++;
110
+ lineStart = i;
111
+ break;
112
+ }
113
+ rows.push(row);
114
+ }
115
+ return rows;
116
+ }
117
+ //# sourceMappingURL=csv.js.map