local-storefront 0.2.0 → 0.3.0-rc.2

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.
@@ -0,0 +1,303 @@
1
+ # Architecture
2
+
3
+ The store data is one crate, and two sides are built on it: the storefront, which renders a
4
+ theme with Liquid, and the app, which runs the Functions, checkout UI extensions and backend of
5
+ a Shopify app. The command line puts them together.
6
+
7
+ ```text
8
+ crates/core lsf-core the store data, and what the storefront and the app both need of it
9
+ crates/liquid lsf-liquid the Liquid language: no knowledge of Shopify
10
+ crates/theme lsf-theme Shopify's storefront on Liquid: objects, filters, tags, rendering
11
+ crates/app lsf-app a Shopify app: its checkout, its Admin API (GraphQL, 2026-10) and webhooks
12
+ crates/fmt lsf-fmt the formatter of `lsf fmt`: a source in, a source out
13
+ crates/cli lsf-cli the command line and the HTTP server: the `lsf` binary
14
+ ```
15
+
16
+ `lsf-theme` uses `lsf-liquid` and `lsf-core`; `lsf-app` uses `lsf-core` alone, so that neither
17
+ it nor `lsf-core` builds Liquid or the renderer, and `lsf-theme` builds no Wasm runtime.
18
+ `lsf-cli` uses them all.
19
+
20
+ ## `lsf-liquid`: the language
21
+
22
+ A port of Shopify's `liquid` gem, kept faithful to it down to the error messages. Nothing in
23
+ it is specific to a storefront.
24
+
25
+ | Module | Role |
26
+ |---|---|
27
+ | `tokenizer`, `lexer`, `lax` | Source → tokens. `lexer` is the strict expression parser, `lax` the regex-like scanners the gem falls back to; a tag is parsed strictly first and leniently if that fails, as the gem does in its default mode. |
28
+ | `parser`, `template`, `tags/` | Tokens → a tree of `Tag`s. Each tag implements `render` and says whether it is `blank`. |
29
+ | `expr`, `variable`, `condition` | Expressions, filter chains, and the comparison rules of `if`/`case`. |
30
+ | `value`, `number`, `time` | The value model and Ruby's arithmetic, string conversion and `strftime`. |
31
+ | `context` | Scopes, counters, registers, the globals object, and the partial loader `render`/`include` use. |
32
+ | `environment` | The registry of tags and filters. Embedders add their own. |
33
+ | `profiler` | What a render spends its time in, when it is asked to record it: every named template as a frame inside the one that rendered it, and on demand every tag and output by line. Each event also carries the points charged so far: one kind for a tag or an output, the others charged by the host through the profiler of the thread, since the objects that load things do not see the context. A render without a profiler pays one test per block body. |
34
+ | `filters/` | The standard filters. |
35
+ | `strict2` | Shopify's strict parser (`error_mode: :strict2`): the rules each tag registers with `Environment::register_syntax`, an audit the parser runs as it parses (`audit`), and the fixes of `lsf check --fix` (`apply`, `apply_intents`, `intent`). A fix that keeps the output is printed from what the engine parses and kept only when both parsers read it back the same. |
36
+
37
+ Values that are not plain data implement the `Object` trait (`get`, `index`, `items`, `size`,
38
+ `render`, `to_json`, ...): this is how the Shopify layer exposes lazy objects ("drops").
39
+
40
+ Tests: `tests/golden.rs` renders `tests/cases/*.txt` and compares with `tests/golden/*.json`,
41
+ which `tools/oracle/generate_golden.rb` produces by running the same templates through the
42
+ real gem. `tests/liquid_spec.rs` replays Shopify's
43
+ [liquid-spec](https://github.com/Shopify/liquid-spec) suite, downloaded by
44
+ `mise run liquid-spec:fetch`: it reports the specs that pass, differ and do not apply for each
45
+ suite, and fails when fewer pass than its recorded baselines. `tests/strict2_spec.rs` checks
46
+ the strict parser against the liquid-spec specs that run in `strict2` mode, and
47
+ `mise run oracle:strict2-fixes` renders the fixes with the gem.
48
+
49
+ ## `lsf-core`: the store data
50
+
51
+ What the storefront and the app both read, and nothing that renders Liquid.
52
+
53
+ | Module | Role |
54
+ |---|---|
55
+ | `store/model` | The input format, as serde types. Their doc comments are the user documentation: the JSON Schema and the field reference are generated from them. |
56
+ | `store/validate` | JSON Schema validation, turned into diagnostics with a code, a JSON pointer and a hint. |
57
+ | `store/load`, `store/build` | Read and merge the data files, then resolve them into a `Store`: defaults, ids, handles, references between entities, dates (ISO 8601, `store/date`), with semantic diagnostics. The files of `checkouts/` are not merged: each is a checkout scenario, resolved against the store (`Store::scenarios`) and kept with its errors when it has some. |
58
+ | `session`, `cart` | The visitor's state (cart, customer, country), and its cart lines resolved against the store: what a line costs, which the storefront cart and the checkout both show. |
59
+ | `theme` | A theme as files on disk: the directories it is made of, cached and revalidated reads, the files the Admin API writes over them (`FileOverlay`), and which theme of the shop it is. |
60
+ | `color`, `money`, `urls` | Values both sides print: CSS colors, amounts in a shop's money format, percent-encoding and the pieces of CDN URLs. |
61
+ | `util`, `json`, `diagnostics`, `error` | Handles and stable ids, lenient JSON, the diagnostics of the store data, errors. |
62
+
63
+ Tests: unit tests next to the code, `tests/docs.rs` (the generated reference is current).
64
+
65
+ ## `lsf-theme`: Shopify's storefront
66
+
67
+ | Module | Role |
68
+ |---|---|
69
+ | `theme/` | A theme directory read through `lsf-core`'s files: parsed Liquid, section schemas, JSON templates, locales. |
70
+ | `drops/` | Shopify's Liquid objects, backed by the `Store`, the session and the request. Properties are computed on demand. |
71
+ | `filters/`, `tags/` | Shopify's filters and tags (`section`, `sections`, `content_for`, `form`, `paginate`, `render` with blocks, ...). |
72
+ | `render/` | URL → template (`routes`), settings resolution, sections and blocks, layouts, the Section Rendering API, the compiled stylesheet and script bundles, and what Shopify injects into pages (`platform`). |
73
+ | `assets/platform/` | The JavaScript counterparts of Shopify's own scripts (`Shopify.actions`, `Shopify.loadFeatures`), embedded in the binary and served from the local CDN, with Shopify's global assets (`themes_support/`, see its README). Tested with Node's test runner (`mise run test:js`). |
74
+ | `images`, `fonts`, `urls` | Image transformations, the format an image is sent in (JPEG, PNG, WebP or AVIF, by what the client accepts) and placeholders, the blank fallback font, the local CDN URLs of a site. |
75
+ | `previews` | Shopify's feature previews (`--preview liquid-july-26`): what a render adds when one is on. |
76
+ | `app_extensions` | Theme app extensions of `--app` (`Theme::set_app_extensions`): their files read as the theme's, the block a `shopify://apps/...` type names, app blocks and app embed blocks rendered with the extension's snippets (`Context::set_partials`), assets and locales (`render`), the app blocks a theme names and what is wrong with them (`references`), and the checks of `app build` (`validate`). |
77
+ | `site` | What one render sees: theme, store, request, session, clock. |
78
+
79
+ A render is a pure function of `(theme, store, request, session, now)`: `Renderer::render`
80
+ returns the body, the status, the template name and the Liquid errors. It does no I/O beyond
81
+ reading theme files, which is what makes it fast and reproducible.
82
+
83
+ Tests: unit tests next to the code, `tests/render.rs` (a fixture theme rendered against the
84
+ demo store, with HTML snapshots), `tests/conformance.rs` (the examples of shopify.dev,
85
+ downloaded by `mise run docs:fetch`; it also fails when Shopify documents a filter or a tag lsf
86
+ lacks), `tests/overlay.rs` (the files the Admin API writes over the theme).
87
+
88
+ ## `lsf-app`: a Shopify app
89
+
90
+ Reads a Shopify app, runs its Functions against a cart and describes the checkout the buyer
91
+ sees, and serves the Admin API and the webhooks its backend talks to. It does not serve anything
92
+ itself: `lsf` does, with `lsf app dev`, `lsf serve --app` and `lsf app function run`.
93
+
94
+ | Module | Role |
95
+ |---|---|
96
+ | `config` | Reads `shopify.app.toml` and `extensions/*/shopify.extension.toml`: Functions, checkout UI extensions, theme app extensions (which `lsf-theme` renders), the others listed and ignored. What is wrong with the app (a missing build, an unknown target) is a diagnostic, not an error. `Selection` is the set of extensions in use (`--extension`). `config::version` holds the API releases and the Function schemas of each (`schemas/functions/`). |
97
+ | `checkout` | The checkout, below. |
98
+ | `admin` | The Admin API, below. |
99
+
100
+ ### `checkout`
101
+
102
+ Its design, and what each supported Shopify API release changes, are in
103
+ `crates/app/docs/checkout/` (`design.md`, `api-versions.md`, `ui-api-versions.md`).
104
+
105
+ | Module | Role |
106
+ |---|---|
107
+ | `input` | Builds the input of a Function: runs its GraphQL input query against the cart, the buyer and the store (a small executor over a resolver of the Function input graph). |
108
+ | `runner` | Runs a Wasm module as Shopify's own `function-runner` runs it (`runner::host`, its engine ported, with the providers of `providers/`), runs every Function on one engine (a module is compiled once, whatever the number of runs at once; at most one run per CPU), caches the compiled modules by path, time and size, stops a Function after 10 seconds, and reports logs, instructions and errors. |
109
+ | `pipeline` | `compute`: the Functions in the order Shopify runs them, applying each output, to a `CheckoutResult`. |
110
+ | `schema` | A Function API's schema: the input query only asks for what it has, the output fits its result type. |
111
+ | `fetch` | The HTTP request of a `.fetch` target, checked against Shopify's limits, and the answer its `.run` target reads. |
112
+ | `javascript/`, `trampoline`, `providers` | Building a Function as Shopify's CLI does, without Node: a JavaScript one bundled and compiled with Javy, a Rust one linked by Shopify's trampoline; and the providers a module imports when it runs. `javascript/` also bundles the UI extensions that name their sources. |
113
+ | `expect` | The `expect` of a checkout scenario, checked against what its Functions computed: what `lsf app check` fails on when a Function decides wrong. |
114
+ | `placement`, `app_metafields` | Where the pages place each target of the checkout and customer account UI extensions, and the metafields `appMetafields` gives them. |
115
+ | `types` | The state the buyer builds, the configuration (`CheckoutConfig`, which `lsf` maps from the `checkout` section of the store data) and the result, which is the JSON the page reads. |
116
+ | `assets` | The page and the sandbox of its extensions, embedded in the binary: the files of `assets/`, except `tests/` and `preview/`. |
117
+
118
+ `assets/` holds the checkout page and `extension-host.js`, which runs each extension in a
119
+ sandboxed frame (`sandbox.html`; its scripts are classic scripts, since the frame has an opaque
120
+ origin and cannot load modules). They need no build step.
121
+
122
+ ### `admin`
123
+
124
+ The GraphQL Admin API of a shop seeded from the store data, in the versions of `admin::version`
125
+ (their schemas in `schemas/admin/`), the OAuth endpoints, the webhooks of the app, and the sync
126
+ that writes what the backend changed back into the store data. It reads the app and the
127
+ checkout's types directly. Its design, and what each module owns, are in
128
+ `crates/app/docs/admin/design.md`.
129
+
130
+ Tests: the unit tests, and `tests/` against the fixture apps (`tests/fixtures/app`: Rust
131
+ Functions whose `dist/function.wasm` is committed and two built UI extensions,
132
+ `mise run fixtures:functions` rebuilds the Functions; `tests/fixtures/admin-app` and
133
+ `tests/fixtures/platform-app` for the Admin API) and the `rich` store. `lsf-app` does not render pages, not
134
+ even in its tests: that what the backend changes shows in the storefront is tested by lsf-cli
135
+ (`crates/cli/src/server/tests/store_sync.rs`). Node's test runner tests the scripts (`mise run test:js`).
136
+
137
+ ## `lsf-fmt`: the formatter
138
+
139
+ A port of Prettier with Shopify's Liquid plugin (`@shopify/prettier-plugin-liquid`), step for
140
+ step, so that the same source gives the same output. It shares nothing with `lsf-liquid`: a
141
+ formatter needs a tree that keeps HTML, comments and where everything was written, which a
142
+ renderer does not.
143
+
144
+ | Module | Role |
145
+ |---|---|
146
+ | `tokenizer`, `parser`, `markup`, `liquid_doc` | Source → tree: Shopify's `liquid-html-parser`, in the tolerant mode the plugin runs it in. Liquid and HTML are one tree; what cannot be parsed is printed as Prettier prints it (an unclosed `{% for %}` gets its `{% endfor %}`), so a source is never refused. |
147
+ | `ast` | The tree, in an arena, with what the plugin adds to each node before printing. |
148
+ | `preprocess` | Parents and siblings, the CSS `display` of each node, and whether the whitespace around it may change without changing the page. |
149
+ | `printer/` | Tree → document: the printer of the plugin. `children` decides what separates two nodes, `tag` and `element` print HTML, `liquid` and `expression` print Liquid. |
150
+ | `doc` | The document (groups, lines that break or not, indentation) and the algorithm that lays it out in a width: Prettier's `printDocToString`. |
151
+ | `embed/json` | The JSON of `{% schema %}` and of JSON scripts, as Prettier's `json` parser prints it. |
152
+ | `embed/css` | CSS, as Prettier's PostCSS printer prints it: `parser` cuts a style sheet the way PostCSS does, `value`, `selector` and `media` are ports of the three parsers Prettier reads values, selectors and media queries with. |
153
+ | `embed/js` | JavaScript, formatted by Oxc's formatter (`oxc_formatter`, a port of Prettier's printer) from the indentation the script is printed at, which `doc` gives it through `Doc::AtIndent`. |
154
+
155
+ `crates/cli/src/commands/fmt/` finds the files and reads the configuration Prettier reads
156
+ (`.prettierrc`, its `overrides`, `.editorconfig`, `.prettierignore`).
157
+
158
+ Tests: unit tests next to the code; `tests/golden.rs` formats the cases of
159
+ `tests/cases/*.liquid` and compares with `tests/golden/*.json`, which
160
+ `tools/prettier/generate_golden.mjs` produces by running the same cases through Prettier.
161
+ `tests/plugin_fixtures.rs` replays the tests of the plugin itself, downloaded by
162
+ `mise run prettier:fixtures`, and fails when fewer pass than its recorded baseline.
163
+
164
+ ## `lsf-cli`: commands and server
165
+
166
+ `commands/` holds one file per subcommand (`app.rs` is `lsf app`: `app dev` is `serve` without a theme, `App::open_without_theme`, and `app init` (`app_init.rs`) writes the checkout scenarios for an app; `function.rs` is `app function run`, which runs the pipeline once, and `app function build`, with `function/schema.rs` for `app function schema`; `webhook.rs` is `app webhook trigger`). `theme_extensions.rs` gives the theme the theme app extensions of `--app` and describes them for the `app:` line, the home page, `check --app` and `app build`. `server/` is the HTTP side:
167
+
168
+ | Module | Role |
169
+ |---|---|
170
+ | `mod` | State (the loaded store, sessions, caches), the request/response types handlers work with (`reply`, `params`), reloading when files change (`data_files`: a change of checkout scenarios alone keeps the shops of the Admin API). Handlers are plain functions from a request to a reply, so they are tested without a network (`tests.rs`, `tests/`). |
171
+ | `storefront` | Pages, the Section Rendering API, product, search and recommendation JSON. |
172
+ | `cart`, `forms` | The Cart Ajax API and form submissions. |
173
+ | `account` | The page that stands in for the customer accounts Shopify hosts, and the B2B location switch. |
174
+ | `throttle` | `--throttle` and the `throttle` of a session: answering some requests late. |
175
+ | `cdn` | Theme assets, files, images and the variants of them kept in memory, bundles, fonts. |
176
+ | `minify` | The stylesheets and the scripts of the theme, minified with a source map. |
177
+ | `compress` | Brotli or gzip for the responses that gain from it, by what the client accepts. |
178
+ | `control` | The `/__lsf` API. |
179
+ | `parts` | The parts the server runs (`--only`): the theme when it was opened with one, the checkout and the Admin API when it was given them (`ServerState::parts`). `dispatch` answers the routes of a part that is off with a `404` and an `X-LSF-Reason` naming the `--only` that serves it; what nothing else answers is the theme's. `commands/serve.rs` picks the parts (the defaults, the errors of an option whose part is off). |
180
+ | `home` | `--app`: the home page (`/` of `lsf app dev`, `/__lsf/checkouts`) and the notices that say why a scenario did not open: HTML in the look of the checkout, drawn from the JSON of `?format=json`. `home_check` runs the Functions of every scenario for it (`/__lsf/checkouts/check`), as `lsf app check` does. |
181
+ | `checkout` | `--app`: the pages at `/checkouts/cn/<token>`, `/checkout?scenario=<name>`, the operations the page sends, the files of the page and of the extensions. What the buyer chose is in the session (`SessionEntry::checkout`), next to the cart they share with the theme. The options, the mapping from the store data to `lsf_app::checkout::CheckoutConfig` and the build of the Functions are in `src/checkout.rs`. `checkout_account` logs the checkout in as a customer of the data. |
182
+ | `customer_account` | `--app`: the customer account where the app's customer account UI extensions render (`/account`, its orders, its profile, the full pages of extensions), in place of `account`. |
183
+ | `admin` | `--app`: `lsf_app::admin::AdminApi`, built from the store data and the app, routed before the storefront and the checkout: the GraphQL Admin API, OAuth, lsf's admin, `/__lsf/admin`, `/__lsf/webhooks`, the Partner API. It prints the `admin:` lines at start. |
184
+ | `store_sync` | What the backend changes in the default shop of the Admin API shows in the storefront and the checkout: the store is built again from the files with those changes on top, on the next request that reads it. |
185
+ | `polaris` | `/__lsf/cdn/<path>` and `/__lsf/polaris`: Polaris for the app's pages in lsf's admin, a redirect to Shopify's CDN (`online`) or the copy in the binary (`offline`, `assets/shopify-cdn/`). |
186
+ | `app_proxy` | `--app` with an `[app_proxy]`: `/<prefix>/<subpath>` and below (a locale prefix too), routed after `/__lsf` and `/cdn` and before the theme's pages, forwarded with `ureq` to the backend from the blocking thread of the request, the query signed with `lsf_app::admin::auth::app_proxy_signature` and the client secret of the Admin API, the headers as Shopify forwards and strips them. An answer in `application/liquid` is rendered by `Renderer::render_source` of `lsf-theme`, as a Liquid template inside the theme's layout. While the default shop has the app uninstalled (`ServerState::app_uninstalled`), nothing is forwarded: the storefront answers the path (the theme's `404` page) with `X-LSF-Reason`. |
187
+ | `../profile` | Shared by `lsf profile` and `/__lsf/profile`: renders a page several times with the profiler, keeps the render in the middle, and writes it as a tree of text or in the format of speedscope, in time or in points. `lsf-theme` adds the frames Liquid does not know (`render`, a JSON template, `section <id>`, `block <key>`) and the costs (`render/cost`): the objects charge for what they load where they are created. |
188
+ | `speedscope` | The viewer of flame graphs, served from the binary for `/__lsf/profile?...&html=1`. Its files are in `assets/speedscope/`, with their licenses. |
189
+ | `timing` | The `Server-Timing` header of each response, and the render times `/__lsf/timings` adds up: a histogram per template, per section and per theme block, so that they take the same room after a million pages. |
190
+ | `live_reload` | `--live-reload`: the WebSocket open pages listen on, and the task that looks at the theme and data directories while a page listens. The script it injects is `assets/live-reload.js`, tested with Node (`mise run test:js`). |
191
+
192
+ Rendering is CPU-bound and synchronous; the async server hands each request to a blocking
193
+ thread with a large stack, because deeply nested snippets and blocks recurse.
194
+
195
+ ## Development
196
+
197
+ ```bash
198
+ mise install # Rust, Node (the tests of the scripts, Prettier, the docs site), and Ruby for the oracle
199
+ mise run check # format check, clippy, tests
200
+ mise run horizon:serve # clone Shopify's Horizon theme and serve it
201
+ mise run oracle:install # install the reference liquid gem
202
+ mise run oracle:golden # regenerate the golden expectations from it
203
+ mise run docs:fetch # download shopify.dev's Liquid reference for the conformance test
204
+ mise run docs:coverage # what Shopify documents and lsf lacks: filters, tags, object properties
205
+ mise run liquid-spec:fetch # download Shopify's liquid-spec suite, which the engine's tests replay
206
+ mise run prettier:install # install Prettier and Shopify's Liquid plugin, to compare `lsf fmt` with
207
+ mise run prettier:golden # regenerate the golden expectations of the formatter from them
208
+ mise run prettier:fixtures # download the tests of the plugin, which the formatter's tests replay
209
+ mise run prettier:compare -- path/to/theme # list the files `lsf fmt` and Prettier disagree on
210
+ mise run oracle:strict2-fixes # check the fixes of `lsf check --fix` with the gem
211
+ mise run admin:schema # regenerate the Admin API schema of lsf-app from the pinned @shopify/dev-mcp
212
+ mise run partner:schema # regenerate the schema of its Partner API
213
+ mise run admin:access-scopes # regenerate its access scopes and search filters
214
+ mise run admin:topics # regenerate its webhook topics
215
+ mise run admin:webhook-examples # download Shopify's example webhook payloads the payload tests compare with
216
+ mise run admin:version -- 2027-01 # everything a new Admin API version needs, then what is left to do by hand
217
+ mise run admin:e2e # run examples/admin-backend (Shopify's libraries) end to end against lsf
218
+ mise run admin:bench -- admin --bin target/release/lsf # RAM and CPU of a release build, suite by suite (tools/admin/bench.mjs)
219
+ mise run docs:site # the guides of docs/ as a site (VitePress), at http://localhost:5173
220
+ mise run docs:site:build # build it; fails on a link of llms.txt to a page the site does not have
221
+ ```
222
+
223
+ Documentation: user-facing changes go into the guide of their task in `docs/` and into
224
+ `CHANGELOG.md`; the README only lists the tasks. `docs/README.md` is the index of the guides,
225
+ and the sidebar of the docs site (`tools/docs-site`): a new guide goes in its tables, and in
226
+ `llms.txt`, the index an AI agent reads first. Tables that repeat values of the code are
227
+ checked by tests (`throttle.rs` reads `docs/testing.md`, `fmt` reads `docs/formatting.md`,
228
+ `profile.rs` reads `docs/performance.md`), `docs/data-reference.md` is generated from the JSON
229
+ Schema (`UPDATE_SNAPSHOTS=1 cargo test -p lsf-core --test docs` rewrites it after a change to
230
+ `store/model.rs`), and `docs/data-format.md` is embedded in the binary by `lsf docs` and
231
+ `lsf init`.
232
+
233
+ ## Releasing
234
+
235
+ Set the version in `Cargo.toml` (`workspace.package.version`), add a section for it at the top
236
+ of `CHANGELOG.md` (`## 0.2.0 - 2026-10-06`), commit, then push a tag with the same version:
237
+
238
+ ```bash
239
+ git tag v0.2.0 && git push origin v0.2.0
240
+ ```
241
+
242
+ `.github/workflows/release.yml` runs the tests, builds `lsf` for macOS, Linux (glibc and
243
+ static musl) and Windows on x86-64 and ARM64, and publishes a GitHub release with one archive
244
+ per platform and a `SHA256SUMS` file. A tag that does not match the version of the crate, or
245
+ that `CHANGELOG.md` has no section for, fails before anything is built. Tags with a suffix
246
+ (`v0.2.0-rc.1`) are published as prereleases.
247
+
248
+ The release says what the changelog says of the version. Write that section for someone who
249
+ uses `lsf`: what they can do now, what behaves differently, what was wrong. Links are
250
+ absolute, as a release does not resolve them from the repository. To read it as the release
251
+ will show it:
252
+
253
+ ```bash
254
+ mise run release:notes 0.2.0
255
+ ```
256
+
257
+ To try the builds without releasing, run the workflow by hand from the Actions tab: the
258
+ archives are then attached to the run instead of a release.
259
+
260
+ ### npm
261
+
262
+ `npm/build.mjs` assembles the npm packages from the archives of a release:
263
+ `local-storefront`, a launcher (`npm/local-storefront/`), and one package per platform
264
+ holding the binary (`local-storefront-<os>-<cpu>`), which the launcher lists as optional
265
+ dependencies so that npm installs only the one that matches the machine. Linux gets the
266
+ static musl build, which runs on every distribution.
267
+
268
+ `.github/workflows/npm.yml` publishes them. No token is stored: on npmjs.com each package
269
+ names this repository and `npm.yml` as its *trusted publisher*, and npm recognises the
270
+ workflow run. When the repository variable `NPM_PUBLISH` is `true`, `release.yml` starts
271
+ `npm.yml` once the GitHub release is published. It can also be run by hand, for a release
272
+ made before publishing was enabled or after a publication that failed:
273
+
274
+ ```bash
275
+ gh workflow run npm.yml -f tag=v0.2.0
276
+ ```
277
+
278
+ npm can only trust a workflow for a package that already exists. The first publication is
279
+ therefore made from a machine, once, and again whenever a package is added (a new platform):
280
+
281
+ ```bash
282
+ npm login
283
+ ```
284
+
285
+ ```bash
286
+ mise run npm:first-publish v0.2.0
287
+ ```
288
+
289
+ The script publishes the packages of that release that are not on npm yet, then registers
290
+ `npm.yml` as their trusted publisher. It needs two-factor authentication on the npm account.
291
+
292
+ Every run of `release.yml`, including one started by hand, assembles the packages, installs
293
+ them the way a project would and runs `lsf` through `npx`, without publishing.
294
+
295
+ The Linux targets are cross-compiled with `cargo-zigbuild`, which is what lets the glibc
296
+ builds target glibc 2.17 whatever the runner has. The Rust version comes from `mise.toml`.
297
+
298
+ After changing the data model, regenerate the field reference with
299
+ `UPDATE_SNAPSHOTS=1 cargo test -p lsf-core --test docs`; after an intentional change of the
300
+ rendered HTML, regenerate the snapshots with
301
+ `UPDATE_SNAPSHOTS=1 cargo test -p lsf-theme --test render`; after a change of the guide or of the
302
+ schemas, write the copies of `examples/checkout-playground/shopify-local` again with
303
+ `UPDATE_SNAPSHOTS=1 cargo test -p lsf-cli the_example_data`.