@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/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## 0.0.0 — first npm release (2026-11-06)
4
+
5
+ `@johnhenry/sitefile` is a new package: Sitefile was never published to npm
6
+ under any name, and `0.0.0` is its first version. It joins the @johnhenry
7
+ family as its static build orchestrator, next to `@johnhenry/fileable` (file
8
+ trees in JSX) and `@johnhenry/packfile` (the Web Bundle reader and server).
9
+
10
+ - **Published as `@johnhenry/sitefile`** (was `sitefile`, `private: true`,
11
+ `0.1.0` in `package.json` only). The tarball ships `lib/src/` (compiled by
12
+ `prepack`, no source maps), the README, `LICENSE` and this file; it has no
13
+ runtime dependencies. The Rust port stays at `0.1.0` in `rust/Cargo.toml`
14
+ and is not published (johnhenry/sitefile#69).
15
+ - **MIT `LICENSE` file added.** `package.json` and `rust/Cargo.toml` already
16
+ said MIT. See johnhenry/sitefile#69.
17
+ - **Node floor raised from 22 to 26** (`engines`, `.nvmrc`, CI), the family
18
+ floor. The suite passed unchanged on Node 26.
19
+ - **CI on every push and pull request** (johnhenry/sitefile#67): typecheck,
20
+ tests and the examples smoke test (`npm run examples`), the Rust
21
+ fmt/build/test (clippy non-blocking until triaged), conformance between the
22
+ two implementations with the release binary, the VS Code extension and
23
+ tree-sitter grammar tests, actionlint, and the family's workflow lint.
24
+ `publish.yml` publishes from `main` through npm trusted publishing.
25
+ - **Interop with `@johnhenry/packfile` is tested**
26
+ (`test/packfile-interop.test.ts`, packfile as a devDependency only): packfile
27
+ reads a Sitefile `.wbn` back into exactly the exported files and serves it,
28
+ and its `fromArchive()` reads the same bundle once gzipped. The other
29
+ direction, importing gzip-wrapped bundles from fileable or packfile with
30
+ `AS tree`, is johnhenry/sitefile#141.
31
+ - **Fixed: a replaced archive destination went unnoticed on Linux.** Before
32
+ renaming the new archive into place, `exportFile` (and the Rust
33
+ `export_file`) checked that the existing destination was still the file
34
+ they inspected by device and inode number only. Linux often gives a file
35
+ created right after an unlink the same inode number, so a destination
36
+ swapped mid-export passed the check and was replaced. The check now also
37
+ compares size and nanosecond `ctime`. Found by the new CI's first Linux
38
+ run; the existing test only ever passed on macOS. Fixed in 0e0a1a1 (#142).
39
+ - **Fixed: a case-only collision inside one directory had no source line.**
40
+ Two names that differ only in case in a directory read by `COPY` (possible
41
+ on a case-sensitive file system) failed with `E_PATH_COLLISION` but without
42
+ the instruction's line; both implementations now report it. Its test only
43
+ runs on case-sensitive file systems, so it had never run. Fixed in 0e0a1a1
44
+ (#142).
45
+ - **Docs:** README badges, `## Contents`, `## Install`, `## Adding a new
46
+ plugin`, `## Security model` (the former "Safety defaults", with what stays
47
+ the user's responsibility), `## Family`; `examples/README.md`;
48
+ `AGENTS.md` reshaped into the family playbook with `CLAUDE.md` as a symlink.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Henry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,476 @@
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`) |
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>]` | yes; archives are deterministic, `wbn` is an unsigned Web Bundle (needs `BASE` or `--base-url`), `oci` is a single-layer OCI image of the files (under `AT`, default `/srv/www`) |
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]
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>
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
+ ```
277
+
278
+ Archives are reproducible: sorted entries, fixed timestamps (or `SOURCE_DATE_EPOCH`),
279
+ uid/gid 0, modes 0644/0755. See [docs/LANGUAGE.md](docs/LANGUAGE.md#export-formats).
280
+
281
+ ## Rust implementation
282
+
283
+ `rust/` is a full port, maintained alongside the TypeScript version and checked
284
+ against it by `node conformance/run.mjs` (identical output on every example and
285
+ case). The Rust binary is the shipped product. TypeScript is the reference
286
+ specification and the library API, and both get every feature (see
287
+ [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
288
+ sites, with a fraction of the memory; see [docs/RUST.md](docs/RUST.md) and
289
+ [bench/README.md](bench/README.md).
290
+
291
+ ```bash
292
+ (cd rust && cargo build --release) && rust/target/release/sitefile build examples/blog
293
+ ```
294
+
295
+ ## Editor support
296
+
297
+ Highlighting works on its own; the language features (diagnostics,
298
+ completion, hover, go to definition, references, rename, outline) come from
299
+ the Rust binary's language server, `sitefile lsp`
300
+ ([docs/LSP.md](docs/LSP.md)), so put it on `PATH` with
301
+ `cargo install --path rust` or point the editor at it.
302
+
303
+ - **VS Code**: [editors/vscode/](editors/vscode/), a TextMate grammar plus a
304
+ language client that starts `sitefile lsp` (`sitefile.server.path`). Not
305
+ published; `(cd editors/vscode && npm install && npm run package)`, then
306
+ `code --install-extension editors/vscode/sitefile-0.2.0.vsix`.
307
+ - **Zed**: [editors/zed/](editors/zed/), tree-sitter highlighting and the
308
+ `sitefile-lsp` language server; **zed: install dev extension** on
309
+ `editors/zed`.
310
+ - **Neovim**: [editors/neovim/](editors/neovim/), tree-sitter queries and a
311
+ `vim.lsp.config` snippet for `sitefile lsp`.
312
+ - **Helix**: [editors/helix/](editors/helix/), queries and a `languages.toml`
313
+ with the grammar and the `sitefile-lsp` server.
314
+
315
+ The grammar behind Zed, Neovim and Helix is
316
+ [editors/tree-sitter-sitefile/](editors/tree-sitter-sitefile/). Without an
317
+ extension, `"files.associations": { "Sitefile": "dockerfile" }` is a usable
318
+ stopgap in VS Code.
319
+
320
+ ## Docs
321
+
322
+ - [docs/CLI.md](docs/CLI.md) — every command, the image store layout, reference grammar
323
+ - [docs/LANGUAGE.md](docs/LANGUAGE.md) — syntax and the exact semantics chosen for v0.1
324
+ - [docs/RUST.md](docs/RUST.md) — the Rust port: layout, parity checks, known differences
325
+ - [docs/LSP.md](docs/LSP.md) — the language server for editors (`sitefile lsp`, Rust binary)
326
+ - [docs/PLUGINS.md](docs/PLUGINS.md) — writing renderer plugins, and the sandbox
327
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — invariants and module layout
328
+ - [docs/MILESTONES.md](docs/MILESTONES.md), [docs/TASKS.md](docs/TASKS.md)
329
+ - [AGENTS.md](AGENTS.md) — the verification loop and rules for anyone (human or agent) changing this repo
330
+
331
+ ## Development
332
+
333
+ ```bash
334
+ npm ci
335
+ npm test # compile with tsc, then run node:test suites
336
+ npm run typecheck
337
+ (cd rust && cargo build --release && cargo test --release)
338
+ npm run conformance # both implementations on every example and case
339
+ ```
340
+
341
+ From a clone, `node lib/src/cli.js` runs the TypeScript CLI after `npm run build`.
342
+ See [AGENTS.md](AGENTS.md) for the full verification loop CI runs.
343
+
344
+ ## Adding a new plugin
345
+
346
+ A plugin is a custom renderer that `TRANSFORM` calls. Write one ES module in its
347
+ own folder that exports `transform(request)` and returns the bytes of the output
348
+ file (or `{ files }` for several), declare it with `PLUGIN`, and allow it when you
349
+ build:
350
+
351
+ ```js
352
+ // plugins/upper/upper.mjs
353
+ export const apiVersion = 1;
354
+ export async function transform(request) {
355
+ return { bytes: (request.input?.text ?? '').toUpperCase() };
356
+ }
357
+ ```
358
+
359
+ ```
360
+ PLUGIN upper FROM ./plugins/upper/upper.mjs
361
+ TRANSFORM ./title.txt WITH upper TO /title.txt
362
+ ```
363
+
364
+ ```bash
365
+ sitefile build --allow-plugin upper
366
+ ```
367
+
368
+ A WebAssembly plugin that imports nothing needs no flag. The request shape, the
369
+ directory form (one call per file), the ABI for `.wasm` plugins, caching and the
370
+ first-party image plugin are in [docs/PLUGINS.md](docs/PLUGINS.md); a complete
371
+ example is `examples/motion/`. Run an external program instead with `TOOL` and
372
+ `--allow-exec` (same document).
373
+
374
+ ## Security model
375
+
376
+ A Sitefile can come from someone else (a Git URL, a theme image, a shared
377
+ stage), so building one is treated as running untrusted input. Templates,
378
+ imports and WebAssembly plugins are data; JavaScript plugins and external
379
+ tools are code, and run only when you name them on the command line.
380
+
381
+ **What Sitefile guarantees:**
382
+
383
+ - **Local reads stay inside the build context.** No `..`, absolute paths or
384
+ symlinks out; hard-linked files are refused (`E_HARDLINK`) unless
385
+ `--allow-hardlinks`, since a hard link can alias a file outside the context.
386
+ A `.sitefileignore` (gitignore-style subset) at the context root excludes files
387
+ from directory copies; explicitly named files are never ignored.
388
+ - **Remote imports are `https:` only, to public addresses only** — checked at
389
+ connect time and on every redirect (SSRF), size- and time-limited, never
390
+ decompressed or executed. `--allow-http` and `--allow-private-network` are the
391
+ only ways to widen this.
392
+ - **Pins are enforced.** Unpinned URLs are recorded in `Sitefile.lock` and must
393
+ match on later builds; `--strict-pins` refuses any remote import or BASE image
394
+ that is not pinned (`INTEGRITY`, `@sha256:` or a lockfile entry).
395
+ - **Git imports run no `git`.** `IMPORT … FROM git+https://host/repo.git#ref:path`
396
+ speaks protocol v2 over the same HTTPS client and policy, checks every object's
397
+ SHA-1, records the commit in `Sitefile.lock`, keeps symlinks inside the tree and
398
+ refuses submodules ([docs/LANGUAGE.md](docs/LANGUAGE.md#git-sources-githttps)).
399
+ - **Secrets go only where they are sent.** `--secret id=token,env=API_TOKEN` with
400
+ `IMPORT … SECRET token AS BEARER` (or `AS HEADER X-Api-Key`) sends the secret
401
+ only to that import's own origin, and never into the output, caches,
402
+ `Sitefile.lock`, vendor files, provenance, traces or messages.
403
+ - **Nothing executes unless you allow it by name.** A JavaScript `PLUGIN` runs
404
+ only with `--allow-plugin <name>`, in its own Node process under Node's
405
+ permission model (read access to its own folder only, no writes, network,
406
+ processes or environment); a `TOOL` runs only with `--allow-exec <name>`, argv
407
+ only, no shell, scrubbed environment. The Sitefile cannot allow itself. A
408
+ WebAssembly plugin must import nothing (checked before the build starts) and
409
+ runs with a time and memory limit, so it needs no flag
410
+ ([docs/PLUGINS.md](docs/PLUGINS.md#trust-model---allow-plugin)).
411
+ - **No silent overwrites.** Writing different content to an existing path fails
412
+ unless `--overwrite` is given. Two output paths that differ only in case
413
+ (`/A.html`, `/a.html`) always fail (`E_PATH_COLLISION`), and every path is
414
+ Unicode NFC, so a build gives the same files and image IDs on macOS, Windows
415
+ and Linux.
416
+ - **Exports are atomic.** A directory export is staged in a temporary sibling and
417
+ swapped in; a non-empty destination outside the context is only replaced with
418
+ `--force`. Archive exports (`zip`, `tar`, `tgz`, `wbn`) write one file via a
419
+ temporary sibling and rename; `--out` may not be the context, its parent, root
420
+ or home, nor a directory or symlink, and an existing file outside the context
421
+ needs `--force`.
422
+ - **Offline builds are verified.** `sitefile vendor` stores every remote import
423
+ (and the BASE image's blobs) under `vendor/sitefile/` by content digest; builds
424
+ then read them from there, verified, with no network access
425
+ ([docs/LANGUAGE.md](docs/LANGUAGE.md#vendoring-sitefile-vendor)).
426
+ - **`sitefile serve` listens on `127.0.0.1`**, GET/HEAD only, no directory
427
+ listings, unless `-p` names another address.
428
+
429
+ **What is still yours:**
430
+
431
+ - **What you allow is trusted code.** Node calls its permission model a "seat
432
+ belt" for trusted code, not a defence against deliberately malicious code.
433
+ `--allow-plugin`/`--allow-plugins` on a Sitefile you did not write runs its
434
+ author's JavaScript on your machine; `--allow-exec` runs a program on your
435
+ `PATH` with the arguments the Sitefile chose. Read them first.
436
+ - **The content of what you build.** `RENDER --escape=html` escapes template
437
+ values; `${x|raw}` and templates without it do not. Markdown and data you
438
+ import from elsewhere end up in your pages as written.
439
+ - **Unpinned remote content.** The lockfile catches a change; it does not tell
440
+ you whether the first version you locked was the one you meant. Use
441
+ `INTEGRITY`, `@sha256:` and `--strict-pins` for builds that must be
442
+ reproducible.
443
+ - **Web Bundles are unsigned.** `EXPORT … AS wbn` writes an unsigned b2 bundle;
444
+ signing (for example for an Isolated Web App) happens outside Sitefile.
445
+ - **Widening flags are yours to justify.** `--allow-http`,
446
+ `--allow-private-network`, `--allow-hardlinks`, `--force` and `serve -p
447
+ 0.0.0.0:…` each turn off one of the guarantees above for that build.
448
+
449
+ ## Family
450
+
451
+ Sitefile is the build orchestrator of the @johnhenry family's static tools. It
452
+ runs the pipeline (pinned imports, stages, templates, sandboxed plugins, checks,
453
+ exports); its siblings write file trees in code and serve the result. It has no
454
+ runtime dependency on either: they meet at file formats, a directory or a `.wbn`
455
+ Web Bundle.
456
+
457
+ - **[`@johnhenry/fileable`](https://github.com/johnhenry/fileable)** -- writes
458
+ file trees in JSX (`Dir`, `File`, `Rm`), for scaffolding and codegen. A
459
+ fileable build's output directory, or its `encode="zip"` archive, is an
460
+ ordinary Sitefile input: `COPY ./generated/ /`, or `IMPORT gen FROM
461
+ ./site.zip AS tree` and `COPY gen:/ /` (both give the same output). Its
462
+ `encode="wbn"` archive is gzip around a Web Bundle, which `AS tree` does not
463
+ read yet (johnhenry/sitefile#141). Neither package depends on the other.
464
+ - **[`@johnhenry/packfile`](https://github.com/johnhenry/packfile)** -- the
465
+ family's Web Bundle reader and in-memory server. `EXPORT / AS wbn BASE <url>`
466
+ writes a plain b2 Web Bundle that packfile's `fromWebBundle(buffer, { baseURL })`
467
+ reads back into exactly the exported files, and `createRouter()` serves as a
468
+ `(Request) => Response` handler. packfile's own archive format
469
+ (`fromArchive()`) is gzip around the same bundle under
470
+ `https://packfile.invalid/`: gzip a Sitefile bundle exported with that BASE and
471
+ `fromArchive()` reads it as is. `test/packfile-interop.test.ts` checks all
472
+ three; `@johnhenry/packfile` is a devDependency for that test only.
473
+
474
+ ## License
475
+
476
+ [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;