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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,1384 @@
1
+ # Changelog
2
+
3
+ What changed in each version of `lsf`, for someone who uses it. The section of a version is
4
+ what its [GitHub release](https://github.com/0xtlt/local-storefront/releases) says.
5
+
6
+ ## 0.3.0-rc.2 - 2026-11-12
7
+
8
+ Second release candidate of 0.3.0. It fixes what was reported on 0.2.0 and 0.3.0-rc.1, each
9
+ change checked against what Shopify does, and the store data gains provinces, currencies,
10
+ market languages, URLs per market and translated metaobjects. Some theme tests and store data
11
+ need a change: see Breaking changes.
12
+
13
+ ### Breaking changes
14
+
15
+ What to change when you upgrade from 0.3.0-rc.1.
16
+
17
+ - **A theme block rendered more than once on a page gets Shopify's numbered ids.** The first
18
+ render keeps `<prefix>__<key>`, the next ones get `-1`, `-2`, … in `block.id` and in
19
+ `id="shopify-block-…"`. A block rendered once is unchanged. Theme tests that snapshot HTML with
20
+ a repeated block need their snapshots updated.
21
+ - **A `rating` metafield's `value`, `scale_min` and `scale_max` are strings, as on Shopify.**
22
+ `{{ m.value.scale_max | json }}` gives `"5.0"`, no longer `5.0`. A number written in the store
23
+ data is kept as written (`5` gives `"5"`).
24
+ - **A reference metafield lsf can't resolve is a `validate` error.** A GID now names the entity
25
+ with that id; a GID or any other value that names nothing, which used to pass and render `nil`,
26
+ fails `validate` with the form to write (`use "faq/size"`).
27
+ - **A country's `currency` is what its buyers pay in.** It was only displayed. With no `rate`,
28
+ prices are converted at a rate of 1: give the market a `rate` or fixed `prices`.
29
+
30
+ ### Added
31
+
32
+ #### Store data and the storefront
33
+
34
+ - **Provinces.** `data-provinces` of `country_option_tags` and `all_country_option_tags` lists
35
+ the provinces Shopify lists for a country (37 countries), as Shopify prints them.
36
+ `localization.countries[].provinces` limits a country to those the store ships to. Addresses
37
+ get their province's name from its code and the reverse, the checkout has a province
38
+ selector, and `shopify_common.js` has `Shopify.CountryProvinceSelector`.
39
+ - **Currencies per market.** `localization.markets[].currency`, `rate` and fixed `prices` price
40
+ the storefront, the cart, `/cart.js`, the money filters, `Shopify.currency` and the checkout
41
+ in the currency of the visitor's country. Functions read the rate as
42
+ `presentmentCurrencyRate`.
43
+ - **The URL picks the country.** `?country=XX` sets it (a `301` to the country's subfolder when
44
+ its market has one). `localization.markets[].subfolder_suffix` serves a market under
45
+ `/<language>-<suffix>/` (`/en-ca/`), with its links, `request.locale.root_url` and
46
+ `Shopify.routes.root` under it. The localization form moves the visitor to that subfolder.
47
+ - **Languages per market.** `localization.markets[].languages` (default first) drives
48
+ `country.available_languages` and `localization.available_languages`, and the subfolders a
49
+ market has. Switching country keeps the current language when the new market offers it, and
50
+ uses the market's default otherwise. Without `languages`, every country gets every language.
51
+ - **Translated metaobjects.** A metaobject entry takes `translations`, by language then field
52
+ key: localized routes show them, field by field, with `fr-CA` falling back to `fr`. `validate`
53
+ checks them, and the Admin API's `translationsRegister` and `translationsRemove` read and write
54
+ them. Translations into a locale the Admin API unpublishes leave the storefront until it is
55
+ published again.
56
+
57
+ #### The command line
58
+
59
+ - **`validate` warns when the theme names a menu the store data lacks.** `default_menu` for
60
+ `main-menu` and `footer`, which lsf makes up, and `missing_menu` for the others. `render` warns
61
+ and the `--live-reload` panel shows a note when a page renders one of lsf's default menus.
62
+
63
+ ### Fixed
64
+
65
+ #### Themes and the storefront
66
+
67
+ - **Cart lines have `index`, their 0-based position**, so Horizon's remove button removes the
68
+ line clicked, not the first one.
69
+ - **`/cart/change.js` keeps to the stock, as `/cart/add.js` does.** More than a tracked variant's
70
+ stock caps the line and answers `422`, with Shopify's 2025 descriptions ("Only N items were
71
+ added to your cart due to availability.", "The maximum quantity of this item is already in
72
+ your cart.").
73
+ - **Minified stylesheets mean what their source means.** `translate`, `rotate` and `scale` stay
74
+ apart from `transform`, `font` and `transition` stay before what they reset, and
75
+ `border-image: none` is kept.
76
+ - **A minified asset links its source map at its own `?v=`**, as Shopify's CDN does, so the map
77
+ of an edited file is fetched again.
78
+ - **A reference metafield given as a GID renders the entity it names.** It rendered `nil`.
79
+ - **An object or list metafield can be written as its Admin API JSON text** (the `value` the
80
+ Admin API gives), so a value can be pasted from the API as is.
81
+
82
+ #### The command line
83
+
84
+ - **Paths in messages no longer start with `\\?\` on Windows.**
85
+ - **On Windows, path options read a Git Bash path (`/c/Users/me`) as `C:\Users\me`.** A missing
86
+ data directory given that way no longer suggests `init` with it. `render` and `profile` refuse
87
+ a route Git Bash rewrote into `C:/Program Files/Git/…`, and name `MSYS_NO_PATHCONV=1`.
88
+ - **`lsf app check` with no scenario names the extensions that are not built** in its summary,
89
+ with `lsf app build`, instead of a `fails` line for each. It still exits with `1`.
90
+
91
+ ## 0.3.0-rc.1 - 2026-11-12
92
+
93
+ lsf now serves a local GraphQL Admin API (version 2026-10) and delivers an app's webhooks to its
94
+ backend: the backend of a Shopify app runs and is tested offline, against the store data, with
95
+ the same data every run. The embedded app opens in a local admin, and the theme renders the app's
96
+ theme app extensions. Some store data, extensions and tests written for 0.2.0 need a change: see
97
+ Breaking changes.
98
+
99
+ ### Breaking changes
100
+
101
+ What to change when you upgrade from 0.2.0. Each one is told in full below.
102
+
103
+ - **A Function runs only when the shop has an enabled owner of it, as on Shopify.** At the
104
+ shop's checkout and in a scenario without `extensions`, a payment or delivery customization, a
105
+ validation or a cart transform that nothing enables no longer runs: in 0.2.0 every Function
106
+ ran. Enable it in the store data (`"enabled": true` in `checkout.functions.<handle>`), let the
107
+ backend create its owner through the Admin API, or name it in the scenario's `extensions`
108
+ ([what runs](docs/checkout.md#owners-and-scenarios)). Check:
109
+ - **the tests that drive the checkout** (`PUT /__lsf/session`, then `/checkout`): the
110
+ Functions no longer run there, and nothing in the test says so. A test that checks their
111
+ effect fails; one that only places an order passes without them. Give the session
112
+ `"enabled": true` in `data.checkout.functions.<handle>`, or open a scenario that names them
113
+ ([a checkout in a test](docs/testing.md#a-checkout-in-a-test));
114
+ - **`lsf app check`**: a Function that now runs in no scenario is no longer tested. The check
115
+ says so (`warning <handle>: ran in no scenario, so nothing checks it`, `ranInNoScenario` in
116
+ JSON) and still exits with `0`;
117
+ - **the `all-extensions` scenario that `app init` wrote in 0.2.0**: it has no `extensions`.
118
+ List the app's extensions in it, or write it again with `lsf app init --force`, which
119
+ overwrites the other scenarios too.
120
+ - **`shopify.appMetafields` has only the metafields an extension declares, as on Shopify.** In
121
+ 0.2.0 it had every metafield of the cart, declared or not: an extension that read one locally
122
+ could read nothing on Shopify. Declare each one the extension reads in
123
+ `[[extensions.metafields]]` (`owner_type = "CART"` for the cart's), with the app's namespace
124
+ written `$app` or `$app:<name>`: Shopify does not support `app--<id>--<name>` there, so such a
125
+ declaration reads nothing, and the loader warns. [Checkout](docs/checkout.md).
126
+ - **The dates of the store data are ISO 8601 only.** `May 1, 2024`, `2024/05/01`,
127
+ `05/01/2024`, `2024-5-1`, RFC 2822, an offset after a space and a time alone are now
128
+ `invalid_date` errors, and `validate` exits with `1`. Write `2024-05-01`, or
129
+ `2024-05-01T10:00:00Z`. The `date` filter of a theme still reads them all.
130
+ - **The storefront opens in the shop's country**, not in the first country of the data:
131
+ `localization.country`, `Shopify.country` and the localization form of a visitor who chose
132
+ none can change, and so can a page a test compared with 0.2.0's. A test that needs another
133
+ country gives its session one (`PUT /__lsf/session` with `country`).
134
+ - **Some results of the checkout change, to match Shopify's:**
135
+ - before an address with a country, no shipping rate is selected: `cost.shipping` is 0,
136
+ `cost.total` and `cost.dueNow` leave it out, and the Functions read
137
+ `selectedDeliveryOption: null`;
138
+ - with no address, the checkout UI extensions read the shop's country and its primary market,
139
+ as the Functions do (they read no country, and the market `primary`);
140
+ - a code whose discount Function answers a candidate of 0 is applied at 0, where it was "not
141
+ applicable".
142
+ - **For contributors: the crates are renamed.** `cargo build -p lsf` is now
143
+ `cargo build -p lsf-cli`, and the renderer, `lsf-core` in 0.2.0, is `lsf-theme`.
144
+ `cargo install --path crates/cli` does not change. [Architecture](docs/architecture.md).
145
+ - **`validate` and `check` find more problems, and exit with `1` for them.** `validate` now
146
+ checks typed metafields against their type and resolves their references
147
+ (`invalid_metafield`, `unknown_metafield_type`, `unknown_reference`), and checks
148
+ `theme_settings` against the theme's `config/settings_schema.json` (`unknown_setting`,
149
+ `invalid_setting`). `check` reports a snippet, a section, a section group or a key of the `t`
150
+ filter the theme names and does not have. Data and themes that passed with 0.2.0 can fail:
151
+ each problem has its file, its place and a hint.
152
+ - **`"available": false` alone makes a variant sold out**: tracked, with a quantity of 0
153
+ (`inventory_management` is `"shopify"`), as on Shopify. In 0.2.0 it was untracked, and themes
154
+ said "Unavailable". A page or a product JSON compared with 0.2.0's can change.
155
+ - **`check` reports what Shopify refuses on upload, and exits with `1` for it.** A
156
+ `{% schema %}` that does not parse, a setting type Shopify does not have, a setting without
157
+ its `id` or `label`, two settings with one id, a `select` default that is not an option, a
158
+ `range` default that is not one of its steps (`invalid_schema`, `unknown_setting_type`,
159
+ `duplicate_setting_id`); a theme block that `blocks/` does not have (`missing_block`); a `t:`
160
+ key of a schema missing from the schema locale (`missing_schema_translation`). A section or a
161
+ block with such a schema also renders a Liquid error, so `render --strict` can fail on a page
162
+ that passed. Horizon and Dawn have none.
163
+ - **`check --app` exits with `1` for an app block that renders nothing**: a block the app does
164
+ not have, or one its `enabled_on` or `disabled_on` keeps out of the template. These were
165
+ warnings, and the check passed.
166
+
167
+ ### Added
168
+
169
+ #### Getting started
170
+
171
+ - **Four quickstarts**, one page each from the first command to a test in CI: a theme
172
+ ([quickstart-theme](docs/quickstart-theme.md)), Functions and checkout UI extensions
173
+ ([quickstart-functions](docs/quickstart-functions.md)), an Admin API backend
174
+ ([quickstart-admin-api](docs/quickstart-admin-api.md)) and a public app made from the React
175
+ Router template ([quickstart-app](docs/quickstart-app.md)). The app's Playwright config is the
176
+ same in every page of the docs. It runs the backend from a build, so the test passes on a cold
177
+ checkout too: Vite's dev server reloads the first page while it optimizes its dependencies.
178
+ - **The docs ship in the npm package.** `node_modules/local-storefront/` has the `llms.txt`,
179
+ `docs/` and `CHANGELOG.md` of the version you installed, for a tool or an agent to read
180
+ offline.
181
+
182
+ #### Themes and the storefront
183
+
184
+ - **Theme app extensions render in the theme.** With `serve --app`, the app blocks a JSON
185
+ template or a section group names (`shopify://apps/<app>/blocks/<block>/<id>`) render where the
186
+ theme renders blocks: `{% render block %}` for a `@app` block of `section.blocks`,
187
+ `{% content_for 'blocks' %}` (Horizon), and the `apps` section. App embed blocks render before
188
+ `</head>` and `</body>` when `config/settings_data.json` turns them on, or `app.embeds` of the
189
+ store data does; `app.metafields` there sets the app-data metafields. In the Liquid of an
190
+ extension, `render`, `asset_url` and `t` read its own snippets, assets and locales, and the
191
+ `javascript` and `stylesheet` of a schema load once per page. A block that renders nothing
192
+ says why, in the terminal and in `X-LSF-Reason`. The `app:` line and the home page list the
193
+ extensions and their blocks; `check --app` checks the app blocks the theme names; `app build`
194
+ checks the extensions as Shopify does, where it said "nothing was built"; live reload follows
195
+ their files. [Storefront](docs/storefront.md#theme-app-extensions).
196
+ - **`lsf render --customer`, `--cart` and `--session`** render a page as a visitor with that
197
+ session: `--customer jane@example.com` (or `default`, `none`), `--cart TEE-BLK-M=2`
198
+ (repeatable), or the JSON of `PUT /__lsf/session`. A name the data does not have exits with
199
+ `2`. [render](docs/cli.md#render).
200
+ - **Under `--live-reload`, the page shows its problems.** A small panel, closed with its button,
201
+ lists the store data errors, the Liquid errors of the page and why a page is the 404 page.
202
+ Without `--live-reload` (tests, `--static`) pages are as Shopify renders them.
203
+ - **`validate` checks typed metafields.** A value its type refuses (`"two"` for a
204
+ `number_integer`, a `weight` in `"kgs"`, a rating out of its scale) is an `invalid_metafield`,
205
+ a type Shopify does not have an `unknown_metafield_type`, and a reference to a product,
206
+ variant, collection, page, article or metaobject the data does not have an
207
+ `unknown_reference`, with the closest name. A `weight`, `dimension` or `volume` takes the units of
208
+ Shopify's docs (`kilograms`) and their short forms (`kg`).
209
+ [Metafields](docs/data-format.md#metafields-and-metaobjects).
210
+ - **`validate` checks `theme_settings`** against the theme's `config/settings_schema.json`: an id
211
+ it does not have (`unknown_setting`, "did you mean"), a value its setting does not take
212
+ (`invalid_setting`: a `select` option, a `range`, a `checkbox`). `PUT /__lsf/session` refuses
213
+ them too. Without a theme, a warning says they are not checked.
214
+ - **`check` finds what the theme names and does not have:** `{% render 'x' %}` and
215
+ `{% include %}` of a missing snippet (`missing_snippet`), `{% section %}` and the section
216
+ types of JSON templates and section groups (`missing_section`), `{% sections %}`
217
+ (`missing_section_group`), a literal key of the `t` filter missing from the default locale
218
+ (`missing_translation`). Each has its file, its line and the closest name, in text and in
219
+ `--format json`. Horizon and Dawn have none.
220
+ - **`check` checks the `{% schema %}` of sections and theme blocks**, as Shopify does on
221
+ upload, and the theme blocks that schemas, JSON templates, section groups and
222
+ `{% content_for 'block', type: '…' %}` name. Each problem has its line and, for a typo, the
223
+ name it probably is: `setting "align" is of type "selct"` gives `did you mean "select"?`.
224
+ A section or block whose schema Shopify refuses also counts as a Liquid error of the page that
225
+ renders it (`render --strict`, `x-lsf-liquid-errors`, the panel of `--live-reload`): its
226
+ settings had no default, and nothing said why. [check](docs/cli.md#check).
227
+ - **`check` warns about `asset_url` of a file `assets/` does not have** (`missing_asset`), with
228
+ the closest name.
229
+ - **`check`, `validate` and `render` take `--format github`**: each problem is a GitHub Actions
230
+ annotation, shown on its line of the pull request. `validate` gives the line of the value in
231
+ the file of the store data. [GitHub annotations](docs/cli.md#github-annotations).
232
+ - **`routes --all`** adds every route in the other languages of the store (`/fr/…`), a search
233
+ that finds a product, and the sections the theme fetches: product recommendations and
234
+ predictive search. The CI loop of the [theme quickstart](docs/quickstart-theme.md#5-run-it-in-ci)
235
+ renders them all, and reports every page that fails, not only the first.
236
+ - **`validate` warns about a variant with `"available": false` and stock**
237
+ (`unavailable_in_stock`): Shopify never has both. lsf keeps it unavailable with its stock,
238
+ which Horizon shows as "Unavailable", and the hint says which field to remove.
239
+
240
+ #### Checkout and Functions
241
+
242
+ - **Expected results in checkout scenarios: `lsf app check` fails when a Function decides
243
+ wrong,** not only when it fails to run. A scenario's `expect` says what its checkout must look
244
+ like once the Functions ran: the payment methods, the express buttons, the delivery options,
245
+ the discounts, the lines and the errors, each with `includes`, `excludes` and `equals` (an
246
+ entry by its handle or name, or by the fields it must have, such as
247
+ `{"handle": "standard", "cost": "0.00"}`), and exact `totals`.
248
+ `"express_methods": {"excludes": ["paypal"]}` with `"payment_methods": {"includes": ["paypal"]}`
249
+ replaces piping `app function run --format json` into `jq`. Each expectation that is not met
250
+ prints the scenario, the assertion, what was expected and what the checkout has, and makes the
251
+ check exit with `1`; `--format json` has them in `expectations`. The Functions run as the
252
+ order is placed (`"step": "interaction"`: the page before it). `validate` warns about a name in
253
+ `excludes` that the checkout does not offer, which would check nothing. With `--extension`, the
254
+ `expect` of a scenario that runs a Function left out is not checked, and the summary says so:
255
+ its checkout is not the one it describes. The scenario schema has `expect`, and two scenarios
256
+ of the playground use it.
257
+ [Expected results](docs/checkout.md#expected-results).
258
+ - **`lsf app check --build`** builds the extensions first, for a clone where `dist/` is not
259
+ committed. `app check` also warns about a Function that ran in no scenario, so that nothing
260
+ checks it (`ranInNoScenario` in JSON), and its summary counts it (`no Function failed, 1
261
+ Function ran in no scenario`); the exit code does not change. `app check --help` describes
262
+ the JSON it prints. A missing build says to run `lsf app build <handle>`.
263
+ - **`lsf app function run --input <file>`** runs one Function on a JSON input (`-` for stdin),
264
+ as `shopify app function run --input` does, without the store data. A fixture of the unit
265
+ tests of Shopify's templates (`{"payload": {"target", "input", "output"}}`) runs as it is,
266
+ and its `output` is checked. `--export <name>` picks the target of a Function that has several,
267
+ as Shopify's `--export` does, and `--save-input <file>` writes the input lsf built for a
268
+ scenario's run. [Checkout](docs/checkout.md#run-a-function-on-an-input).
269
+ - **`lsf app function run --input <folder>`** runs each `*.json` fixture of the folder, as
270
+ the unit tests of Shopify's templates do: a line each, a summary, and exit `1` when one fails.
271
+ A fixture whose output is not the run's prints each path that differs, with what it expects
272
+ and what the run answered, instead of the expected output on one line. In an app with one
273
+ Function, or with the fixture in a Function's folder, the handle can be left out.
274
+ - **`lsf app function run` builds a Function whose files changed after its module was built**,
275
+ as `shopify app function run` builds before it runs: it ran the old module, without a word.
276
+ Each build lsf makes records what it was built from, in the user's cache folder.
277
+ - **`lsf app init` writes a stub of the settings of each UI extension** that declares some
278
+ (`[[extensions.settings.fields]]`): a value of each field's type in `app-settings.json`, to
279
+ fill in, as it does for the variables of a Function.
280
+ - **`lsf app function run <handle>` says when the checkout of the store data would not run the
281
+ Function.** A named Function runs whatever its owner, as before; when its payment customization
282
+ (or other owner) is disabled in the store data, or there is none, the output says so first
283
+ (`Named: …`, `forced` in JSON), and that it reads the files, not a running lsf.
284
+ - **`lsf app function typegen`** writes a JavaScript Function's GraphQL types
285
+ (`generated/api.ts`), offline, as `shopify app function typegen` does: the `typegen` script of
286
+ Shopify's templates works.
287
+ - **`lsf app function schema`** writes a Function's `schema.graphql`, the GraphQL schema of its
288
+ API at its `api_version`, as `shopify app function schema` does, from the schemas lsf
289
+ carries, without the network: in the Function's folder, by handle, or with `--path`;
290
+ `--stdout` prints it. [The schema of a Function](docs/checkout.md#the-schema-of-a-function).
291
+ - **A Shopify CLI to lsf table** in [checkout](docs/checkout.md#coming-from-shopifys-cli): each
292
+ `shopify app` command, lsf's, and what lsf does not do.
293
+ - **The settings of a UI extension can be set.** `checkout.extensions.<handle>.settings` of
294
+ the store data or of a scenario are the values `shopify.settings` (`useSettings`) reads, in
295
+ the checkout and the customer account, as a merchant fills them in. `validate` and the home
296
+ page warn about a key the extension's `[extensions.settings]` does not declare, a value of the
297
+ wrong type, or one not among its `choices`. `shopify.settings.value` was always empty.
298
+ [Checkout](docs/checkout.md#set-up-the-checkout).
299
+ - **Express checkout buttons.** The checkout shows the payment methods of the
300
+ `ACCELERATED_CHECKOUT` placement as express buttons above the contact section (Shop Pay,
301
+ PayPal, Apple Pay, Google Pay… in their colors, then "OR"), once the payment customizations
302
+ ran: a Function that hides a method there (`paymentMethodHide` with `placements`) now has a
303
+ visible effect. A click says lsf does not simulate the wallet. The page's JSON has
304
+ `expressMethods`, and every method its `placements`; `lsf app function run` prints each method
305
+ with its placements. The `payment-methods` scenario of `app init` has PayPal in both
306
+ placements, as Shopify offers it with PayPal Express on.
307
+ - **Order metafields on the order status page of the customer account.** A customer account UI
308
+ extension declares them with `owner_type = "ORDER"`, which lsf refused as an unknown owner
309
+ type. `shopify.appMetafields` of the order status targets now has them as `order` entries
310
+ (`target: {type: 'order', id: 'gid://shopify/Order/…'}`), read from the order in the Admin
311
+ API: what the backend sets with `metafieldsSet`, on an order placed in the local checkout or
312
+ on one of the store data, and the cart metafields copied onto an order placed here. As on
313
+ Shopify since 2026-04, a cart metafield is copied when the order is placed if an ORDER
314
+ definition with its namespace and key has `cartToOrderCopyable` enabled, made with
315
+ `metafieldDefinitionCreate` or declared in `shopify.app.toml`
316
+ (`[order.metafields.app.gift_message]` with `capabilities.cart_to_order_copyable = true`,
317
+ which lsf now reads). The checkout and its thank-you page have no `order` entry, as on
318
+ Shopify: an `ORDER` declaration reads nothing there. [Checkout](docs/checkout.md),
319
+ [Admin API](docs/admin.md#what-the-storefront-and-the-checkout-see).
320
+
321
+ #### The Admin API, for the backend of an app
322
+
323
+ - **A local GraphQL Admin API, version 2026-10, with every mutation.** A server with `--app`
324
+ answers `POST /admin/api/2026-10/graphql.json` over the store data, with the ids the
325
+ storefront, the checkout and the Function inputs use. Queries follow Shopify's 2026-10 schema
326
+ (its introspection matches Shopify's), with its validation errors, query costs
327
+ (`extensions.cost`, and the cost of each field with `Shopify-GraphQL-Cost-Debug: 1`),
328
+ throttling (`THROTTLED`, `MAX_COST_EXCEEDED`), cursors, the `query:` search syntax, and
329
+ `ACCESS_DENIED` for a scope the token lacks. All 496 mutations of 2026-10 are simulated,
330
+ the deprecated ones included, with their user errors, side effects and webhooks:
331
+ - the catalog: products, variants, media, `productSet`, collections and their sources,
332
+ bundles, combined listings, inventory, transfers and shipments, locations, publications,
333
+ channels, product feeds, selling plans, gift cards;
334
+ - orders: payments and payment terms, fulfillments and fulfillment orders (split, merge,
335
+ reroute), fulfillment services, shipping labels, refunds, returns and exchanges, order
336
+ edits, draft orders, disputes, store credit, risk, tax;
337
+ - customers and B2B companies, segments (their query language is evaluated over the
338
+ customers), saved searches, merges, data erasure, consent, marketing, analytics and a
339
+ ShopifyQL subset;
340
+ - the Subscriptions API: contracts, drafts, billing cycles and attempts (test cards decide the
341
+ outcome), and customers' payment methods;
342
+ - markets, catalogs, price lists and contextual prices, quantity rules, locales, web presences;
343
+ - metafields and their definitions, metaobjects, pages, blogs, menus, redirects (in bulk and by
344
+ CSV import), files, translations, policies;
345
+ - themes and theme files (written over the theme directory in memory, never to disk), script
346
+ tags (loaded by `content_for_header`), pixels, storefront tokens, delegate tokens;
347
+ - the app: billing, discounts and Function settings, bulk operations, staged uploads,
348
+ `appUninstall`, scope revocation, Flow triggers, point of sale.
349
+
350
+ A failed mutation changes nothing. New records have 13-digit ids, as Shopify's do, the same
351
+ every run. Discount summaries read like the admin's (`$5.00 off entire order • Minimum
352
+ purchase of $50.00`), a completed draft order keeps its discounts, a deprecated field reads
353
+ what its replacement has (truncated, before discounts or before taxes where the schema says
354
+ so), `productSet` refuses an option value `productOptions` does not
355
+ declare, the `shop/update` and `app/uninstalled` payloads have the shop's `domain`, and an
356
+ image uploaded to a staged target has its `width` and `height`. A product created without
357
+ `write_publications`, or a `fileCreate` source lsf does not download, gets a note in
358
+ `extensions.lsf.notes`. `GET /admin/oauth/access_scopes.json` lists the token's scopes; the
359
+ other REST paths answer JSON that says lsf does not simulate the REST Admin API.
360
+ `crates/app/tests/coverage.rs` fails if a mutation of a version lsf serves is not simulated.
361
+ Guide: [Admin API](docs/admin.md).
362
+ - **GraphiQL for the Admin API** at `/__lsf/graphiql`: the shops, their tokens and the served
363
+ API versions filled in, the schema's docs, the cost debug header. It works offline. The start
364
+ lines and the app's home page link to it. A browser that opens the GraphQL endpoint is
365
+ pointed there.
366
+ - **API version 2027-01, the release candidate, on demand.** `--admin-versions 2026-10,2027-01`
367
+ serves it next to 2026-10, each at its own `/admin/api/<version>/graphql.json`, with its own
368
+ schema, introspection, access scopes, search filters and webhook topics:
369
+ - a field or a mutation of one version is refused in the other with Shopify's error;
370
+ - a webhook subscription is delivered in the version that made it (`X-Shopify-API-Version`);
371
+ - the records are shared, and 2027-01 reads what it removed the way Shopify says to replace it.
372
+
373
+ A version Shopify no longer supports (`2025-10`) is answered by 2026-10, as Shopify falls
374
+ forward, and `X-Shopify-API-Version` says so. Another answers `404` saying which versions are
375
+ served. `lsf app webhook trigger --api-version 2027-01` builds the payload in 2027-01 for
376
+ every subscription, and says so of a subscription of another version.
377
+ [API versions](docs/admin.md#api-versions).
378
+ - **The storefront and the checkout show what the backend changes.** A product it creates is on
379
+ the theme's pages at once, and a draft leaves them. A customer it creates can log in at the
380
+ checkout. A metafield it sets is read by Liquid and by the Functions, and an app discount it
381
+ creates runs its Function at the checkout. Open pages reload. An order placed in the local
382
+ checkout is an order of the Admin API, and its `orders/create` webhook reaches the backend.
383
+ A checkout scenario added or changed on disk keeps what the backend set up; only a change of
384
+ the store's own data seeds the shops again, and the server names the files
385
+ ([when the data changes on disk](docs/cli.md#when-the-data-changes-on-disk)).
386
+ - **OAuth, token exchange and session tokens**, answered as Shopify answers them:
387
+ - the authorization code grant, and token exchange from a session token (embedded apps);
388
+ - expiring offline tokens and their refresh, and client credentials;
389
+ - a launch URL that opens the backend as Shopify's admin opens an embedded app.
390
+
391
+ Tokens have the scopes of `shopify.app.toml`. They are sealed with the client id and the
392
+ client secret, and lsf takes back one of an earlier run, in whatever order the backend presents
393
+ them, so a backend's stored session works after lsf restarts. A reset, a shop deleted or an
394
+ uninstall stops that for the installation: no token of an earlier run is taken back after it,
395
+ presented before or not ([tokens](docs/admin.md#tokens)). With `client_id = ""`, as the template
396
+ ships it, lsf takes `SHOPIFY_API_KEY` of the environment or of the app's `.env`, and warns when
397
+ it has to use its own. The guide says what Shopify's libraries need to reach lsf.
398
+ - **Webhooks, signed as Shopify signs them.** The subscriptions of `shopify.app.toml` and those
399
+ the backend makes with `webhookSubscriptionCreate` are delivered to the backend:
400
+ - with Shopify's headers and HMAC;
401
+ - retried after 1, 5 and 15 s;
402
+ - filtered by `filter` and `include_fields`;
403
+ - refused by the backend (`401`, `403`): the delivery's `reason`, one line of the server and
404
+ `lsf app webhook trigger` name the secret lsf signed it with, as `client secret:` does.
405
+
406
+ Payloads follow Shopify's 2026-10 examples, for every topic Shopify publishes an example of
407
+ (224 of the 227); the other three are logged, never sent with a made-up body. The storefront's
408
+ cart and the local checkout send `carts/*` and `checkouts/*`, and a checkout left without an
409
+ order becomes an abandoned checkout the backend reads. `lsf app webhook trigger` sends one on
410
+ demand, compliance topics included, as `shopify app webhook trigger` does (`--format json`
411
+ for a tool). After an uninstall, nothing is delivered for the shop but `app/uninstalled`.
412
+ - **Shopify's Events of `[events]` in `shopify.app.toml`.** lsf delivers the `Product` Events
413
+ (`create`, `update`, `delete`) when a product or a variant changes through the Admin API, with
414
+ their triggers, their `query`, their `query_filter` and Shopify's `Shopify-*` headers. The
415
+ template's `webhooks.products.update.tsx` receives them. `GET /__lsf/webhooks?topic=Product`
416
+ lists them, and `lsf app webhook trigger` names them for a topic that has no webhook
417
+ subscription ([webhooks](docs/admin.md#webhooks)).
418
+ - **Webhooks stay on this machine.** lsf sends one to this machine (`localhost`,
419
+ `*.localhost`, `127.0.0.0/8`, `::1`), to `--app-url`, or to the `--address` you give, nowhere
420
+ else: a subscription that points at your production backend or a stale tunnel is logged
421
+ `not_sent`, with why, and `lsf app webhook trigger` sends nothing and exits with `2`. Give
422
+ `--app-url http://localhost:<port>`: the start lines say when it is missing. A name under
423
+ `localhost` (`app.localhost`) goes to the loopback whatever your system's resolver says.
424
+ - **The webhook log, for tests.** `GET /__lsf/webhooks` lists the deliveries, filtered by
425
+ `topic`, `state`, `shop` (`test-1` or `test-1.myshopify.com`), `resource` (the GID of a
426
+ record: the order a test placed) and `since` (a delivery's id). `?wait=1` waits for one that
427
+ matches every filter. Every answer has `last`, the newest id, to pass as `since` next time:
428
+ a test no longer gets another test's webhook. A wait that could only time out (a shop lsf
429
+ does not have, a topic no subscription covers) answers `422` at once.
430
+ [Control API](docs/control-api.md#webhook-deliveries).
431
+ - **The start lines of an app.** With `--app`, the Admin API's URL and token, where the client
432
+ secret came from, where the webhooks go and the app proxy are printed before `ready:`: a tool
433
+ that reads up to `ready:` has the token. Without the Admin API, none of them. An app without
434
+ an `extensions` directory is said to be a backend, in one line.
435
+ - **The app proxy.** A storefront request under the prefix of `[app_proxy]` goes to the backend
436
+ with Shopify's parameters and signature. A Liquid answer is rendered in the theme's layout.
437
+ [App proxy](docs/admin.md#app-proxy).
438
+ - **Control endpoints under `/__lsf/admin`:** reset the shop, make and delete extra shops for
439
+ isolated tests, install and uninstall the app, and make session tokens for a test to call the
440
+ backend's routes. [Control API](docs/control-api.md).
441
+ - **An uninstall does what Shopify does** (`POST /__lsf/admin/uninstall`, or `appUninstall`).
442
+ It revokes the app's tokens on the shop, cancels its subscription and delivers
443
+ `app/uninstalled`, then:
444
+ - **its Functions stop.** Every object that runs one of them goes, with its metafields:
445
+ payment and delivery customizations, validations, cart transforms, fulfillment constraint
446
+ rules and app discounts, those of the store data included (`removedFunctionOwners`). While
447
+ the app is uninstalled, the shop's checkout runs none of its Functions, even one a scenario
448
+ names in `extensions`. A reinstall finds none of them, as on Shopify: the app creates them
449
+ again;
450
+ - **its UI extensions no longer render**: the checkout, the thank-you page (of an order placed
451
+ before included) and the customer account load none, and an extension's full page answers
452
+ `404`. Each place says why, "the app is not installed on the shop", and so do the home page
453
+ (`"installed": false` in its JSON) and the terminal. Its script tags and web pixel are
454
+ deleted (`removedScriptTags`, `removedWebPixels`). A reinstall renders the UI extensions
455
+ again (lsf has no checkout editor); the app creates its script tags and pixel again. Theme
456
+ app blocks and embeds are files of the theme, and still render;
457
+ - **its app proxy no longer forwards**: `/apps/<subpath>` is the theme's `404` page, nothing
458
+ reaches the backend, and `X-LSF-Reason` and the terminal say why. A reinstall forwards it
459
+ again;
460
+ - **its own metafield and metaobject definitions go**, those of its reserved namespaces
461
+ (`$app`, `$app:<name>`) and types (`$app:<type>`), declared in `shopify.app.toml`, in the
462
+ store data or made through the Admin API (`removedMetafieldDefinitions`,
463
+ `removedMetaobjectDefinitions`). Their metafields keep their values and their metaobjects
464
+ are kept aside (`retainedMetaobjects`); a reinstall creates the definitions again under new
465
+ ids, and gives them back. Definitions of other namespaces and types are the merchant's and
466
+ stay.
467
+
468
+ [Uninstall](docs/admin.md#tokens).
469
+ - **The shop's clock and its outbox.** `POST /__lsf/admin/clock` moves a shop's time: payment
470
+ schedules come due, store credit expires, payouts are made, scheduled listings go live,
471
+ checkouts are abandoned, data erasures run, each with its webhooks. What Shopify would send
472
+ the outside world (emails, labels, Flow triggers, point-of-sale messages) is never sent: it is
473
+ in `GET /__lsf/admin/outbox`.
474
+ - **`--only theme,checkout,admin` runs some parts of lsf** (also `LSF_ONLY`), on `serve` and
475
+ `app dev`: the storefront of the theme, the checkout of the app (its Functions, UI
476
+ extensions, scenarios), its Admin API (OAuth, webhooks, the app proxy, lsf's admin, managed
477
+ pricing). `serve` runs the theme, all three with `--app`; `app dev` the checkout and the Admin
478
+ API. `serve --app ../my-app --only admin` is the Admin API alone, for a backend's tests, without
479
+ a theme folder. A part left out answers `404` on its routes, with an `X-LSF-Reason` that names
480
+ the `--only` serving it; a line before `ready:` says what runs (`serving: theme, checkout,
481
+ admin`), and `GET /__lsf/status` and `GET /__lsf/admin` list it (`parts`). An option of a part
482
+ left out (`--pricing-plans` without the admin part, `--variant` without the checkout) stops the
483
+ start when given on the command line; set by its variable (`LSF_APP_URL`), it is ignored and
484
+ said. `serve` without `--app` serves no Admin API: it costs memory a theme does not use.
485
+ [What runs](docs/cli.md#what-runs).
486
+ - **New options:**
487
+ - `--app-url`: where the backend listens.
488
+ - `--no-functions`: the app's UI extensions and Admin API, and none of its Functions, for a
489
+ backend's tests.
490
+ - `--client-secret`: else `SHOPIFY_API_SECRET`, else the app's `.env`.
491
+ - `--admin-throttle`.
492
+ - `--admin-versions`: the API versions served.
493
+ - `--admin-fetch-local`: sources from a URL (`themeCreate`, `themeFilesUpsert`,
494
+ `urlRedirectImportCreate`) may be on this machine. Off by default, as Shopify cannot reach
495
+ it.
496
+ - `--admin-source <url>=<file>`: a URL of your backend's own answered with a file, never
497
+ downloaded; `PUT /__lsf/admin/sources?url=` stubs one from a test.
498
+ - **Ids are unique across shops.** A record a test shop creates (`POST /__lsf/admin/shops`)
499
+ has an id no other shop gives: a product, an order, a customer, a metafield, a charge. A
500
+ backend that keeps every test shop in one database does not mix them up. The ids of a shop
501
+ are a number of its name alone, the same every run whatever other shops the run makes and in
502
+ whatever order; the store data's records keep their ids in every shop. A test shop's ids have
503
+ 14 digits, and its charges no longer count from 1:
504
+ `/__lsf/admin/charges/<n>` takes the number of the charge's id, as the `confirmationUrl` gives
505
+ it. The default shop does not change.
506
+ - **Webhooks:** a subscription made with `metafieldNamespaces` (or `metafields`) gets the
507
+ record's metafields of those namespaces in its payload, as `metafields`. A `filter` that reads
508
+ a field `includeFields` leaves out delivers nothing, as Shopify requires the field in
509
+ `includeFields`: the subscription's answer and each `not_sent` delivery name the field to add.
510
+ - **`Metaobject.referencedBy`** lists the metafields and metaobject fields that reference the
511
+ entry. A list lsf does not compute (`Order.agreements`, `Order.events`) is now in
512
+ `extensions.lsf.defaulted`.
513
+ - **More of Shopify's checks:** an input list over 250 items (`nodes(ids:)` with 251 ids) is
514
+ refused with `MAX_INPUT_SIZE_EXCEEDED`, except where Shopify documents another limit (25
515
+ metafields for `metafieldsSet`, 2048 variants for `productSet`); the arrays of a `JSON` value
516
+ are not counted; a `metaobject_reference` definition needs a
517
+ `metaobject_definition_id` the shop has; `bulkOperationRunMutation` refuses a mutation the
518
+ schema refuses, or one with more than one mutation field (`INVALID_MUTATION`).
519
+ - **`description`** keeps words whole: `it<em>s</em>` reads `its`; only block tags and `<br>`
520
+ separate words.
521
+ - **Scopes changed in `shopify.app.toml`** while lsf runs are said in the terminal, with how to
522
+ grant them (restart, or `POST /__lsf/admin/install`).
523
+ - **`examples/admin-backend`:** a backend written with Shopify's libraries
524
+ (`@shopify/shopify-app-react-router`, `@shopify/shopify-api`), with a script that runs it
525
+ against lsf end to end (`mise run admin:e2e`).
526
+
527
+ #### The embedded admin and billing
528
+
529
+ - **An admin to preview the embedded app in, with a local App Bridge.**
530
+ `http://127.0.0.1:9292/admin/apps/<handle>` (`/admin` too) shows the app in a frame, at a new
531
+ launch URL on every load, with the shop's name, the app's navigation and its title bar around
532
+ it; `serve` and `app dev` print its URL, and the home page links to it. Its navigation links to
533
+ the storefront and to the checkout the server runs (**Preview**: Online Store, Checkout), each
534
+ in a new tab, and lists no section of the admin lsf does not simulate (Orders, Products…).
535
+ lsf's App Bridge (`/__lsf/admin/app-bridge.js`) runs in the app's pages:
536
+ - `shopify.idToken()` and `fetch()` with the session token, as App Bridge v4 adds it;
537
+ - the title bar (`ui-title-bar`, `s-page`), the navigation (`s-app-nav`, `ui-nav-menu`), the
538
+ save bar (`ui-save-bar`, `data-save-bar` forms), toasts, the loading bar, `shopify.user()`;
539
+ - the resource picker, with the shop's products, variants and collections;
540
+ - modals (`ui-modal`, `s-app-window`, Polaris's `s-modal`), over the whole admin as on
541
+ Shopify, and `shopify:admin/…` links;
542
+ - `shopify.intents.invoke()` of `edit:shopify/Product` (the template's **Edit product**),
543
+ `…/ProductVariant` and `…/Collection`: a dialog shows what lsf has of it, and closing it
544
+ answers `{code: 'closed'}`. Another intent answers `{code: 'error'}` and says why.
545
+
546
+ What lsf does not simulate (the scanner, POS, Sidekick, …) rejects or warns and says so.
547
+ [Preview the embedded app](docs/admin.md#preview-the-embedded-app).
548
+ - **The embedded app opens in lsf's admin as the React Router template ships it.** The admin's
549
+ frame goes through a proxy on a port of its own. It lets lsf's admin frame every page, the
550
+ library's bounce page and `redirect(…, {target: "_top"})` page included, gives the pages lsf's
551
+ App Bridge and Polaris (a `Content-Security-Policy` that allowed Shopify's CDN allows lsf there
552
+ too), and says Chrome to the template's bot check, so that Playwright's
553
+ headless Chromium gets no `410 Gone`. React hydrates the pages without a "Prop `src` did not
554
+ match": lsf's App Bridge puts the CDN's URLs back in the scripts the proxy pointed at lsf. The
555
+ template's one edit is the fetch function, for the Admin API: lsf warns, on the home page and
556
+ in its admin, when `app/shopify.server.ts` has none or `app/routes/app.tsx` has no `headers`
557
+ export. The admin reads the files again at each load, so the warning goes once they are
558
+ fixed. A backend started without `LSF_URL` sends its calls to Shopify: when a page of it fails
559
+ and lsf has received nothing from it yet (a token exchange, OAuth, an Admin API call with a
560
+ token they issued: not a test's calls with the printed token), the admin and the terminal
561
+ say to check `LSF_URL`. A backend that talked to lsf is not blamed for later failures; one
562
+ the frame finds stopped is watched again.
563
+ `LSF_ADMIN_FRAME=direct` opens the backend
564
+ itself, and the admin says when the app runs Shopify's App Bridge instead
565
+ ([preview the embedded app](docs/admin.md#preview-the-embedded-app)).
566
+ - **Polaris without the internet.** lsf serves the paths of Shopify's CDN at `/__lsf/cdn/<path>`:
567
+ in lsf's frame, an app's pages load Polaris from there. `--polaris online`, the default,
568
+ redirects to the CDN; `--polaris offline` (or `PUT /__lsf/polaris` while lsf runs) serves lsf's
569
+ copy of Polaris 2.0.0 and 1.1.0, its icons and its fonts, pointed at lsf, so that the
570
+ components, icons and fonts render with no request to `cdn.shopify.com`. Offline, a page that
571
+ still loads Polaris from the CDN gets lsf's copy from lsf's App Bridge when the CDN's fails.
572
+ The app and lsf can share a host on two ports: lsf answers the CORS preflights.
573
+ [Polaris without the internet](docs/admin.md#polaris-without-the-internet).
574
+ - **Billing.** `appSubscriptionCreate` and `appPurchaseOneTimeCreate` answer a
575
+ `confirmationUrl` on the shop's admin, as Shopify's, which the library's exit-iframe page
576
+ accepts: `billing.require` and `billing.request` work in the loader of a page. lsf's admin
577
+ serves that path and shows a local approval page, and the frame's proxy answers it on lsf's
578
+ origin in a redirect and in `X-Shopify-API-Request-Failure-Reauthorize-Url`. An app with
579
+ `embedded = false` gets the same path on lsf's origin, which works as it is; for an embedded
580
+ app, the answer's `extensions.lsf.localConfirmationUrl` has it, for a test that opens the page
581
+ itself. Approving goes to the `returnUrl`, and the
582
+ charge is active. Subscriptions renew on the shop's clock: past `currentPeriodEnd`, the
583
+ period moves on by its interval, the usage balance starts again, and a limited discount has
584
+ an interval less. [Approve a charge](docs/admin.md#approve-a-charge).
585
+ - **Managed pricing (Shopify App Pricing).** Declare the app's plans in a file
586
+ (`--pricing-plans pricing.toml`) or through `PUT /__lsf/admin/pricing-plans`. lsf's admin shows
587
+ them at `/admin/charges/<app handle>/pricing_plans`, where a `shopify:admin/charges/…` link
588
+ and the template's `redirect(…, { target: "_top" })` land. The merchant picks a plan: a test
589
+ subscription with the plan's `planHandle` replaces the current one, `app_subscriptions/update`
590
+ is delivered, and the browser goes to the plan's welcome link with `charge_id` and
591
+ `plan_handle`. The Billing API then refuses to create charges, as Shopify does. With
592
+ `legacy = false` (Shopify App Pricing since April 2026), no change of the subscription sends a
593
+ webhook, `appSubscriptionCancel` from the app included. The page takes the app's handle only:
594
+ a client id there is a `404`, as on Shopify, unless `shopify.app.toml` has no handle.
595
+ `pricing.toml` is read again when it changes (unless `--static`), and `DELETE
596
+ /__lsf/admin/pricing-plans` reads it again; plans a `PUT` set stay until the `DELETE`.
597
+ [Managed pricing](docs/admin.md#managed-pricing).
598
+ - **A test ends the merchant's plan: `POST /__lsf/admin/pricing-plans/cancel`.** The shop's
599
+ subscription of managed pricing is `CANCELLED` at once, without the app's token, and
600
+ `app_subscriptions/update` is delivered, none with `legacy = false`: the case of an app that has
601
+ to notice by itself that the plan ended. Shopify's plan page has no button for it (the merchant
602
+ chooses another plan or uninstalls), so lsf's page has none either: the route stands for a plan
603
+ that ends while the app stays installed. [Managed pricing](docs/admin.md#managed-pricing).
604
+ - **A local Partner API: an app on Shopify App Pricing reads its plan.** Since April 2026 such an
605
+ app learns its plan from the Partner API's `activeSubscription`, not from a webhook. lsf serves
606
+ it at Shopify's path on its own origin, `POST /{partner_id}/api/2026-10/graphql.json` (also
607
+ 2026-07 and 2027-01), with Shopify's schema: the app changes only the origin of
608
+ `partners.shopify.com`. `activeSubscription` answers the shop's plan, its trial and its billing
609
+ cycle by the shop's clock; `events` and `app { events }` answer the history: installs,
610
+ uninstalls, plans chosen, changed and ended, and every change of a charge. Both `legacy` modes
611
+ feed it. The Partner ID and the `prtapi_lsf_` token are printed at start (`partner:`, with the
612
+ plans) and in `GET /__lsf/admin/partner`; they are the same every run. A wrong token is
613
+ Shopify's `401`. The rest of the Partner API answers `LSF_NOT_SIMULATED`.
614
+ [Read the plan from the Partner API](docs/admin.md#read-the-plan-from-the-partner-api).
615
+
616
+ ### Changed
617
+
618
+ #### Checkout and Functions
619
+
620
+ - **`lsf app check` fails on what it cannot check, with why.** An extension the checkout does
621
+ not offer (an `api_version` other than `2026-10` and `2027-01`, no module or bundle, and for
622
+ one `--extension`, a scenario or a discount names, no target the checkout has) is a failure,
623
+ where it passed (`every Function ran`) or was said to run in no scenario. A Function whose
624
+ files changed after its module was built is `stale` unless `--build`. What Shopify rejects
625
+ in a theme app extension fails it too. JSON: `notOffered`, `stale`, `themeExtensions`.
626
+ [app check](docs/cli.md#app-check).
627
+ - **`lsf validate` checks the theme app extensions of the app** as `lsf app build` does, and
628
+ exits with `1` on what Shopify rejects (a block without `{% schema %}`).
629
+ - **A fixture's `target` or `export` that is not the Function's is an error** of `app function
630
+ run --input`, not left aside.
631
+
632
+ - **A Function runs only through an enabled owner, as on Shopify.** At the shop's checkout (no
633
+ scenario, or a scenario without `extensions`), a payment or delivery customization, validation
634
+ or cart transform Function runs only when the shop has an enabled owner of it: the
635
+ `PaymentCustomization`, `DeliveryCustomization`, `Validation` or `CartTransform` the app
636
+ creates through the Admin API, or one the store data declares
637
+ (`checkout.functions.<handle>.enabled`, new). Disabling it (`paymentCustomizationUpdate`,
638
+ `…Activation`, `validationUpdate(enable:)`) or deleting it takes it out of the next checkout.
639
+ A scenario that names a Function in `extensions`, and `lsf app function run <handle>`, still
640
+ run it alone, without an owner; `app init`'s scenarios name theirs, `all-extensions` every
641
+ extension. The checkout's "Functions" dialog, the home page and `app function run` say which
642
+ Functions did not run and why (`idle` in JSON). Store data that relied on every Function
643
+ running adds `"enabled": true` to them. [What runs](docs/checkout.md#owners-and-scenarios).
644
+ - **No owner is created at install.** lsf no longer seeds an enabled payment customization,
645
+ delivery customization, validation and cart transform for every Function: a fresh shop has
646
+ none, so an app that checks whether it is set up sees that it is not. Only those the store data
647
+ declares (`enabled`) are there. Extension names written as a locale key (`name = "t:name"`)
648
+ are read from the extension's default locale (`locales/en.default.json`), for the Functions'
649
+ titles and wherever lsf shows a name.
650
+ - **`shopify.appMetafields` has the metafields an extension declares, of every resource, and no
651
+ others.** As on Shopify, it holds the metafields of `[[extensions.metafields]]` (and of
652
+ `[[extensions.targeting.metafields]]` at their target): those of the shop, of the products and
653
+ variants of the lines (and of the components of a bundle), of the customer logged in, of the
654
+ company and location they buy for and of the cart, each with its `target` (`shop`, `product`,
655
+ `variant`, …) and the GID of the resource, read from the store data and what the backend sets
656
+ through the Admin API. An `owner_type` keeps one resource. It held only the cart's, each one
657
+ whether declared or not: an extension that writes a cart metafield with
658
+ `applyMetafieldChange` and reads it back now declares it (`owner_type = "CART"`). Shopify's
659
+ checkout UI template in Preact reads `$app.requestedFreeGift` without declaring it: its
660
+ checkbox does not stay ticked until the declaration is added, as the playground's `ui-preact`
661
+ now does. The order status page of the customer account reads them too (it had none), with
662
+ the company location the order was placed for and the values typed as its `AppMetafield`
663
+ types them (`false`, `2`). A namespace written `app--<id>--<name>` in the toml reads nothing,
664
+ as on Shopify, and the loader warns with the `$app:<name>` to write. `shopify.shop.id` is the
665
+ id of the shop of the store data
666
+ ([#25](https://github.com/0xtlt/local-storefront/issues/25)).
667
+ - **`POST /checkouts/cn/<token>/complete` answers the order it placed:**
668
+ `{"redirect", "order": {"id", "number", "name"}}`, as the Admin API recorded it. Match a
669
+ webhook by `order.id`. The thank-you page keeps the session's number, which can differ
670
+ (`#1004` there, `#1005` in the Admin API when the shop has a `#1004` already).
671
+
672
+ #### Store data and the storefront
673
+
674
+ - **Store data:** products gain `status` (`active`, `draft`, `archived`) and `published`. Only
675
+ an active, published product is on the storefront. Entries of `checkout.discounts` gain an
676
+ `id`. [Data format](docs/data-format.md).
677
+ - **Two companies with the same name** are both kept, and reported (`duplicate_company`). The
678
+ second used to replace the first.
679
+ - **The dates of the store data are ISO 8601 only:** a day (`2024-05-01`), or a day and a time
680
+ (`2024-05-01T10:00:00`, or a space for the `T`) with `Z`, an offset or neither, for the shop's
681
+ `timezone`; `now` too. The other forms that Liquid's `date` filter reads were accepted as well,
682
+ and are now an `invalid_date` error: `May 1, 2024`, `2024/05/01`, `05/01/2024`, `2024-5-1`,
683
+ RFC 2822 (`Wed, 01 May 2024 10:00:00 +0000`), an offset after a space
684
+ (`2024-05-01 10:00:00 +0200`), a comma before the fraction of a second, and a time alone
685
+ (`10:00`, which took the day 2024-01-01). None of the examples or guides used one. The `date`
686
+ filter of a theme still reads them all.
687
+ - **The storefront opens in the shop's country, like the checkout.** A visitor who selected no
688
+ country is in the shop's (`shop.address.country_code`), the one of its primary market, or the
689
+ first country when the store does not sell to it: `localization.country`, `Shopify.country`
690
+ and the localization form say so. The storefront used to show the first country and the
691
+ checkout the shop's: the demo store showed the United States, then checked out in Canada.
692
+
693
+ #### The command line
694
+
695
+ - **`lsf serve --help` lists the theme's options first.** The options of an app are under
696
+ "App (with --app)", those of the Admin API and lsf's admin under their own heading. Its first
697
+ line, in `lsf --help` too, is about the theme; the app comes after.
698
+ - **`X-LSF-Reason` gives every reason of the page** when several app blocks render nothing
699
+ (`3 reasons: … | … | …`). It gave the first.
700
+ - **For contributors, the crates are split along what they need.** `lsf-core` is the store data
701
+ and what the storefront and the app share of it, `lsf-theme` the renderer (the old `lsf-core`),
702
+ `lsf-app` the app (`lsf-checkout` and `lsf-admin` in one crate: `config`, `checkout`, `admin`)
703
+ and `lsf-cli` the command line: `cargo build -p lsf-cli` builds `lsf`. `lsf-app` and
704
+ `lsf-core` build without Liquid or the renderer. Nothing else changes for someone who uses
705
+ `lsf`. [Architecture](docs/architecture.md).
706
+
707
+ ### Fixed
708
+
709
+ #### Themes and the storefront
710
+
711
+ - **A large store loads much faster.** The default product recommendations took a time cubic in
712
+ the number of products: 3,000 products took 5 to 6 s to load, and now take 0.3 s. This speeds
713
+ up startup and `--watch` too.
714
+ - **`validate` reports every problem in one run.** A data file with an error was left out whole,
715
+ so the problems of the rest of it showed only once that error was fixed. Its entries without
716
+ errors are now read and checked. A reference to an entity left out because of its errors says
717
+ so (`the product "a" is left out because products/a.json has errors`), rather than suggesting
718
+ another.
719
+ - **A JSON template that is not valid JSON, or names a section type the theme does not have, is
720
+ an error of the page.** It counts in `x-lsf-liquid-errors`, makes `render --strict` fail and is
721
+ in the log of `serve`, with the template, its line and the closest section type. The page
722
+ showed it with a 200 and 0 errors.
723
+ - **`--live-reload` no longer breaks a template that writes JSON.** Its script went at the end
724
+ of every response, `{% layout none %}` ones included (`?view=json-data`). It now goes only in
725
+ HTML pages with a `</body>` or an `</html>`.
726
+ - **`?section_id=` and `?sections=` say which section is missing.** `x-lsf-not-found` names the
727
+ ids the page does not have, the closest one, and the ids it has. The 404 had no reason.
728
+ - **`/de/` says the store has no German language**, when the theme has `locales/de.json` and
729
+ `localization.languages` does not list it, with what to add.
730
+ - **`'gift-card/card.svg' | shopify_asset_url` is served**, as a drawing of the size of
731
+ Shopify's: the gift card page of Dawn and Horizon showed a broken image.
732
+ - **`payment_button` speaks the language of the storefront.** "Buy it now" was English on
733
+ `/fr/` too; it is in the language of the page, for the languages of Shopify's checkout, and in
734
+ English for the others.
735
+ - **An unknown filter close to one of Shopify has a hint:** `{{ x | uppercas }}` gives
736
+ `hint: did you mean "upcase"?`, in the text and the JSON of `check`.
737
+ - **`lsf routes` no longer lists `/account`**, a page of lsf's, not of the theme. `GET
738
+ /__lsf/status` lists the same routes.
739
+ - **`render /account` answers as `serve` does**, with the customer of `--customer`: it was the
740
+ theme's 404. `/recommendations/products` and `/search/suggest` are answered as `serve` answers
741
+ them too, and a `section_id` or `sections` in the query renders those sections.
742
+ - **An app embed block the theme turns on says why it renders nothing without `--app`**, in the
743
+ terminal and in `X-LSF-Reason`, as an app block does.
744
+ - **`--theme` naming a folder that does not exist says so.** It said the folder "does not look
745
+ like a theme".
746
+ - **A control API body that is JSON is read as JSON** without `Content-Type: application/json`
747
+ (`fetch` sends a string as `text/plain`): `PUT /__lsf/session` used to read it as a form.
748
+ - **The docs no longer promise classic customer account pages.** `/account/login` and the
749
+ other account URLs lead to `/account`, as [storefront](docs/storefront.md) says;
750
+ [testing](docs/testing.md) said otherwise.
751
+
752
+ #### Checkout and Functions
753
+
754
+ - **No shipping in any total before an address.** Without an address with a country, no
755
+ shipping rate is selected: `cost.shipping` is 0, and `cost.total`, `cost.dueNow` and
756
+ `shopify.cost.totalAmount` leave it out, as the page does ("Enter shipping address").
757
+ `shopify.cost.totalShippingAmount` is undefined, and the Functions read
758
+ `selectedDeliveryOption: null` and a `cart.cost.totalAmount` without shipping, as on Shopify.
759
+ - **One country for a buyer with no address.** With no address and no selected country, the
760
+ Functions, the checkout UI extensions and the address form all read the shop's country
761
+ (`shop.address.country_code`), the one of its primary market. The extensions used to read no
762
+ country, and the market `primary`; they now read the market the Functions read.
763
+ - **A code whose discount Function answers a candidate of 0 is applied, at 0.** A candidate of
764
+ 0 % (or a fixed 0.00), which a Function answers to accept a code before the gift it stands for
765
+ reaches the cart, made the code "not applicable". As on Shopify, the code is now accepted and
766
+ its discount shows at 0 on the lines it targets: in the page, `discounts`, `discountCodes`,
767
+ the extensions' `discountAllocations` and the output of `lsf app function run`. An order or a
768
+ shipping candidate of 0 applies the same way; a shipping one gives way to a shipping discount
769
+ that takes something off the option the buyer selected. An automatic discount of 0 shows too,
770
+ at 0 (a Function that reads its percentage from settings that are not set)
771
+ ([#26](https://github.com/0xtlt/local-storefront/issues/26)).
772
+ - **The metafields of a discount or a Function's owner take Shopify's forms.** Besides
773
+ `"namespace.key"`, a list of `{namespace, key, type, value}`, as `metafieldsSet` takes it, is
774
+ read. The docs write them as Shopify's templates do, `"$app.function-configuration"` for
775
+ `metafield(namespace: "$app", key: "function-configuration")`, and `"$app:function-configuration"`
776
+ says to write a dot.
777
+ - **JavaScript, TypeScript and Preact builds no longer break after macOS empties the temporary
778
+ folder.** lsf's copy of the npm packages lived in `$TMPDIR`; once macOS's cleaner deleted its
779
+ files and kept its folders, every build failed with `cannot write lsf's npm packages` until
780
+ the folder was deleted by hand. The packages now live in the user's cache folder
781
+ (`~/Library/Caches/lsf/` on macOS, `~/.cache/lsf/` on Linux). A folder with a file missing or
782
+ changed is written again, safely when several lsf run at once.
783
+ - **A change to a Rust Function's query builds it again.** The `typegen` macro reads the
784
+ `.graphql` files without telling cargo: after a change to one of them alone, `--watch` said
785
+ `✓ delivery-rules built` while cargo compiled nothing, and the old module ran on the new
786
+ query's input. lsf now touches `src/main.rs` (its time only) before the build when a
787
+ `.graphql` file is newer than the module, and the watch says when a module came out the same
788
+ (a compiler cache that does not see the change).
789
+ - **`--watch` no longer rebuilds the Functions when the app's web front end builds.** In the
790
+ app's own folder, `--watch` now follows only what the builds of the Functions read: the
791
+ workspace's manifests and lock files (`Cargo.toml`, `Cargo.lock`, `package.json`, …) and the
792
+ folders that hold a crate or a package (`crates/common/Cargo.toml`). A project whose web build
793
+ writes `dist/` at its root on every save of `src/` built every Function and reloaded the pages
794
+ each time. `dist/`, `build/`, `target/`, `node_modules/`, dot folders (`.git/`, `.shopify/`)
795
+ and the app's own sources are left alone.
796
+ - **`--build` then no "no Wasm module".** `app function run`, `app dev` and `serve` printed
797
+ the diagnostics of the app as it was before `--build` built it.
798
+ - **An endless loop in a Function stops in a moment.** It ran for 10 s (some 35 billion
799
+ instructions) before it was stopped, and three of them took the 20 s of the home page's check.
800
+ A run now stops at 110 million instructions, ten times Shopify's limit:
801
+ `exceeded Shopify's limit of 11,000,000 instructions: stopped at 110,000,000 (an endless
802
+ loop?)`. A run under that keeps its exact count.
803
+ - **`app check` says what a trapped Function threw:** `the Function trapped: Error: boom (in
804
+ b2b)`, the last message it logged. Each JSON failure has the `logs` of the first scenario it
805
+ failed in. `lsf app function run`, the checkout's Functions dialog and their JSON have the
806
+ message too, where they said `the Function trapped` alone.
807
+ - **`lsf app check` prints each way a Function fails.** A Function that threw in one scenario
808
+ and answered a wrong output in another was printed with the first error for both, and the
809
+ JSON had the logs of the first. Each error is now a failure of its own, with its scenarios and
810
+ its logs, in the text, in `--format json` and on the home page.
811
+ - **`lsf app check` prints a scenario that does not validate once**, its problems indented under
812
+ it, and no longer says `no scenario to check` when the only scenarios are invalid.
813
+ `app function run --scenario` prints them once too.
814
+ - **An extension that does not build exits with `1`**, as the exit codes say of something to
815
+ fix (`app build`, `app function build`, `--build`). It exited with `2`, "could not run".
816
+ - **The home page has no warnings right after `lsf app init`.** It warned, for each Function
817
+ that is not a discount, that the shop has no customization that runs it, while the scenarios
818
+ `app init` wrote run each of them. It now warns only about a Function no scenario runs.
819
+ - **`app init`'s `all-extensions` scenario runs the discount Functions.** It entered the code
820
+ `WELCOME`, which no discount had, and named the discount Functions, which then never ran. It
821
+ now has a discount of each, with the code `SAVE10` entered, as `discount-code` does.
822
+ - **`app init`'s `discount-code` scenario** says that one Function "applies as a code
823
+ discount".
824
+ - **`lsf app init` counts customer account UI extensions apart**: `for 5 Functions, 1 checkout
825
+ UI extension and 1 customer account UI extension`, not `2 checkout UI extensions`.
826
+ - **`lsf app function run` prints no discount as `discounts 0.00`**, not `-0.00`.
827
+ - **`lsf app function run` says why shipping is 0 while a rate is listed:** no rate is
828
+ selected before a shipping address, as on Shopify, and the line gives the `--checkout` that
829
+ adds one.
830
+ - **`--save-input tests/fixtures/x.json` creates `tests/fixtures/`**, as the quickstart runs it:
831
+ it stopped with "No such file or directory".
832
+ - **`lsf app build` exits with `1` on an error of a theme app extension**, as on the other
833
+ errors to fix, not `2`.
834
+ - **The home page counts only the scenarios with problems.** Settings of a UI extension that
835
+ do not fit, in one scenario, are that scenario's warning: every scenario that ran the
836
+ extension was said to have problems (`11 scenarios · 11 with problems`).
837
+ - **A UI extension whose build fails under `--watch` says the previous bundle runs**, not "No
838
+ module of it built yet."
839
+ - **A JSX build that misses `react/jsx-runtime` says what to add:** a Preact extension without
840
+ its `tsconfig.json` needs `"jsxImportSource": "preact"`.
841
+
842
+ #### The command line
843
+
844
+ - **The `next:` line of `lsf app init` drops `--app` in the app's folder**, where `app dev`
845
+ finds the app.
846
+ - **The hints say `npx local-storefront` when it was installed with npm**, where `lsf` is not
847
+ on the PATH: ``build it with `npx local-storefront app build x` ``, the `next:` lines, the home
848
+ page. The launcher sets `LSF_COMMAND`; the binary of a release says `lsf`.
849
+ - **`lsf docs | head` ends quietly.** A command that only prints (`docs`, `schema`, `render`,
850
+ `routes`, `validate`, `check`, `fmt`, `profile`) stops like `cat` when its reader leaves,
851
+ instead of panicking on the closed pipe.
852
+ - **One is in the singular, everywhere lsf counts:** `1 Liquid error · 0 warnings` (`render`),
853
+ `wrote 1 file, kept 15 existing ones` (`init`, `app init`), `1 file formatted` (`fmt`),
854
+ `validate`, `check`, the banner, the store data's errors and the status page. No more `(s)`.
855
+
856
+ ## 0.2.0 - 2026-10-30
857
+
858
+ lsf now runs the checkout and the customer account of a Shopify app: its Functions and its UI
859
+ extensions, on your machine. Classic customer accounts are no longer simulated: see Changed.
860
+
861
+ ### Added
862
+
863
+ - **A local checkout for Shopify apps.** `lsf app dev --app <dir>` serves the checkout of an
864
+ app without a theme, and `lsf serve --app <dir>` leads the `/checkout` of a theme to it. It
865
+ runs the Shopify Functions of the app (cart transform, discounts, delivery and payment
866
+ customization, validation) from their built Wasm modules, and shows its checkout UI
867
+ extensions in the page, in the standard one-page look or in the Shop app's (`--variant shop`).
868
+ `--extension <handle>` (repeatable) picks the extensions to use, `--build` runs the build
869
+ command of each Function first. A badge in the corner of the page opens a dialog with the
870
+ input and output of each Function run side by side. API versions 2026-10 and 2027-01 are
871
+ supported. Guide: [checkout](docs/checkout.md).
872
+ - **Customer account UI extensions, and every checkout target.** With an app, `/account` is the
873
+ customer account of Shopify's new customer accounts: the orders of the customer logged in (from
874
+ the store data, and the last order placed in the local checkout), the status of an order (its
875
+ shipments, what is not shipped, its returns), the profile (addresses, and the company and
876
+ locations of a B2B customer), and the full pages of extensions. The 24 targets of customer
877
+ account UI extensions render there, with the `shopify` global they read (the account logged in,
878
+ the order, navigation, toasts) and the components only customer accounts have (`s-page`,
879
+ `s-customer-account-action`, `s-menu`, …); an order action opens its dialog. The checkout gains
880
+ the 14 targets it lacked: pickup locations and points, the targets of payment apps (with the
881
+ form dialog), the gift card form, bundle lines, the chat, and the address autocomplete, which
882
+ suggests addresses as the buyer types. An order of the store data can list its `returns`. See
883
+ [customer account](docs/checkout.md#customer-account).
884
+ - **The commands of a Shopify app are under `lsf app`**, named as Shopify's CLI names them:
885
+ `app dev` serves the app alone (its checkout and its customer account), `app init` writes
886
+ checkout scenarios, `app build` builds its extensions, `app check` runs the scenarios for CI,
887
+ `app function run` and `app function build` its Functions. [CLI reference](docs/cli.md#app).
888
+ - **Testing an app's checkout in CI.** `lsf app check` runs the Functions of every scenario
889
+ and exits with 1 when one fails or does not validate, or when there is no scenario. `lsf validate`
890
+ checks the app of the folder too (with the demo store when the app has no store data), and its
891
+ JSON counts the app's errors with the data's. The checkout and the customer account set `data-lsf-ready` on the document once nothing is
892
+ on its way and every extension rendered (`aria-busy` meanwhile). `DELETE /__lsf/sessions` resets
893
+ every session. The orders of a session are numbered after the last one of the store data, one
894
+ after the other whatever the other sessions do, and placed at its `now`: a test reads the same
895
+ number and date every time. [Testing guide](docs/testing.md#a-checkout-in-a-test).
896
+ - **Commands of an app need no `--app` in its folder** (`lsf app dev`, `lsf app init`,
897
+ `lsf app function run`, `lsf app check`), and read its `shopify-local/` from anywhere.
898
+ `lsf app init` gives every Function the settings its input query reads
899
+ (`[extensions.input.variables]`): those of the app's own store data, or empty values of the
900
+ right type in `app-settings.json`, so that the scenarios it writes pass `app check`; a
901
+ variable without a value says where to give it. The frame of an extension is in the language
902
+ it renders in (`lang`). `lsf app function run` says why a
903
+ Function asked for did not run (no discount names it, an empty cart, an error of the app) and
904
+ exits with 1; a handle or a scenario close to an existing one is suggested. Its `--session`
905
+ takes `data` as `PUT /__lsf/session` does: a discount or settings for one run.
906
+ - **Watch mode for the checkout of an app.** `lsf app dev --app <dir> --watch` (and `serve
907
+ --app <dir> --watch`) builds a Function again when its files (or the files the Functions
908
+ share, such as a Cargo workspace's `Cargo.toml`) change, and reloads the open checkout,
909
+ thank-you and home pages once the builds are over, as `shopify app dev` does. At start it
910
+ builds only the Functions that have no module or changed since the last watch (all with
911
+ `--build`). The checkout runs a copy of the last module that built, never what a build is
912
+ writing: a build that fails keeps it and shows its first error on the page and on the home
913
+ page. A checkout UI extension's bundle, `shopify.app.toml` and the scenarios reload the pages
914
+ too. See [watch mode](docs/checkout.md#watch-mode).
915
+ - **Log in as a customer at the checkout.** "Log in" in the Contact section of the local
916
+ checkout opens a menu of the customers of the store data. Picking one logs the checkout (and
917
+ the session) in as them: the Contact section shows the account, Delivery offers their saved
918
+ addresses with the default one chosen, a B2B buyer picks the location they buy for, and the
919
+ Functions and extensions read the new buyer. `/checkout?customer=<email>` opens the checkout
920
+ logged in, and the home page lists the customers. The demo store has a profile of each kind
921
+ (new, returning, B2B, abroad). See [log in as a customer](docs/checkout.md#log-in-as-a-customer).
922
+ - **The checkout looks like Shopify's.** The standard look of the local checkout now follows
923
+ Shopify's one-page checkout: its header, the two columns with the grey order summary, floating
924
+ labels, grouped payment and shipping choices, the order summary with its quantity badges, and
925
+ a mobile layout with a folded summary. The checkout UI extensions use the same design tokens
926
+ (`tokens.css`), so they look native in it. See [the look of the page](docs/checkout.md#the-look-of-the-page).
927
+ - **`lsf app function run [handle]... --app <dir>`** runs the Functions once, without a server, and
928
+ prints each input, output and log with the checkout that results. `--session` and
929
+ `--checkout` set the cart and what the buyer entered, `--format json` is for tools. It exits
930
+ with `1` when a Function fails to run.
931
+ - **A `checkout` section in the store data** sets the shipping rates, the payment methods, the
932
+ discounts that use the Functions of the app, the settings of its other Functions and a tax
933
+ rate. Without it: one "Standard" rate at 5.00, three payment methods and no tax.
934
+ - **The checkout in the control API.** `/checkouts/cn/<token>` serves the page, `.json` its
935
+ state, `/update` takes operations and `/complete` places the order, which `/thank-you` shows.
936
+ `GET /__lsf/checkout?session=<id>` reads the state of a session for tests. Without `--app`,
937
+ `/checkout` is the summary of the cart, as before.
938
+ - **The Functions of the checkout can read the fields they could not.** An input query that asks
939
+ for `Cart.deliverableLines`, the discount applications and allocations of the cart, its lines
940
+ and its delivery groups (each Function sees the discounts the steps before it applied),
941
+ `PurchasingCompany.contact`, the dates, locale and totals of companies and their locations,
942
+ `MailingAddress.market`, `PaymentCustomizationPaymentMethod.placements` or `Shop.metaobject`
943
+ now runs instead of failing with the field's path. The store data sets them: `created_at`,
944
+ `updated_at`, `locale`, `orders_count` and `total_spent` on companies and locations, a
945
+ `company_contact` on a customer, `localization.markets`, `placements` on a payment method;
946
+ everything has a default. Metaobjects are the ones of the data.
947
+ - **Network access for Functions.** The `.fetch` targets of the lines discounts, delivery
948
+ discounts and cart validation Functions run: the checkout sends the HTTP request they ask for
949
+ and gives the response to the `.run` target as `fetchResult`, with Shopify's limits (GET and
950
+ POST, HTTPS, a timeout of 100 to 2000 ms, 100 KB, no redirect; `http` is accepted for
951
+ `localhost`). The request and the response show in the Functions dialog and in
952
+ `lsf app function run`.
953
+ - **A deferred payment in the checkout.** A line sold with a selling plan that takes a deposit
954
+ (a pre-order, try before you buy: `checkout_charge` in the store data) is no longer charged in
955
+ full. The order summary shows the total, what is **due today** and what is **due later**; the
956
+ state (`.json`) has `cost.dueNow`, `cost.dueLater` and `cost.checkoutChargeAmount` (the
957
+ `cart.checkout_charge_amount` of the storefront cart) and `dueLater` on the lines; the order
958
+ records `totalReceived` and `totalOutstanding`, and the thank-you page shows the remaining
959
+ balance. `lsf app function run` prints it. Shipping is due today and tax follows the amount it is
960
+ on. The `shopify` global and the Function input do not change: Shopify exposes no deferred
961
+ amount there.
962
+ - **A home page for the checkout.** `GET /` of `lsf app dev` (and `/__lsf/checkouts`, which
963
+ `lsf serve --app` prints when it starts, as the theme owns `/`) lists every Function of the
964
+ app (API, targets, build status, network access with a `.fetch` target, what is wrong with
965
+ it) and every checkout UI extension (API version, capabilities, targets, and the place each
966
+ target has on the page, or that it has none), and a card for every checkout scenario, with a
967
+ link that opens a checkout for it. `?format=json` answers what it says.
968
+ - **Checkout scenarios.** A file of `checkouts/` in the store data (`checkouts/<name>.json`) is a
969
+ named checkout setup, on top of the store data: the cart (lines, quantities, selling plans,
970
+ note, attributes), the buyer (a guest, a customer, a company location), the country and
971
+ language, what the buyer entered (email, address, discount codes, delivery, payment),
972
+ overrides of the `checkout` section, the look and the extensions that run.
973
+ `/checkout?scenario=<name>` opens a fresh checkout for it, in a new session. Scenarios are
974
+ validated (`validate`, with the codes of the data guide), reloaded when they change, have a
975
+ JSON Schema (`schema checkout-scenario`, and `schema/` written by `init`), and one with errors
976
+ is listed with them, not hidden.
977
+ - **`lsf app init [--app <dir>]`** writes the scenarios: for the Functions and checkout UI
978
+ extensions of the app (a cart a cart transform can merge or expand, a cart and a code for a
979
+ discount, several shipping rates or payment methods, a cart a validation refuses, one with
980
+ everything for the extensions), and always a guest, a customer, a B2B buyer, another country
981
+ and language, a subscription, a pre-order with a deposit, the Shop look and a large cart. It
982
+ creates the data folder with the demo store if there is none, uses the products of an existing
983
+ one, and never overwrites without `--force`.
984
+ - **`/checkout?targets=all`** opens a checkout where every extension renders at every target it
985
+ declares: the steps of a cart that ships are always there, the thank-you page is shown under the
986
+ checkout, and a panel lists each target as shown, without a place on the page, or waiting for
987
+ something the cart lacks.
988
+ - **`lsf app function run --scenario <name>`** runs the Functions for a scenario.
989
+ - **`lsf app function build`** builds the Functions of an app. A JavaScript or TypeScript Function
990
+ without a build command, as Shopify's CLI generates it, is built by lsf as
991
+ `shopify app function build` builds it, without Node: its types (`generated/api.ts`, as
992
+ graphql-codegen writes them), `dist/function.js` (bundled as esbuild does, enums inlined and
993
+ unused code left out), then its module, the bytes Javy writes. `--build` and `--watch` build
994
+ it too.
995
+ - **Rust Functions run as `cargo build` writes them**: after a Function's build command
996
+ (`--build`, `--watch`, `lsf app function build`), a module that calls the Function Wasm API by name
997
+ is linked to it with Shopify's `shopify-function-trampoline` (2.0.1, as a library), as
998
+ `shopify app build` does, to the byte.
999
+ - **`lsf app build`** builds the extensions of an app as `shopify app build` does, without
1000
+ Node: its Functions, and its checkout UI extensions whose targets name their sources, bundled
1001
+ (JSX with the `jsxImportSource` of their `tsconfig.json`, minified) into `dist/<handle>.js`.
1002
+ `--build` and `--watch` build those too. Without a `node_modules`, `preact`,
1003
+ `@preact/signals`, `@shopify/ui-extensions` and `@shopify/shopify_function` come from lsf's
1004
+ own copy.
1005
+
1006
+ ### Changed
1007
+
1008
+ - **Customer accounts are Shopify's new ones only.** Classic customer accounts are no longer
1009
+ simulated: the theme's `templates/customers` are never rendered, every `/account/…` page and
1010
+ account form (login, register, password reset, addresses) leads to `/account`, and customers
1011
+ have no password. Data that still sets `shop.customer_accounts` or a customer's `password` is
1012
+ read as before: those fields are ignored, with a warning. `serve` and `check` say that the
1013
+ theme's `templates/customers` are not rendered (a warning of `check`, which does not fail). `/__lsf/status` no longer has
1014
+ `customer_accounts`.
1015
+ - **A field the format does not have no longer drops its file.** It is still an error, and is
1016
+ left out: the rest of the file is read, and one typo gives one error. `data.checkout` of
1017
+ `PUT /__lsf/session` replaces the store data's field by field, as a scenario's does, and keeps
1018
+ the settings of the Functions it does not name.
1019
+ - **The binaries are larger: they run the checkout.** lsf runs Functions with only what it uses
1020
+ of wasmtime (the compiler for the machine it runs on) and Shopify's providers compressed
1021
+ (1.6 MB instead of 9.7 MB), and has one copy of each Wasm library; the release binaries leave
1022
+ out their symbol table and are optimized as a whole. `lsf` for macOS on Apple silicon is 40 MB
1023
+ (18 MB to download), up from 33 MB (12 MB) in 0.1.18. The storefront renders as fast as
1024
+ before.
1025
+
1026
+ ## 0.1.18 - 2026-10-21
1027
+
1028
+ ### Changed
1029
+
1030
+ - **Security update of the dependencies.** 33 crates are updated, among them `brotli` 9,
1031
+ `jsonschema` 0.59 and `base64` 0.23. Nothing changes in what `lsf` does.
1032
+ - **The release binaries are built with Rust 1.99.1**, up from 1.98.1.
1033
+ - **The npm packages are built and published with Node 24.22.1.** The release and npm workflows
1034
+ pin it, and the development toolchain pins Node 26.12.1.
1035
+
1036
+ ## 0.1.17 - 2026-10-20
1037
+
1038
+ ### Added
1039
+
1040
+ - **The pages of metaobjects are served.** With `templates/metaobject/<type>.json` (or
1041
+ `.liquid`) in the theme, `/pages/<type>/<handle>` renders the entry, with the global
1042
+ `metaobject`, `?view=` and a per-entry `template_suffix` in the data. `shopify://metaobjects/`
1043
+ links lead there, and `metaobject.system.url` is nil for a type without a template, as on a
1044
+ store whose definition has no web pages.
1045
+ - **A 404 page says why.** The server answers it with an `x-lsf-not-found` header (no route,
1046
+ unknown resource, no template) and logs the reason, so a test can fail on it.
1047
+ - **`shopify_asset_url` files are served.** `vendor/qrcode.js` is the MIT qrcode.js library that
1048
+ Shopify serves, so a gift card draws its QR code. The other documented files are small local
1049
+ stand-ins: `shopify_common.js` formats money, the others only warn in the console.
1050
+
1051
+ ### Fixed
1052
+
1053
+ - **`metaobject` and `metaobject_list` settings are metaobjects**, not the string of the
1054
+ template: a handle, `type.handle`, `type/handle` or a `gid://shopify/Metaobject/` id.
1055
+ - **The search follows `sort_by`**: `relevance`, `price-ascending` and `price-descending`, with
1056
+ the other results after the products under a price order, as Shopify does. `search.sort_by`
1057
+ says which order applies and `search.sort_options` lists those three.
1058
+ - **`'now' | date: '%N'` differs between calls.** Each `'now'` reads the clock again, as on
1059
+ Shopify, so ids built from it no longer repeat. With a fixed `now` in the data, each read is one
1060
+ nanosecond after the previous one: renders stay reproducible.
1061
+
1062
+ ## 0.1.16 - 2026-10-14
1063
+
1064
+ ### Added
1065
+
1066
+ - **`npx local-storefront check` finds what Shopify's strict Liquid parser rejects, and
1067
+ `npx local-storefront check --fix` rewrites it.** Shopify is moving every store to a strict
1068
+ parser, and a file it rejects can no longer be uploaded
1069
+ (`Expected end_of_string but found pipe`). `npx local-storefront check` reports each tag and
1070
+ output it rejects, with Shopify's message and the fix, in `.liquid` files and in the `liquid`
1071
+ settings of JSON templates, section groups and `config/settings_data.json`.
1072
+ - Where what the code meant is clear, `--fix` does it, and the report says how the output
1073
+ changes: a filter in a `render` argument is computed into a variable on the line above, an
1074
+ `and` in an argument is evaluated, `('X' | downcase)` is the string and not the variable
1075
+ `X`, `reversed` after `limit` reverses, `=` in a condition compares, and the body of
1076
+ `{% when 'x' and y %}` goes in `{% if y %}` (a `case` with an `else` becomes
1077
+ `if`/`elsif`/`else`).
1078
+ - Elsewhere it keeps today's output, as Shopify's own migration does, without the comment it
1079
+ leaves in the file.
1080
+ - A fix keeps the layout of the markup and is checked before it is written. Markup without a
1081
+ fix is reported with the reason.
1082
+ - `--format json` prints one JSON document, for tools and AI agents: each problem with its
1083
+ code, file, line, markup and fix.
1084
+
1085
+ See [strict
1086
+ Liquid](https://github.com/0xtlt/local-storefront/blob/main/docs/strict-liquid.md).
1087
+
1088
+ ### Changed
1089
+
1090
+ - **The documentation is organised by task.** The README is short: what local-storefront does,
1091
+ how to start, and a link per task. Each task has its guide in `docs/` (data, testing, strict
1092
+ Liquid, formatting, performance, storefront), next to two references: the
1093
+ [CLI](https://github.com/0xtlt/local-storefront/blob/main/docs/cli.md), with every option,
1094
+ exit code and output format, and the [control
1095
+ API](https://github.com/0xtlt/local-storefront/blob/main/docs/control-api.md).
1096
+ [docs/agents.md](https://github.com/0xtlt/local-storefront/blob/main/docs/agents.md) tells an
1097
+ AI agent how to drive local-storefront, and `llms.txt` lists the documentation for language
1098
+ models.
1099
+ - Messages name the subcommand to run (`the init command`, `run again with --fix`) rather
1100
+ than `lsf`, which is only the command of the installs that are not from npm.
1101
+
1102
+ ## 0.1.15 - 2026-10-14
1103
+
1104
+ ### Added
1105
+
1106
+ - **`lsf fmt`** formats the Liquid files of a theme the way Prettier does with Shopify's
1107
+ Liquid plugin, without Node: Liquid, HTML, the JSON of `{% schema %}`, the CSS of
1108
+ `<style>`, `{% style %}` and `{% stylesheet %}`, and the JavaScript of `<script>` and
1109
+ `{% javascript %}`. `lsf fmt --check` lists the files that are not formatted and fails if
1110
+ there are any, for CI. The 283 files of Shopify's Horizon theme are checked in 0.08 s, where
1111
+ Prettier takes 1.6 s.
1112
+ - It reads the configuration Prettier reads: `.prettierrc`, `.editorconfig` and
1113
+ `.prettierignore`. `--option printWidth=100` sets an option from the command line, and
1114
+ `--stdin` (with `--stdin-filepath`) formats what an editor sends. Prettier's options for
1115
+ JavaScript (`semi`, `trailingComma`, `arrowParens`, ...) apply too.
1116
+ - Files named one by one, as a commit hook names them, are formatted when they are `.liquid`
1117
+ files that `.prettierignore` does not name. A file is replaced in one step, with its
1118
+ permissions. A file that cannot be formatted is reported, and the others are formatted
1119
+ all the same.
1120
+
1121
+ See [what differs from Prettier](https://github.com/0xtlt/local-storefront/blob/main/docs/compatibility.md#formatting-lsf-fmt).
1122
+ - **Feature previews.** `--preview <name>` (or `LSF_PREVIEW`) turns on a feature preview of
1123
+ Shopify, as a development store can. Without it, a page renders as on a live store: a theme
1124
+ that uses a preview gets the error a live store gives, and `lsf` names the preview to turn
1125
+ on. The first one is `liquid-july-26`, Shopify's "Liquid July '26 changes":
1126
+ - **`{% block 'name', parameter: value %}content{% endblock %}`** renders
1127
+ `blocks/name.liquid` from a template or a layout. The block sees its parameters and its
1128
+ body, as `content` or `block.content`; a parameter named after a setting of its schema
1129
+ sets `block.settings`; arrays can be written in place. With it, Shopify's Skeleton theme
1130
+ renders.
1131
+ - **`{% partial 'name' %}`** and **`@shopify/partial-rendering`**: a theme that uses
1132
+ partials gets an import map for the module, and `partials.refresh('name')` swaps in the
1133
+ region from the server, keeping focus, form values and scroll, in loops and table rows
1134
+ too. Shopify has not published its own markup yet: lsf marks a partial with HTML comments.
1135
+
1136
+ ### Fixed
1137
+
1138
+ - A `file_reference` metafield or metaobject field that points to a video (`.mp4`, `.mov`,
1139
+ `.webm`, `.m4v`) is a video, as on Shopify: `sources`, `preview_image` and `video_tag`
1140
+ work, and `metafield_tag` prints a `<video>` as Shopify does. It was a plain file with only
1141
+ a `url`, so themes rendered it empty. The poster is the `.jpg` of the same name. A `.mov` is
1142
+ announced as `video/mp4`, as Shopify announces the MP4 it makes of it, so that Chrome and
1143
+ Firefox play it; the `video` setting does the same.
1144
+ - Files of `files/` are served in parts when the browser asks with a `Range` header. Safari
1145
+ played no video without it, and no browser could seek in one. Only the part asked for is
1146
+ read, and a part is never compressed. `.m4v` files are served as `video/mp4`.
1147
+ - Metafield values are the objects Shopify gives for four more types. A `color` is a color
1148
+ (`.red`, `.rgb`...), not text. An `article_reference` is the article, named `blog/article`
1149
+ or by the handle of an article that only one blog has. Every measurement type (`distance`,
1150
+ `temperature`, `duration`...) has a `type`, a `value` and a `unit`, and `metafield_tag`
1151
+ prints it with two decimals at most, where only `weight`, `volume` and `dimension` were
1152
+ printed. A `money` has the `currency` it is written in, and is formatted in it: `€5,00 EUR`
1153
+ in a store that sells in dollars.
1154
+ - `request.locale.direction` (and that of every locale) is `rtl` for Arabic, Hebrew, Persian
1155
+ and the other languages written right to left, and follows the script when the locale names
1156
+ one (`pa-Arab`). Horizon sets `dir` on `<html>` from it.
1157
+ - The `applicable_disclosures` filter keeps every disclosure, as Shopify does when it cannot
1158
+ tell the buyer's country. It was an unknown filter that printed a warning.
1159
+
1160
+ ## 0.1.14 - 2026-10-06
1161
+
1162
+ ### Fixed
1163
+
1164
+ - `lsf serve` gives back the memory it used to encode images. A server left running for
1165
+ three hours on a large theme held 1 GB. After every image of that theme is asked for,
1166
+ 277 MB are left where 664 MB were, and pages render as fast as before.
1167
+
1168
+ ## 0.1.13 - 2026-10-05
1169
+
1170
+ ### Added
1171
+
1172
+ - **`Server-Timing` on every response**, as on a Shopify storefront: `processing` for the
1173
+ whole, then `render`, `template`, `layout`, `compress` and `throttle`.
1174
+ `lsf serve --timings` also names every section and every theme block.
1175
+ - **`lsf profile <path>`** renders one page and prints what rendered what: the template, the
1176
+ layout, each section, its blocks and their snippets, with the time of each. `--lines` goes
1177
+ down to the tag.
1178
+ - **`lsf profile --points`** counts what the page asks of a store instead of the time it
1179
+ takes here: a product, a metafield, a search. A local server reads a metafield as fast as
1180
+ a title, Shopify does not. `--cost` changes what each kind of thing costs.
1181
+ - **Flame graphs.** `/__lsf/profile?path=<path>&html=1` opens the profile in speedscope, the
1182
+ viewer that `shopify theme profile` opens. It is embedded: nothing is fetched.
1183
+ - `/__lsf/timings` adds up the renders so far by template, section and block.
1184
+
1185
+ ## 0.1.12 - 2026-10-04
1186
+
1187
+ The responses of `lsf serve` now weigh what a Shopify storefront sends, so that the sizes a
1188
+ browser or an audit reports are those of the real store.
1189
+
1190
+ ### Added
1191
+
1192
+ - **Minified stylesheets and scripts**, by the rules Shopify follows, with source maps: the
1193
+ tools of a browser still show the files as written. `--no-minify` serves them as they are.
1194
+ - **Images in the lightest format the browser reads.** AVIF, WebP or the format of the file,
1195
+ chosen from the `Accept` header as Shopify's image CDN does.
1196
+ - **`Cache-Control`** as Shopify sends it for each kind of response: pages, assets, images,
1197
+ fonts, the cart.
1198
+ - **The resource hints Shopify adds.** The `Link` header preloads the stylesheets and the
1199
+ scripts that block rendering, and connects to the origins they come from.
1200
+
1201
+ ### Fixed
1202
+
1203
+ - The `Link` header of `preload: true` and `preload_tag` is written as Shopify writes it,
1204
+ one header with quoted values. A browser could not read the previous form for an image
1205
+ with a `srcset`.
1206
+ - A font preloaded with `preload_tag` gets `crossorigin`, without which a browser does not
1207
+ use it.
1208
+ - The `?v=` of a `.liquid` asset changes with the theme settings it is rendered with, not
1209
+ only with its source.
1210
+ - A placeholder image served from the cache says that it is one (`x-lsf-placeholder`).
1211
+
1212
+ ## 0.1.11 - 2026-10-03
1213
+
1214
+ ### Added
1215
+
1216
+ - **Responses are compressed** with Brotli or gzip for the clients that accept it, as on a
1217
+ storefront. A 420 KB collection page of Horizon becomes 53 KB. `--no-compression` sends
1218
+ responses as they are.
1219
+
1220
+ ### Fixed
1221
+
1222
+ - `{{-` and `{%-` trim whitespace the way storefronts do, which is not quite what the
1223
+ language says: when the text before one is nothing but whitespace, its first character
1224
+ stays. Line breaks in a `<title>` or between two outputs now match Shopify's.
1225
+ - The rules of `robots.txt` are on their own lines, those a theme adds with
1226
+ `{{ 'Disallow: ...' }}` included.
1227
+
1228
+ ## 0.1.10 - 2026-10-03
1229
+
1230
+ ### Changed
1231
+
1232
+ - **`--live-reload` goes through a WebSocket.** A page reloads about 100 ms after a save,
1233
+ where it asked the server every 700 ms. It still does when the socket cannot open, behind
1234
+ a proxy for example.
1235
+ - A page survives a restart of the server, and reloads if the files changed meanwhile.
1236
+ - A save or a build that writes several files reloads the pages once.
1237
+ - Only the directories a theme is made of and the data directory are watched, which is much
1238
+ faster for a theme that keeps `node_modules` next to its sources. A data directory outside
1239
+ the theme (`--data`) now reloads the pages. A file outside those directories no longer
1240
+ does.
1241
+
1242
+ ## 0.1.9 - 2026-10-03
1243
+
1244
+ ### Added
1245
+
1246
+ - **Selling plans.** `selling_plan_groups` in the data describes subscriptions. Products and
1247
+ variants expose their groups and allocations, `?selling_plan=<id>` selects a plan, and
1248
+ `/cart/add.js` and `/cart/change.js` take `selling_plan`.
1249
+ - **Store pickup.** `locations` in the data are the places that stock the products, and
1250
+ variants have `store_availabilities`. `/variants/<id>?section_id=` renders a section for a
1251
+ variant, which is how Dawn loads pickup availability.
1252
+ - **Native swatches.** `swatches` in the data gives a color or an image to option values:
1253
+ `product_option_value.swatch` and `filter_value.swatch` are filled.
1254
+ - The demo store has a subscription, two locations and swatches for its colors.
1255
+
1256
+ ## 0.1.8 - 2026-10-02
1257
+
1258
+ ### Changed
1259
+
1260
+ - Without `--port`, `lsf serve` listens on the next free port when 9292 is taken, up to
1261
+ 9391, and says which. A port given with `--port` is still that port or an error.
1262
+
1263
+ ## 0.1.7 - 2026-10-02
1264
+
1265
+ The Liquid engine is now checked against
1266
+ [liquid-spec](https://github.com/Shopify/liquid-spec), Shopify's test suite of the language:
1267
+ 4,608 of the 4,630 specs that apply pass. The others are listed in
1268
+ [compatibility](https://github.com/0xtlt/local-storefront/blob/main/docs/compatibility.md).
1269
+
1270
+ ### Added
1271
+
1272
+ - `self`, the object that reads the variables of the current scope (`self[name]`).
1273
+
1274
+ ### Fixed
1275
+
1276
+ - `truncate` crashed when given the smallest 64-bit integer.
1277
+ - Comparing with a float that is not a number raised an error instead of being false.
1278
+ - `replace`, `replace_first` and `remove` ignored the back-references of the replacement
1279
+ (`\'`, `` \` ``, `\+`, `\k<name>`).
1280
+ - Floats from 1e15 on were printed in fixed notation, and some JSON numbers were read one
1281
+ bit off.
1282
+ - `{% assign x ) = 1 %}` was accepted, and `{% docEXTRA %}` was taken for a nested `doc`.
1283
+ - An error at the end of a `{% liquid %}` tag was reported one line too far.
1284
+ - A contact or newsletter form given an `id` posts back to that id
1285
+ (`action="/contact#ContactForm"`), as on Shopify.
1286
+
1287
+ ## 0.1.6 - 2026-10-02
1288
+
1289
+ ### Added
1290
+
1291
+ - **Customer accounts.** `/account` shows who is logged in and lets the visitor become any
1292
+ customer of the data. A theme with `templates/customers` keeps its own pages. A customer
1293
+ is named by email, `default` or `none`: in the data, in a session, with `/__lsf/login` or
1294
+ with `lsf serve --customer`.
1295
+ - **B2B.** Companies with their locations, the customers that buy for them, and the objects
1296
+ that go with them: `customer.b2b?`, `current_company`, `current_location`. Catalogs are
1297
+ not simulated.
1298
+ - **`/robots.txt` and `/sitemap.xml`.** `robots.txt` renders `templates/robots.txt.liquid`,
1299
+ or Shopify's default rules.
1300
+ - **`/password`** shows the password page of the theme and accepts `shop.password`.
1301
+ - `placeholder_svg_tag` draws Shopify's own illustrations for the three names that are
1302
+ published under a free license.
1303
+
1304
+ ### Changed
1305
+
1306
+ - A password in the data no longer locks the storefront: nothing redirects to `/password`.
1307
+
1308
+ ## 0.1.5 - 2026-10-02
1309
+
1310
+ ### Added
1311
+
1312
+ - **`lsf serve --throttle`** answers requests late, so that loading states can be tested:
1313
+ every request (`--throttle 300ms`) or one kind of request
1314
+ (`--throttle cart=500ms,cart-add=1s`).
1315
+ - **Presets.** `simulated` answers as late as a Shopify storefront on a good connection,
1316
+ `slow` as on a slow mobile connection. A preset can be adjusted:
1317
+ `--throttle simulated,cart-add=2s`.
1318
+ - A session can have a throttle of its own through `PUT /__lsf/session`: one test can be
1319
+ slow without slowing the suite.
1320
+
1321
+ ### Changed
1322
+
1323
+ - The README is written around what a user does, and documents every option of `lsf serve`.
1324
+
1325
+ ## 0.1.4 - 2026-10-02
1326
+
1327
+ ### Added
1328
+
1329
+ - **The scripts Shopify adds to a page.** `content_for_header` follows Shopify's, with the
1330
+ `Shopify` object and `Shopify.loadFeatures`, and pages get `ShopifyAnalytics.meta` and
1331
+ `Shopify.analytics`.
1332
+ - **`Shopify.actions`** (`getCart`, `updateCart`, `openCart`) and the `shopify:cart:*`
1333
+ events, written against the local cart.
1334
+ - **`Shopify.customerPrivacy`**, loaded with `consent-tracking-api`: consent is kept in the
1335
+ browser.
1336
+ - The `standard_event_data` filter, for products, collections and the cart.
1337
+ - A page can render a section that belongs to another template, which a cart drawer does to
1338
+ load recommendations.
1339
+
1340
+ ### Changed
1341
+
1342
+ - The `cart` cookie is set when a cart is created, `/cart.js` bundles sections, and
1343
+ `/cart/update.js` removes the attributes set to null.
1344
+
1345
+ ## 0.1.3 - 2026-10-02
1346
+
1347
+ ### Fixed
1348
+
1349
+ - `image_url` accepts decimal sizes, which themes compute with `width | times: ratio`. They
1350
+ raised "invalid integer", and the image was left out.
1351
+
1352
+ ## 0.1.2 - 2026-10-02
1353
+
1354
+ ### Added
1355
+
1356
+ - **`lsf` is on npm.** `npm install --save-dev local-storefront` gives a project the
1357
+ command, with the binary of its platform only. Run it with `npx local-storefront`:
1358
+ `npx lsf` can download an unrelated package of that name.
1359
+
1360
+ ## 0.1.1 - 2026-10-02
1361
+
1362
+ ### Changed
1363
+
1364
+ - **Rendering scales with the number of clients.** On a 14-core machine, the collection page
1365
+ of Horizon goes from 70 pages per second to 430 with one client, and from 107 to 3435
1366
+ with fourteen. The HTML is the same to the byte.
1367
+ - With `--static`, an asset edited while the server runs keeps its `?v=`: nothing is read
1368
+ from disk twice.
1369
+
1370
+ ## 0.1.0 - 2026-10-02
1371
+
1372
+ First release. `lsf` renders a Shopify theme without calling Shopify, so that end-to-end
1373
+ tests run against a local server with no rate limit.
1374
+
1375
+ - **A port of Shopify's Liquid**, checked against the reference gem by 548 differential
1376
+ tests, with Shopify's objects, filters and tags, sections, blocks, JSON templates and
1377
+ locales.
1378
+ - **Store data as JSON files**, with a JSON Schema and a validation that explains mistakes.
1379
+ - **`lsf serve`**: storefront pages, the Cart Ajax API, the Section Rendering API, forms,
1380
+ and images transformed and served locally.
1381
+ - **A control API** that gives each test session its own cart, customer and data.
1382
+ - The other commands: `render`, `validate`, `check`, `init`, `routes`, `schema`, `docs`.
1383
+ - One binary for macOS, Linux (glibc 2.17 or later, and static musl) and Windows, on x86-64
1384
+ and ARM64.