@johnhenry/sitefile 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -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 +120 -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 +119 -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/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
package/README.md CHANGED
@@ -1,3 +1,514 @@
1
- # Temporary Holding Version
1
+ # Sitefile
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/%40johnhenry%2Fsitefile.svg)](https://www.npmjs.com/package/@johnhenry/sitefile)
4
+ [![CI](https://github.com/johnhenry/sitefile/actions/workflows/ci.yml/badge.svg)](https://github.com/johnhenry/sitefile/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/%40johnhenry%2Fsitefile.svg)](LICENSE)
6
+
7
+ Full documentation: [opensource.johnhenry.me/sitefile](https://opensource.johnhenry.me/sitefile/)
8
+
9
+ Docker-inspired static artifact builder. Compose immutable filesystem trees from
10
+ local and remote sources, render templates, and export a deployable directory —
11
+ no container runtime, no shell, no runtime dependencies.
12
+
13
+ ```
14
+ # examples/hello/Sitefile
15
+ ARG TITLE = "Hello"
16
+ LET GREETING = "Welcome to ${TITLE}."
17
+ COPY ./public/ /
18
+ RENDER --escape=html ./templates/index.html TO /index.html
19
+ EXPORT / AS directory
20
+ ```
21
+
22
+ ```bash
23
+ sitefile build examples/hello --build-arg TITLE="My site"
24
+ ```
25
+
26
+ This writes `examples/hello/dist/index.html` (with the title substituted) and
27
+ `examples/hello/dist/style.css`.
28
+
29
+ ## Contents
30
+
31
+ - [Install](#install)
32
+ - [Status](#status)
33
+ - [CLI](#cli)
34
+ - [Variants: `sitefile bake`](#variants-sitefile-bake)
35
+ - [Watch mode](#watch-mode)
36
+ - [Profiling a build](#profiling-a-build)
37
+ - [Images and servers](#images-and-servers)
38
+ - [Export formats](#export-formats)
39
+ - [Rust implementation](#rust-implementation)
40
+ - [Editor support](#editor-support)
41
+ - [Docs](#docs)
42
+ - [Development](#development)
43
+ - [Adding a new plugin](#adding-a-new-plugin)
44
+ - [Security model](#security-model)
45
+ - [Family](#family)
46
+ - [License](#license)
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ npm install -g @johnhenry/sitefile # the `sitefile` command (TypeScript implementation)
52
+ npx @johnhenry/sitefile build . # or without installing
53
+ ```
54
+
55
+ Requires Node.js 26 or later; the npm package has no runtime dependencies.
56
+ The Rust binary (faster, and the one that provides `sitefile lsp` for editors)
57
+ builds from this repository: `cargo install --path rust`. Prebuilt binaries are
58
+ not published yet (johnhenry/sitefile#69).
59
+
60
+ `@johnhenry/sitefile` is a new package: it was never published to npm under any
61
+ name, and `0.0.0` is its first version.
62
+
63
+ The library API is the same engine the CLI uses (`import { … } from
64
+ "@johnhenry/sitefile"`); see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
65
+
66
+ ## Status
67
+
68
+ Milestones **M0** (local vertical slice), **M1** (remote inputs) and **M2**
69
+ (trees and stages: multi-stage builds, `COPY --from`, archive imports,
70
+ `.sitefileignore`; see `examples/multistage`) are implemented and tested; see [docs/TASKS.md](docs/TASKS.md). Requires Node.js 22+,
71
+ no runtime dependencies.
72
+
73
+ | Instruction | Supported |
74
+ | --- | --- |
75
+ | `FROM scratch\|<dir>\|<stage>\|image <ref> [AT <dir>] [AS name]` | yes; several FROMs make a multi-stage build, the last stage is exported; `image` starts from a site image's files (a theme: `examples/theme`, `examples/themed-site`), locked in `Sitefile.lock` when it comes from a registry |
76
+ | `IMPORT name FROM <dir>\|git+https://…#ref:dir AS sitefile [ARG NAME=value]…` | yes; the stages of another Sitefile, local or from Git, as `name+<stage>` in `FROM` and `--from` (like Earthly targets; `examples/shared-stages`), with its own ARGs and paths, pinned in `Sitefile.lock` |
77
+ | `ARG NAME[: type] [= value]` | yes, `string`/`number`/`boolean`, `--build-arg` |
78
+ | `LET NAME[: type] = value` | yes |
79
+ | `IMPORT name FROM <path or https URL> [AS file\|text\|json\|yaml\|csv\|markdown\|wbn\|tree] [INTEGRITY sha256:…] [SORT BY key [DESC]] [SCHEMA <file>] [DRAFTS true\|false]` | yes; `AS tree` unpacks tar, tar.gz, zip and Web Bundles; `AS markdown` reads posts with frontmatter (`SCHEMA` checks it against a YAML/JSON schema); `AS yaml`/`csv`/`wbn` give data for templates |
80
+ | `COPY [--overwrite] [--from=<stage>] <source> <dest>` | yes; source is a local path, `name:/path`, `<<EOF` inline content, or with `--from` an absolute path in an earlier stage |
81
+ | `RENDER [--escape=html] [--overwrite] [--from=<stage>] <template> TO <dest> [FOR item IN list [BY key] [PAGE n [FIRST <dest>]]]` | yes; template is a file or `<<EOF` inline body; `${NAME}`, `${json.path}`, `${#each}`, `${#if}`, `${x\|raw}`; `FOR` renders one page per element, `BY` one per tag, `PAGE` paginates |
82
+ | `PLUGIN name FROM ./plugins/x.mjs\|x.wasm [INTEGRITY sha256:…]` | yes; a custom renderer: a sandboxed JS module (runs only with `--allow-plugin name`) or an import-free WebAssembly module (no flag needed; see [docs/PLUGINS.md](docs/PLUGINS.md), `examples/wasm/`) |
83
+ | `TRANSFORM [--include/--exclude=<pattern>] [<source>] WITH name TO <dest> [OPTIONS k=v …] [FOR item IN list]` | yes; e.g. text → animated SVG (`examples/motion/`), responsive images (`examples/image/`); a directory source runs once per file (`TO /img/thumbs/*.png`) |
84
+ | `TOOL name` + `TRANSFORM … WITH name TO <dest> [OPTIONS …] [FOR …] ARGS …` | yes; run an external program (e.g. `ffmpeg`) you allow with `--allow-exec name`: argv only, no shell, temp files, scrubbed environment (see [docs/PLUGINS.md](docs/PLUGINS.md)) |
85
+ | `SITEMAP BASE <url> [TO <dest>]` | yes; a sitemaps.org `/sitemap.xml` of the `.html` pages so far |
86
+ | `FEED <import> BASE <url> LINK <path> [TO <dest>] [TITLE <text>]` | yes; an Atom `/feed.xml` from Markdown posts (`title` and `date` required) |
87
+ | `REDIRECT <from> <to> [STATUS 301\|302\|303\|307\|308]`, `HEADER <pattern> <Name>: <value>` | yes; `/_redirects` and `/_headers` for Netlify / Cloudflare Pages (and `sitefile serve`); a `wbn` export also bakes HEADER values into each response and exact-path REDIRECTs into 3xx responses (splats stay in `/_redirects`) |
88
+ | `CHECKLINKS [--warn] [--external]` | yes; fails (or warns) on links to files that do not exist, with file and line; `--external` requests http(s) links under the network policy |
89
+ | `FINGERPRINT [--overwrite] <pattern>… [MANIFEST <dest>]` | yes; renames assets to `name.<hash>.ext` and rewrites the HTML and CSS that reference them, plus a JSON manifest |
90
+ | `EXPORT <path> [AS directory\|zip\|tar\|tgz\|wbn\|oci] [BASE <https-url>] [AT <root>] [COMPRESSION gzip\|none]` | yes; archives are deterministic, `wbn` is an unsigned Web Bundle (needs `BASE` or `--base-url`; `COMPRESSION gzip` gzips it, default `none`), `oci` is a single-layer OCI image of the files (under `AT`, default `/srv/www`; `COMPRESSION` sets its layer, default `gzip`) |
91
+
92
+ Templates can also live in the Sitefile itself (see `examples/inline/`):
93
+
94
+ ```
95
+ RENDER --escape=html <<HTML TO /index.html
96
+ <h1>${TITLE}</h1>
97
+ HTML
98
+ ```
99
+
100
+ A blog from Markdown posts with frontmatter (see `examples/blog/`):
101
+
102
+ ```
103
+ ARG DRAFTS: boolean = false # --build-arg DRAFTS=true previews drafts
104
+ IMPORT posts FROM ./posts AS markdown SORT BY date DESC SCHEMA ./post.schema.yaml DRAFTS ${DRAFTS}
105
+ RENDER --escape=html ./templates/index.html TO /page/${page.number}/index.html FOR page IN posts PAGE 10 FIRST /index.html
106
+ RENDER --escape=html ./templates/post.html TO /posts/${post.slug}/index.html FOR post IN posts
107
+ RENDER --escape=html ./templates/tag.html TO /tags/${tag.key}/index.html FOR tag IN posts BY tags
108
+ FEED posts BASE ${SITE_URL} LINK /posts/${item.slug}/index.html TITLE ${SITE_TITLE}
109
+ SITEMAP BASE ${SITE_URL}
110
+ FINGERPRINT /style.css MANIFEST /asset-manifest.json
111
+ CHECKLINKS
112
+ REDIRECT /feed /feed.xml
113
+ HEADER /*.css Cache-Control: public, max-age=3600
114
+ ```
115
+
116
+ Data files are readable too (see `examples/data/`):
117
+
118
+ ```
119
+ IMPORT site FROM ./site.yaml AS yaml # ${site.nav.0.title}
120
+ IMPORT team FROM ./team.csv AS csv SORT BY joined # FOR person IN team
121
+ IMPORT bundle FROM ./vendor/hello.wbn AS wbn # ${#each bundle.entries as e}
122
+ IMPORT hello FROM ./vendor/hello.wbn AS tree # COPY hello:/ /hello/
123
+ ```
124
+
125
+ Custom renderers are plugins that run in a sandboxed Node process (see `examples/motion/`):
126
+
127
+ ```
128
+ PLUGIN motion FROM ./plugins/motion.mjs
129
+ TRANSFORM ./title.txt WITH motion TO /title.svg OPTIONS style=typewriter
130
+ TRANSFORM WITH motion TO /cards/${post.slug}.svg OPTIONS text=${post.title} FOR post IN posts
131
+ ```
132
+
133
+ A JavaScript plugin is code from the repository you are building, so it runs only when you allow it:
134
+
135
+ ```
136
+ sitefile build --allow-plugin motion # or --allow-plugins for every PLUGIN
137
+ ```
138
+
139
+ WebAssembly plugins (`PLUGIN … FROM ./x.wasm`) import nothing, can only compute, and need no flag (see `examples/wasm/`).
140
+
141
+ Images: the first-party image plugin (`plugins/image/image.wasm`, WebAssembly) decodes PNG, JPEG, GIF and WebP, resizes with a fixed Lanczos-3 filter and writes PNG, JPEG or lossless WebP, with a metadata file listing each variant's path, width, height and a ready `srcset` (see [docs/PLUGINS.md](docs/PLUGINS.md#the-image-plugin), `examples/image/`):
142
+
143
+ ```
144
+ PLUGIN image FROM ./plugins/image.wasm INTEGRITY sha256:…
145
+ TRANSFORM ./photos/sky.jpg WITH image TO /img/ OPTIONS widths=400,800,1200 quality=80
146
+ ```
147
+
148
+ External programs are explicit opt-ins: declare a `TOOL`, and allow it on the command line.
149
+
150
+ ```
151
+ TOOL ffmpeg
152
+ TRANSFORM WITH ffmpeg TO /intro.mp4 ARGS -f lavfi -i "color=c=black:s=1280x720:d=3" -fflags +bitexact -map_metadata -1 {output}
153
+ ```
154
+
155
+ ```
156
+ sitefile build --allow-exec ffmpeg
157
+ ```
158
+
159
+ TRANSFORM results are cached in `$SITEFILE_HOME/cache/transform` (`--no-cache` to skip; `sitefile cache info|prune`).
160
+ CI runs and teammates can share them through an OCI registry: `--cache-to <registry/name:tag>` pushes the results a
161
+ build used, `--cache-from <ref>` reuses them (see [docs/CLI.md](docs/CLI.md#registry-cache)).
162
+
163
+ ## CLI
164
+
165
+ ```
166
+ sitefile build [context] [-o dist] [--format directory|zip|tar|tgz|wbn] [--base-url URL] [--compression gzip|none]
167
+ [--build-arg K=V | --build-arg K] [--build-arg-file vars.json|.yaml|.env] [--force]
168
+ [--frozen-lockfile | --update-lock] [--strict-pins] [--allow-http] [--allow-private-network]
169
+ [--allow-hardlinks] [--allow-exec name[=/abs/path]]... [--allow-plugin name]... [--allow-plugins]
170
+ [--no-cache] [--cache-from registry/name:tag]... [--cache-to registry/name:tag] [--watch] [--target stage] [--profile trace.json]
171
+ sitefile validate [context] [--json] [--target stage]
172
+ sitefile inspect [context] [--json] [--target stage]
173
+ sitefile graph [context] [--json | --dot] # dependency graph of imports, stages and operations
174
+ sitefile why <path> [context] [--json] # which instructions wrote an exported file, and what overwrote it
175
+ sitefile vendor [context] [--vendor-dir vendor/sitefile] # download remote inputs for offline builds
176
+ sitefile outdated [context] [--json] # compare remote imports and BASE images with their pins (read-only)
177
+ sitefile bake [targets...] [-f sitefile.bake.yaml] [--print [--json]] [-j n] # build several variants
178
+
179
+ # images and servers (see below and docs/CLI.md)
180
+ sitefile build [context] -t name[:tag] # also store the build as an image
181
+ sitefile images | image inspect|diff|prune | tag | rmi | save
182
+ sitefile cache info | cache prune [--older-than days] # TRANSFORM result cache
183
+ sitefile cache du # cache sizes; SITEFILE_CACHE_MAX bounds them (default 1G)
184
+ sitefile serve [-d] [--name n] [-p [host:]port] [--spa] <image|./dir|site.zip>
185
+ sitefile serve --watch [-p [host:]port] [--spa] [context] # build in memory, serve, live reload
186
+ sitefile servers [-a] | server stop|rm [-f]|restart|logs [-f] <name>
187
+ ```
188
+
189
+ `--target <stage>` (like `docker build --target`) stops after a named stage and exports that stage's whole
190
+ tree, in the EXPORT format but without the EXPORT path or `_redirects`/`_headers`; an unknown name is a usage
191
+ error listing the stages. See [docs/CLI.md](docs/CLI.md#build-target---target-stage).
192
+ `sitefile graph` and `sitefile why` are described in [docs/CLI.md](docs/CLI.md#explaining-a-build-graph-and-why).
193
+
194
+ ## Variants: `sitefile bake`
195
+
196
+ Like `docker buildx bake`, `sitefile bake` builds several variants of a site (locales, staging and production)
197
+ from one bake file, `sitefile.bake.yaml` (or `.yml`, `.json`), with the outputs side by side:
198
+
199
+ ```yaml
200
+ groups:
201
+ default: [site]
202
+ targets:
203
+ site:
204
+ matrix: # one target per combination: site-en-staging, site-en-production, ...
205
+ locale: [en, fr]
206
+ env: [staging, production]
207
+ arg-files: ["env/${env}.env"] # ${name} is a matrix value
208
+ args: {LOCALE: "${locale}"}
209
+ output: dist/${env}/${locale}
210
+ ```
211
+
212
+ ```bash
213
+ cd examples/bake && sitefile bake # four builds, up to 4 at a time (-j), sharing the TRANSFORM cache
214
+ sitefile bake --print # the resolved targets (--json for tools), without building
215
+ ```
216
+
217
+ Targets also take `context`, `file`, `target` (a stage), `format`, `tags` and `inherits`. Every target runs even
218
+ when one fails (then the exit code is 1); outputs that overlap or a tag used twice are refused before anything is
219
+ built. See [docs/CLI.md](docs/CLI.md#variants-sitefile-bake).
220
+
221
+ ## Watch mode
222
+
223
+ ```bash
224
+ sitefile build --watch examples/hello # rebuild dist/ whenever the context, Sitefile or a --build-arg-file changes
225
+ sitefile serve --watch examples/hello # build in memory, serve on 127.0.0.1, reload open pages after each rebuild
226
+ ```
227
+
228
+ Rebuilds are debounced (100 ms) and changes made during a build become one follow-up build. The output
229
+ directory, `.git`, `Sitefile.lock` and the store are not watched. A failed rebuild prints the error and keeps the
230
+ last good output (exports are swapped in atomically; `serve --watch` keeps serving the last good build). Live
231
+ reload is a small script added to HTML responses only under `serve --watch`, listening on the reserved
232
+ `/__sitefile/reload` event stream; it is never written to exports. Rebuilds are incremental: steps whose inputs
233
+ are unchanged (by content digest) are reused from the previous build, in memory, and the result is identical to a
234
+ clean build (`SITEFILE_NO_INCREMENTAL=1` turns it off). See [docs/CLI.md](docs/CLI.md#watch-mode).
235
+
236
+ ## Profiling a build
237
+
238
+ ```bash
239
+ sitefile build examples/blog --profile trace.json # open in ui.perfetto.dev or chrome://tracing
240
+ ```
241
+
242
+ `--profile` writes a Chrome trace of the build: parsing, each import, plugin start, stage, operation, TRANSFORM call
243
+ (with its cache hit or miss), the BASE pull and the export, so slow steps stand out. See
244
+ [docs/CLI.md](docs/CLI.md#profiling---profile).
245
+
246
+ ## Images and servers
247
+
248
+ Builds can be kept in a local, Docker-like **image store**, which is a standard
249
+ [OCI image layout](https://github.com/opencontainers/image-spec/blob/main/image-layout.md)
250
+ in `$SITEFILE_HOME` (default `~/.sitefile`). An image is the site's files as one
251
+ layer; its ID is the manifest digest, and building identical output gives the same ID.
252
+
253
+ ```bash
254
+ sitefile build examples/hello -t hello:1 # no -o: stored only, nothing written to the context
255
+ sitefile images # REPOSITORY TAG IMAGE ID CREATED FILES SIZE
256
+ sitefile save hello:1 -o hello.oci.tar # or .zip / .tar.gz / .wbn / a directory
257
+ sitefile serve -d --name site -p 8080 hello:1 # serve it, in the background
258
+ sitefile servers; sitefile server logs site; sitefile server stop site; sitefile server rm site
259
+ ```
260
+
261
+ `serve` serves GET/HEAD only on `127.0.0.1` (never `0.0.0.0` unless you ask for it in `-p`), with ETags,
262
+ correct MIME types and no directory listings; it serves `/404.html` for missing paths, `/about` from
263
+ `/about.html`, and honours the site's `_redirects` and `_headers` (exact paths and `/*` splats). `EXPORT / AS oci` (or `build -o site.oci.tar`) writes the
264
+ same OCI image without the store; on its own it contains files only, but `EXPORT / AS oci BASE nginx:alpine
265
+ AT /usr/share/nginx/html` (or `--base`) builds a runnable image on top of a pulled base, and `sitefile push` / `pull`
266
+ move images to and from any OCI registry. `--provenance` and `--sbom` attach a SLSA provenance statement and a
267
+ CycloneDX file list to the image as OCI 1.1 referrers (deterministic, pushed with it; the image ID does not change). See
268
+ [docs/CLI.md](docs/CLI.md).
269
+
270
+ ## Export formats
271
+
272
+ ```bash
273
+ sitefile build examples/hello --format zip # examples/hello/dist.zip
274
+ sitefile build examples/hello -o hello.tar.gz # format from the extension
275
+ sitefile build examples/hello --format wbn --base-url https://example.com/
276
+ sitefile build examples/hello -o hello.wbn.gz --base-url https://example.com/ # gzip(Web Bundle)
277
+ ```
278
+
279
+ Archives are reproducible: sorted entries, fixed timestamps (or `SOURCE_DATE_EPOCH`),
280
+ uid/gid 0, modes 0644/0755. See [docs/LANGUAGE.md](docs/LANGUAGE.md#export-formats).
281
+
282
+ ## Rust implementation
283
+
284
+ `rust/` is a full port, maintained alongside the TypeScript version and checked
285
+ against it by `node conformance/run.mjs` (identical output on every example and
286
+ case). The Rust binary is the shipped product. TypeScript is the reference
287
+ specification and the library API, and both get every feature (see
288
+ [docs/RUST.md](docs/RUST.md#roles-decided-in-82)). It starts in ~4 ms instead of ~70 ms and is 1.3–2× faster on large
289
+ sites, with a fraction of the memory; see [docs/RUST.md](docs/RUST.md) and
290
+ [bench/README.md](bench/README.md).
291
+
292
+ ```bash
293
+ (cd rust && cargo build --release) && rust/target/release/sitefile build examples/blog
294
+ ```
295
+
296
+ ## Editor support
297
+
298
+ Highlighting works on its own; the language features (diagnostics,
299
+ completion, hover, go to definition, references, rename, outline) come from
300
+ the Rust binary's language server, `sitefile lsp`
301
+ ([docs/LSP.md](docs/LSP.md)), so put it on `PATH` with
302
+ `cargo install --path rust` or point the editor at it.
303
+
304
+ - **VS Code**: [editors/vscode/](editors/vscode/), a TextMate grammar plus a
305
+ language client that starts `sitefile lsp` (`sitefile.server.path`). Not
306
+ published; `(cd editors/vscode && npm install && npm run package)`, then
307
+ `code --install-extension editors/vscode/sitefile-0.2.0.vsix`.
308
+ - **Zed**: [editors/zed/](editors/zed/), tree-sitter highlighting and the
309
+ `sitefile-lsp` language server; **zed: install dev extension** on
310
+ `editors/zed`.
311
+ - **Neovim**: [editors/neovim/](editors/neovim/), tree-sitter queries and a
312
+ `vim.lsp.config` snippet for `sitefile lsp`.
313
+ - **Helix**: [editors/helix/](editors/helix/), queries and a `languages.toml`
314
+ with the grammar and the `sitefile-lsp` server.
315
+
316
+ The grammar behind Zed, Neovim and Helix is
317
+ [editors/tree-sitter-sitefile/](editors/tree-sitter-sitefile/). Without an
318
+ extension, `"files.associations": { "Sitefile": "dockerfile" }` is a usable
319
+ stopgap in VS Code.
320
+
321
+ ## Docs
322
+
323
+ - [docs/CLI.md](docs/CLI.md) — every command, the image store layout, reference grammar
324
+ - [docs/LANGUAGE.md](docs/LANGUAGE.md) — syntax and the exact semantics chosen for v0.1
325
+ - [docs/RUST.md](docs/RUST.md) — the Rust port: layout, parity checks, known differences
326
+ - [docs/LSP.md](docs/LSP.md) — the language server for editors (`sitefile lsp`, Rust binary)
327
+ - [docs/PLUGINS.md](docs/PLUGINS.md) — writing renderer plugins, and the sandbox
328
+ - [docs/DOCKER.md](docs/DOCKER.md) — Sitefile next to Docker: site images Docker runs, Dockerfile stages, shared registries
329
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — invariants and module layout
330
+ - [docs/MILESTONES.md](docs/MILESTONES.md), [docs/TASKS.md](docs/TASKS.md)
331
+ - [AGENTS.md](AGENTS.md) — the verification loop and rules for anyone (human or agent) changing this repo
332
+
333
+ ## Development
334
+
335
+ ```bash
336
+ npm ci
337
+ npm test # compile with tsc, then run node:test suites
338
+ npm run typecheck
339
+ (cd rust && cargo build --release && cargo test --release)
340
+ npm run conformance # both implementations on every example and case
341
+ ```
342
+
343
+ From a clone, `node lib/src/cli.js` runs the TypeScript CLI after `npm run build`.
344
+ See [AGENTS.md](AGENTS.md) for the full verification loop CI runs.
345
+
346
+ ## Adding a new plugin
347
+
348
+ A plugin is a custom renderer that `TRANSFORM` calls. Write one ES module in its
349
+ own folder that exports `transform(request)` and returns the bytes of the output
350
+ file (or `{ files }` for several), declare it with `PLUGIN`, and allow it when you
351
+ build:
352
+
353
+ ```js
354
+ // plugins/upper/upper.mjs
355
+ export const apiVersion = 1;
356
+ export async function transform(request) {
357
+ return { bytes: (request.input?.text ?? '').toUpperCase() };
358
+ }
359
+ ```
360
+
361
+ ```
362
+ PLUGIN upper FROM ./plugins/upper/upper.mjs
363
+ TRANSFORM ./title.txt WITH upper TO /title.txt
364
+ ```
365
+
366
+ ```bash
367
+ sitefile build --allow-plugin upper
368
+ ```
369
+
370
+ A WebAssembly plugin that imports nothing needs no flag. The request shape, the
371
+ directory form (one call per file), the ABI for `.wasm` plugins, caching and the
372
+ first-party image plugin are in [docs/PLUGINS.md](docs/PLUGINS.md); a complete
373
+ example is `examples/motion/`. Run an external program instead with `TOOL` and
374
+ `--allow-exec` (same document).
375
+
376
+ ## Security model
377
+
378
+ A Sitefile can come from someone else (a Git URL, a theme image, a shared
379
+ stage), so building one is treated as running untrusted input. Templates,
380
+ imports and WebAssembly plugins are data; JavaScript plugins and external
381
+ tools are code, and run only when you name them on the command line.
382
+
383
+ **What Sitefile guarantees:**
384
+
385
+ - **Local reads stay inside the build context.** No `..`, absolute paths or
386
+ symlinks out; hard-linked files are refused (`E_HARDLINK`) unless
387
+ `--allow-hardlinks`, since a hard link can alias a file outside the context.
388
+ A `.sitefileignore` (gitignore-style subset) at the context root excludes files
389
+ from directory copies; explicitly named files are never ignored.
390
+ - **Remote imports are `https:` only, to public addresses only** — checked at
391
+ connect time and on every redirect (SSRF), size- and time-limited, never
392
+ decompressed or executed. `--allow-http` and `--allow-private-network` are the
393
+ only ways to widen this. An explicit proxy (`--proxy` / `$SITEFILE_PROXY`;
394
+ `HTTP(S)_PROXY` is never read) does not widen it either: sitefile resolves and
395
+ checks the target itself before handing it to the proxy. `--allow-hosts` and
396
+ `--deny-hosts` narrow it further, on every hop
397
+ ([docs/CLI.md](docs/CLI.md#network-proxy-host-rules-and-timeouts)).
398
+ - **Pins are enforced.** Unpinned URLs are recorded in `Sitefile.lock` and must
399
+ match on later builds; `--strict-pins` refuses any remote import or BASE image
400
+ that is not pinned (`INTEGRITY`, `@sha256:` or a lockfile entry).
401
+ - **Git imports run no `git`.** `IMPORT … FROM git+https://host/repo.git#ref:path`
402
+ speaks protocol v2 over the same HTTPS client and policy, checks every object's
403
+ SHA-1, records the commit in `Sitefile.lock`, keeps symlinks inside the tree and
404
+ refuses submodules ([docs/LANGUAGE.md](docs/LANGUAGE.md#git-sources-githttps)).
405
+ - **Secrets go only where they are sent.** `--secret id=token,env=API_TOKEN` with
406
+ `IMPORT … SECRET token AS BEARER` (or `AS HEADER X-Api-Key`) sends the secret
407
+ only to that import's own origin, and never into the output, caches,
408
+ `Sitefile.lock`, vendor files, provenance, traces or messages.
409
+ - **Nothing executes unless you allow it by name.** A JavaScript `PLUGIN` runs
410
+ only with `--allow-plugin <name>`, in its own Node process under Node's
411
+ permission model (read access to its own folder only, no writes, network,
412
+ processes or environment); a `TOOL` runs only with `--allow-exec <name>`, argv
413
+ only, no shell, scrubbed environment. The Sitefile cannot allow itself. A
414
+ WebAssembly plugin must import nothing (checked before the build starts) and
415
+ runs with a time and memory limit, so it needs no flag
416
+ ([docs/PLUGINS.md](docs/PLUGINS.md#trust-model---allow-plugin)).
417
+ - **No silent overwrites.** Writing different content to an existing path fails
418
+ unless `--overwrite` is given. Two output paths that differ only in case
419
+ (`/A.html`, `/a.html`) always fail (`E_PATH_COLLISION`), and every path is
420
+ Unicode NFC, so a build gives the same files and image IDs on macOS, Windows
421
+ and Linux.
422
+ - **Exports are atomic.** A directory export is staged in a temporary sibling and
423
+ swapped in; a non-empty destination outside the context is only replaced with
424
+ `--force`. Archive exports (`zip`, `tar`, `tgz`, `wbn`) write one file via a
425
+ temporary sibling and rename; `--out` may not be the context, its parent, root
426
+ or home, nor a directory or symlink, and an existing file outside the context
427
+ needs `--force`.
428
+ - **Offline builds are verified.** `sitefile vendor` stores every remote import
429
+ (and the BASE image's blobs) under `vendor/sitefile/` by content digest; builds
430
+ then read them from there, verified, with no network access
431
+ ([docs/LANGUAGE.md](docs/LANGUAGE.md#vendoring-sitefile-vendor)).
432
+ - **`sitefile serve` listens on `127.0.0.1`**, GET/HEAD only, no directory
433
+ listings, unless `-p` names another address.
434
+
435
+ **What is still yours:**
436
+
437
+ - **What you allow is trusted code.** Node calls its permission model a "seat
438
+ belt" for trusted code, not a defence against deliberately malicious code.
439
+ `--allow-plugin`/`--allow-plugins` on a Sitefile you did not write runs its
440
+ author's JavaScript on your machine; `--allow-exec` runs a program on your
441
+ `PATH` with the arguments the Sitefile chose. Read them first.
442
+ - **The content of what you build.** `RENDER --escape=html` escapes template
443
+ values; `${x|raw}` and templates without it do not. Markdown and data you
444
+ import from elsewhere end up in your pages as written.
445
+ - **Unpinned remote content.** The lockfile catches a change; it does not tell
446
+ you whether the first version you locked was the one you meant. Use
447
+ `INTEGRITY`, `@sha256:` and `--strict-pins` for builds that must be
448
+ reproducible.
449
+ - **Web Bundles are unsigned.** `EXPORT … AS wbn` writes an unsigned b2 bundle;
450
+ signing (for example for an Isolated Web App) happens outside Sitefile.
451
+ - **Widening flags are yours to justify.** `--allow-http`,
452
+ `--allow-private-network`, `--allow-hardlinks`, `--force` and `serve -p
453
+ 0.0.0.0:…` each turn off one of the guarantees above for that build.
454
+
455
+ ## Family
456
+
457
+ Sitefile is the build orchestrator of the @johnhenry family's static tools. It
458
+ runs the pipeline (pinned imports, stages, templates, sandboxed plugins, checks,
459
+ exports); its siblings write file trees in code and serve the result. It has no
460
+ runtime dependency on either: they meet at file formats, a directory, a zip or
461
+ a `.wbn` Web Bundle.
462
+
463
+ - **[`@johnhenry/fileable`](https://github.com/johnhenry/fileable)** -- writes
464
+ file trees in JSX (`Dir`, `File`, `Rm`), for scaffolding and codegen. A
465
+ fileable build's output is an ordinary Sitefile input. Neither package
466
+ depends on the other.
467
+ - **[`@johnhenry/packfile`](https://github.com/johnhenry/packfile)** -- the
468
+ family's Web Bundle reader and in-memory server. It reads what `EXPORT … AS
469
+ wbn` writes and serves it as a `(Request) => Response` handler.
470
+ `@johnhenry/packfile` is a devDependency here for
471
+ `test/packfile-interop.test.ts` only.
472
+
473
+ ### Working together: what works today
474
+
475
+ Checked on 2026-11-07 against the npm releases (`@johnhenry/sitefile` 0.0.0,
476
+ `@johnhenry/fileable` 0.1.1, `@johnhenry/packfile` 0.2.0) and each
477
+ repository's `main`. "Fixed on main, in the next release" means merged but
478
+ not published: the npm versions still behave as the rest of that row says.
479
+
480
+ | From | To | How | Status |
481
+ | --- | --- | --- | --- |
482
+ | fileable, loose output | Sitefile | `COPY ./generated/ /` | Works |
483
+ | fileable, `encode="zip"` | Sitefile | `IMPORT gen FROM ./site.zip AS tree`, then `COPY gen:/ /` | Works (same output as loose) |
484
+ | fileable, `encode="wbn"` | Sitefile | `IMPORT … AS tree` | **Not yet:** it's gzip around a Web Bundle, and `AS tree` reads only plain bundles ([#141](https://github.com/johnhenry/sitefile/issues/141)) |
485
+ | fileable tree, rendered during a build | Sitefile | a JavaScript `TRANSFORM` plugin calling fileable's in-memory `toFiles()` | An example on main, not part of the npm package: [`examples/fileable/`](examples/fileable/) ([#145](https://github.com/johnhenry/sitefile/issues/145)). fileable is vendored into the plugin folder; `cmd`, local `src=` and `env://` fail with `E_PLUGIN` |
486
+ | Sitefile, `EXPORT / AS wbn BASE <url>` | packfile | `fromWebBundle(buffer, { baseURL })` from `@johnhenry/packfile/web-bundle`, then `createRouter(files)` | Works; tested here (`test/packfile-interop.test.ts`), and packfile's own tests read a bundle written by Sitefile 0.0.0 |
487
+ | Sitefile `.wbn` | packfile `fromArchive()` | with 0.0.0 / 0.2.0: gzip it, and export with `BASE https://packfile.invalid/` | Works with that workaround. **Fixed on main, in the next release:** `EXPORT … AS wbn COMPRESSION gzip` (or `-o site.wbn.gz`) writes the gzip ([#143](https://github.com/johnhenry/sitefile/issues/143)), and `fromArchive()` takes a `baseURL` or the origin of the bundle's primary URL, so any `BASE` reads back ([packfile#18](https://github.com/johnhenry/packfile/issues/18)) |
488
+ | Sitefile `HEADER` / `REDIRECT` | packfile `createWebBundleRouter()` | -- | **Not applied in 0.0.0:** only the `/_headers` and `/_redirects` files are in the bundle. **Fixed on main, in the next release:** each response carries its HEADER values, and exact-path 301/302/303/307/308 REDIRECTs are 3xx responses with `location` ([#144](https://github.com/johnhenry/sitefile/issues/144)). Splats, `200` rewrites and `404` rules stay in `/_redirects` only |
489
+ | packfile `toArchive()` | Sitefile | `IMPORT … AS tree` | **Not yet** (same as fileable's `.wbn`, [#141](https://github.com/johnhenry/sitefile/issues/141)) |
490
+
491
+ Gotchas when serving a Sitefile bundle with packfile:
492
+
493
+ - **Pass `createRouter()` a path string for non-ASCII paths.** A `Request`
494
+ for `/docs/ünï/page.html` returns 404 because the pathname isn't
495
+ percent-decoded; the string `"/docs/ünï/page.html"` works
496
+ ([packfile#20](https://github.com/johnhenry/packfile/issues/20)).
497
+ - **`fromWebBundle` skips the bundle's entry at the bare base URL.** Sitefile
498
+ stores the index page there as well as at `/index.html`; you get it from
499
+ `index.html`.
500
+ - **The `baseURL` you pass to `fromWebBundle` must be the `BASE` you exported
501
+ with.** Entries under any other origin are skipped, so the wrong base reads
502
+ back as an empty map, without an error. packfile's `fromArchive()` on its
503
+ main branch (next release) defaults to the origin of the primary URL, which
504
+ Sitefile writes only when the site has `/index.html`; with a `BASE` that has
505
+ a path (`https://site.example/blog/`), the keys start with `blog/` unless you
506
+ pass that `baseURL`.
507
+ - **HEADER and REDIRECT need `createWebBundleRouter()`** (on main, next
508
+ release): `createWebBundleRouter(new wbn.Bundle(bytes), { baseURL })` serves
509
+ each exchange's own status and headers. `createRouter()` serves a files map,
510
+ so it answers with the files only.
511
+
512
+ ## License
513
+
514
+ [MIT](LICENSE)
@@ -0,0 +1,31 @@
1
+ import { crc32 } from './deflate.js';
2
+ import type { Tree } from './tree.js';
3
+ /**
4
+ * Deterministic archive writers. The same tree always yields byte-identical
5
+ * output: entries are sorted by path, there are
6
+ * no directory entries, uid/gid are 0, there are no user/group names, modes are
7
+ * the tree's 0644/0755, and every timestamp is fixed. Compression uses the
8
+ * deterministic encoder in deflate.ts (shared with the Rust port), so zip,
9
+ * tar.gz and OCI layer bytes are the same in both implementations.
10
+ */
11
+ export interface WriterOptions {
12
+ /**
13
+ * Seconds since the Unix epoch used for every entry timestamp (the
14
+ * `SOURCE_DATE_EPOCH` convention). Default: 1980-01-01 00:00 for zip (the DOS
15
+ * epoch) and 0 for tar. The gzip header mtime is always 0.
16
+ */
17
+ readonly epoch?: number | undefined;
18
+ /** tar only: directory entries (relative, no slashes at the ends, e.g. `srv`, `srv/www`) written before the files. */
19
+ readonly dirs?: readonly string[] | undefined;
20
+ }
21
+ /** Read `SOURCE_DATE_EPOCH`; undefined if unset, E_USAGE if malformed. */
22
+ export declare function sourceDateEpoch(env?: NodeJS.ProcessEnv): number | undefined;
23
+ export { crc32 };
24
+ /** Zip (no zip64). Deflate when it shrinks the data, otherwise stored. */
25
+ export declare function writeZip(tree: Tree, options?: WriterOptions): Uint8Array;
26
+ /** POSIX ustar; long names use the prefix field, then a pax `path` extended header. */
27
+ export declare function writeTar(tree: Tree, options?: WriterOptions): Uint8Array;
28
+ /** gzip of the tar, header mtime 0 and no filename, so the stream is reproducible. */
29
+ export declare function writeTgz(tree: Tree, options?: WriterOptions): Uint8Array;
30
+ /** Reproducible gzip of an already-written tar (see deflate.ts for the exact format). */
31
+ export declare function gzipTar(tar: Uint8Array): Uint8Array;