local-storefront 0.2.0 → 0.3.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/cli.md ADDED
@@ -0,0 +1,873 @@
1
+ # CLI reference
2
+
3
+ Every command of local-storefront, its options, what it prints and how it exits.
4
+ `npx local-storefront <command> --help` prints the same options.
5
+
6
+ Installed with mise, from a release or from source, the command is `lsf`: `lsf serve` is
7
+ `npx local-storefront serve`.
8
+
9
+ ## Options of every command
10
+
11
+ | Option | Environment variable | Default | What it does |
12
+ |---|---|---|---|
13
+ | `--theme <dir>` | `LSF_THEME` | `.` | The theme folder: the one with `layout/`, `sections/`, `templates/`. |
14
+ | `--data <dir>` | `LSF_DATA` | `<theme>/shopify-local` | The store data folder. Without one, the built-in demo store. |
15
+ | `--preview <name>` | `LSF_PREVIEW` | none | Turns on a Shopify feature preview. Repeat it, or separate names with commas. See [storefront](storefront.md#feature-previews). |
16
+
17
+ ## Exit codes
18
+
19
+ | Code | Meaning |
20
+ |---|---|
21
+ | `0` | Done, nothing to report. |
22
+ | `1` | Done, and something to fix: data or app errors, theme app extensions included (`validate`), problems in the theme (`check`), Liquid errors in the page or nothing at its path (`render --strict`), files not formatted (`fmt --check`), a Function that failed to run or did not run, a fixture whose output, target or export is not the run's (`app function run`), a scenario that fails or whose `expect` is not met, an extension the checkout does not offer, a stale module, an error in a theme app extension (`app check`), an extension that does not build or a theme app extension Shopify rejects (`app build`, `app function build`, `--build`), a webhook that was not delivered (`app webhook trigger`). |
23
+ | `2` | The command could not run: a wrong option, a theme or a file that cannot be read, a file `fmt` cannot write, a port `serve` cannot listen on. The message is on stderr, after `error:`. |
24
+
25
+ ## Output
26
+
27
+ Results go to stdout; messages and summaries go to stderr. For a tool, `--format json` prints
28
+ one JSON document on stdout: `validate`, `check`, `app check`, `app function run` and
29
+ `app webhook trigger` take it. For a workflow of GitHub Actions, `--format github` prints
30
+ annotations: `check`, `validate` and `render` take it. The other commands print text and refuse
31
+ `--format`. What each one prints: [agents](agents.md#what-each-command-prints).
32
+
33
+ ## serve
34
+
35
+ `npx local-storefront serve`
36
+
37
+ Serves the theme over HTTP. With `--app`, the checkout and the Admin API of the app too;
38
+ `--only` runs a part of them ([what runs](#what-runs)).
39
+
40
+ ```bash
41
+ npx local-storefront serve --port 9292 --static --quiet --strict
42
+ ```
43
+
44
+ | Option | Environment variable | What it does |
45
+ |---|---|---|
46
+ | `--port <port>`, `-p` | | Listens on this port and no other: the server does not start if it is taken. `0` picks a free one. Without the option: `9292`, or the next free port up to `9391`, which a line says (`port: 9292 is taken, using 9293`). |
47
+ | `--host <address>` | | Address to listen on. Default `127.0.0.1`. `0.0.0.0` in a container. |
48
+ | `--static` | | Reads the theme and the data once. The fastest mode, for tests. |
49
+ | `--strict` | | Refuses to start when the store data has errors. |
50
+ | `--quiet`, `-q` | | Does not log requests. |
51
+ | `--live-reload` | | Reloads open pages when the theme or the data change. It adds a script to the pages: leave it off for tests. |
52
+ | `--customer <email>` | `LSF_CUSTOMER` | Visitors arrive logged in as this customer. Also `default` (the first customer) or `none`. |
53
+ | `--throttle <rules>` | `LSF_THROTTLE` | Answers late, to see loading states: `simulated`, `slow`, `300ms`, `cart-add=1s`. See [testing](testing.md#simulating-a-slow-network). |
54
+ | `--timings` | | Names every section and theme block in the `Server-Timing` header. See [performance](performance.md#render-times). |
55
+ | `--no-minify` | | Serves styles and scripts as written. Without it, they are minified with a source map, as on Shopify. |
56
+ | `--no-compression` | | Sends responses uncompressed. Without it, they are compressed (Brotli or gzip) for clients that accept it, as on Shopify. |
57
+ | `--app <dir>` | `LSF_APP` | A Shopify app. `/checkout` then runs its Functions and shows its checkout UI extensions, `/account` shows its customer account UI extensions, its backend gets the [Admin API](admin.md), and the theme renders the app blocks and app embed blocks of its [theme app extensions](storefront.md#theme-app-extensions). See [checkout](checkout.md). |
58
+ | `--only <parts>` | `LSF_ONLY` | Runs only these parts: `theme`, `checkout`, `admin`, comma-separated or repeated. Without it, the theme, and all three with `--app`. `checkout` and `admin` need `--app`. See [what runs](#what-runs). |
59
+ | `--extension <handle>` | | With `--app`: uses only this extension of the app. Repeat it for several. Without it, every extension is used. |
60
+ | `--build` | | With `--app`: builds the extensions first, as `app build` does: the Functions, then the UI extensions that name their sources. |
61
+ | `--no-functions` | | With `--app`: serves the app's UI extensions and its Admin API, but runs none of its Functions. For a backend's tests, whose orders a Function that fails would block: a discount of the data whose Function is not run is left out. A Function named with `--extension` is refused; not with `--watch`. |
62
+ | `--watch` | | With `--app`: builds a Function again when its files change, and reloads the open pages (it implies `--live-reload`). At start, builds only the Functions that have no module or changed since the last watch; with `--build`, all of them. Not with `--static`. See [watch mode](checkout.md#watch-mode). |
63
+ | `--variant <standard\|shop>` | | With `--app`: the look of the checkout page, `standard` by default. |
64
+ | `--app-url <url>` | `LSF_APP_URL` | Where the backend of the app listens (`http://localhost:3000`). The webhooks of `shopify.app.toml` go there (a relative `uri` is joined to it; an absolute one on the origin of `application_url` is moved to it), and so do the OAuth redirects and the launch of the embedded app. Without it, `application_url`. See [Admin API](admin.md). |
65
+ | `--client-secret <secret>` | `LSF_CLIENT_SECRET` | The secret webhooks, OAuth and session tokens are signed with. Default: `SHOPIFY_API_SECRET` of the environment, else of the app's `.env`, else `lsf-local-client-secret`, printed at start. |
66
+ | `--admin-throttle <plan>` | `LSF_ADMIN_THROTTLE` | The cost bucket of the Admin API, per shop and app: `store` (the default: Plus for store data with companies, Standard otherwise), `standard` (2000 points, 100 a second), `advanced` (4000, 200), `plus` (20000, 1000), `enterprise` (40000, 2000), `legacy` (1000, 50), or `off`. |
67
+ | `--admin-versions <list>` | `LSF_ADMIN_VERSIONS` | The versions of the Admin API served, comma-separated, among those lsf knows: `2026-10` alone by default, `2026-10,2027-01` with the release candidate. Another version answers `404`; one Shopify no longer supports is answered by the oldest stable version served, as Shopify does. See [API versions](admin.md#api-versions). |
68
+ | `--admin-fetch-local` | `LSF_ADMIN_FETCH_LOCAL` | Lets `themeCreate`, the `URL` bodies of `themeFilesUpsert` and `urlRedirectImportCreate` download their source from this machine or its network (the loopback, private, link-local and unique-local addresses), for a theme ZIP served from localhost on purpose. Off by default: Shopify cannot reach them, and a backend could otherwise read what the machine serves. See [Sources from a URL](admin-reference.md#themes-script-tags-redirects-in-bulk-and-policies). |
69
+ | `--admin-source <url>=<file>` | | Answers a URL with a file, never downloading it: what `themeCreate`, a `URL` body of `themeFilesUpsert` or `urlRedirectImportCreate` reads there. Repeatable. For your backend's own URLs (a theme ZIP on its CDN) that nothing serves in a test, wherever they point, `localhost` included. The file must exist when lsf starts, and is read at each use. `PUT /__lsf/admin/sources?url=` stubs others while lsf runs. See [Stubbed URLs](admin-reference.md#stubbed-urls). |
70
+ | `--pricing-plans <file>` | `LSF_PRICING_PLANS` | The plans of the app's [managed pricing](admin.md#managed-pricing) (Shopify App Pricing), which Shopify keeps in the Partner Dashboard: a TOML file of `[[plans]]` (JSON for a `.json` file). lsf's admin shows them at `/admin/charges/<app handle>/pricing_plans`, where the merchant chooses one, and the Billing API creates no charge, as on Shopify. A wrong file stops the start. Read at start, and again when it changes unless `--static` ([when the data changes on disk](#when-the-data-changes-on-disk); a wrong file then keeps the plans it had, and says why); `PUT /__lsf/admin/pricing-plans` changes them while lsf runs. Printed before `ready:` as `pricing: <n> plans (<file>): <page>`, then `partner: <url> (Partner API: …; token prtapi_lsf_…)`: where the app reads its plan ([Partner API](admin.md#read-the-plan-from-the-partner-api)). |
71
+ | `--polaris <online\|offline>` | `LSF_POLARIS` | What `/__lsf/cdn/<path>` answers, where an embedded app loads Polaris from: `online` (the default) redirects to Shopify's CDN, `offline` serves lsf's copy. See [Polaris without the internet](admin.md#polaris-without-the-internet). |
72
+
73
+ The options from `--extension` to `--variant` are the checkout's, those from `--app-url` to
74
+ `--polaris` the Admin API's. One given on the command line while its part does not run stops the
75
+ start: `error: --pricing-plans needs the admin part, which --only leaves out`, or without
76
+ `--app`, ``which needs the app: `--app DIR` (or LSF_APP)``. One set by its variable is ignored,
77
+ and said (`note: LSF_APP_URL (--app-url) is set, but the admin part is not served, …: ignored`):
78
+ a variable exported for `app dev` does not stop a `serve` of the theme alone.
79
+
80
+ Every line of the server goes to stderr; stdout stays empty. It prints the theme and the data
81
+ folder, the parts it runs (`serving: theme, checkout, admin`), then, once it takes requests, a
82
+ line that starts with `ready: http://127.0.0.1:<port>/`. With `--port 0`, read the port from
83
+ that line. Without `--quiet`, each request gets a line (`200 GET / 39.1ms`), with the reason of
84
+ a 404 above it.
85
+
86
+ With `--app`, the lines of the app come before `ready:`: how many checkout scenarios the data
87
+ has (when the checkout runs a Function), then, with the admin part, the [Admin API](admin.md)
88
+ of the store and its token, the client secret it signs with and where that secret comes from,
89
+ the webhooks of the app, its proxy, the embedded app in lsf's admin (`embedded:`), with
90
+ `--pricing-plans` the plans and the Partner API (`pricing:`, `partner:`), and with
91
+ `--polaris offline` where Polaris is served (`polaris:`). After `ready:`, with the theme and the
92
+ checkout, the address of the home page of the checkout, then `watch:` with `--watch`:
93
+
94
+ ```text
95
+ serving: theme, checkout, admin
96
+ scenarios: 10
97
+ admin: http://127.0.0.1:9292/admin/api/2026-10/graphql.json
98
+ X-Shopify-Access-Token: shpat_… (shop: local-supply-co.myshopify.com)
99
+ client secret: from SHOPIFY_API_SECRET of ../my-app/.env
100
+ webhooks: 1 subscription of shopify.app.toml → http://localhost:3000
101
+ ready: http://127.0.0.1:9292/ (status and control API: http://127.0.0.1:9292/__lsf)
102
+ home: http://127.0.0.1:9292/__lsf/checkouts (extensions and checkout scenarios of the app)
103
+ ```
104
+
105
+ Without `--app`, or with `--only` leaving the admin part out, it prints no `admin:`, secret,
106
+ `webhooks:`, `proxy:`, `embedded:`, `pricing:`, `partner:` or `polaris:` line. An app without an
107
+ `extensions` directory is a backend, and its line says so: `app: ../my-app (no extensions:
108
+ the Admin API, webhooks and the app proxy)`. The token is the same every run for a shop and an app; a tool reads it from
109
+ `GET /__lsf/admin` (`shops[0].token`). When the data changes on disk, the server reads it
110
+ again, and seeds the shops of the Admin API again only when the store's own data changed, not
111
+ for a checkout scenario ([when the data changes on disk](#when-the-data-changes-on-disk)).
112
+
113
+ While it runs, a webhook the backend refuses (`401`, `403`) gets one line, at its first refusal,
114
+ `--quiet` or not, naming the secret lsf signed it with as `client secret:` does:
115
+ `webhooks: products/create → http://localhost:3000/webhooks answered 401: lsf signed it with the
116
+ client secret from SHOPIFY_API_SECRET of ../my-app/.env: does the backend verify with the same
117
+ one?`.
118
+
119
+ It runs until it is stopped (Ctrl-C, `SIGTERM`, `SIGHUP`): then it finishes the requests it is
120
+ answering, for 3 seconds at most, abandons the rest, and exits with `0`. It exits with `2`
121
+ before `ready:` when it cannot listen on the port (`error: cannot listen on 127.0.0.1:9292:
122
+ Address already in use`), or, with `--strict`, when the store data has errors (each one, then
123
+ `error: the store data has errors (see above)`). A page whose Liquid never ends stops at its
124
+ render budget (see [the render budget](compatibility.md#the-render-budget)).
125
+
126
+ While it runs, tests talk to it through the [control API](control-api.md).
127
+
128
+ ### What runs
129
+
130
+ lsf has three parts:
131
+
132
+ - **theme**: the storefront rendered from the theme: its pages, the cart, search, and the
133
+ stand-ins of the checkout (`/checkout` lists the cart) and of the customer accounts
134
+ (`/account`). With `--app`, the app blocks and app embed blocks of the app's
135
+ [theme app extensions](storefront.md#theme-app-extensions), and their assets at
136
+ `/cdn/extensions/<id>/assets/`.
137
+ - **checkout**: what `--app` brings to the shop: the [checkout](checkout.md), where its
138
+ Functions run, its UI extensions (checkout, thank-you page, order status, customer account at
139
+ `/account`), the scenarios and the home page of the app.
140
+ - **admin**: the [Admin API](admin.md), OAuth and tokens, webhook delivery, the app proxy,
141
+ lsf's admin (`/admin/…`, App Bridge, Polaris at `/__lsf/cdn/`), managed pricing and the
142
+ Partner API, and their control endpoints (`/__lsf/admin/…`, `/__lsf/webhooks`,
143
+ `/__lsf/polaris`).
144
+
145
+ `--only` (or `LSF_ONLY`) names the parts to run. Without it:
146
+
147
+ | Command | Runs |
148
+ |---|---|
149
+ | `serve` | `theme` |
150
+ | `serve --app <dir>` | `theme`, `checkout`, `admin` |
151
+ | `app dev` | `checkout`, `admin`: `serve --only checkout,admin` |
152
+
153
+ ```bash
154
+ npx local-storefront serve --only theme # the theme alone
155
+ npx local-storefront serve --app ../my-app --only admin # the Admin API alone, for a backend's tests
156
+ npx local-storefront serve --app ../my-app --only checkout # the extensions and the Functions alone
157
+ npx local-storefront serve --app ../my-app --only checkout,admin # what app dev serves
158
+ ```
159
+
160
+ `checkout` and `admin` need `--app`: without it, ``error: `--only admin` needs the app: `--app
161
+ DIR` (or LSF_APP)``. `app dev` serves no theme, so `app dev --only theme` is an error. Without
162
+ the theme, `serve` needs no theme folder, as `app dev`: the store data is still looked for in
163
+ `<theme>/shopify-local`, then in the app's.
164
+
165
+ A part that does not run answers `404` on its routes, with an `X-LSF-Reason` that names the
166
+ `--only` that serves it: ``the admin part is not served (serving: theme): `serve --app <dir>
167
+ --only theme,admin` serves it`` ([control API](control-api.md)). With the theme and without the
168
+ checkout, `/checkout` and `/account` are the theme's stand-ins, as without `--app`. Without the
169
+ theme, the pages of the theme answer that `404`. `/` is then the checkout's home page, and its
170
+ `404` names the checkout part when the checkout does not run either. `GET /__lsf/status`, `GET /__lsf/admin` and the page of `/__lsf` say which parts run.
171
+
172
+ The checkout without the admin part reads the owners of its Functions from the store data, and
173
+ records no order in an Admin API. The theme with the admin part shows what the backend writes,
174
+ with or without the checkout.
175
+
176
+ ### When the data changes on disk
177
+
178
+ Without `--static`, the server looks at the data folder before it answers (every 300 ms at most)
179
+ and reads it again when a file changed. What it does then depends on the files, and the
180
+ terminal says which changed and what was done:
181
+
182
+ | What changed | What the server does | The line |
183
+ |---|---|---|
184
+ | The store's own data: a data file at the root (`store.json`), a file of `products/`, `collections/`, `pages/`, `blogs/`, `customers/`, `menus/`, or of `files/` | Reads everything again and seeds the shops of the Admin API again from it: what the backend changed there (a product, a subscription, a customization) is dropped. | `store data reloaded (products/tee.json changed): the shops of the Admin API were seeded again, and what the Admin API changed in local-supply-co.myshopify.com was dropped` |
185
+ | Only checkout scenarios (`checkouts/*.json`) | Reads the scenarios again on top of the same store data. The shops of the Admin API keep their state: the app's subscription, its customizations, its other writes. An open checkout follows its scenario. | `checkout scenarios reloaded (checkouts/b2b.json added): the store and what the Admin API changed are kept (no re-seed)` |
186
+ | The `--pricing-plans` file, wherever it is (looked at before every request, the control endpoints and lsf's admin included) | Reads the plans again. Plans a `PUT /__lsf/admin/pricing-plans` set stay until its `DELETE`. A file that no longer reads keeps the plans it had. | `pricing plans reloaded (pricing.toml)`, or `pricing plans not reloaded: …` |
187
+ | A file the store does not read (`schema/`, `README.md`, another folder, a name that starts with a dot) | Nothing. | none |
188
+
189
+ A scenario is a checkout input, not store data: adding one while the backend has set the shop
190
+ up must not undo that setup. A change that touches both kinds seeds again. `POST /__lsf/reload`
191
+ reads everything again and seeds the shops again whatever changed, and `DELETE /__lsf/admin`
192
+ puts them back to the store data without reading the files.
193
+
194
+ ## app
195
+
196
+ The commands of a Shopify app, named as Shopify's CLI names them (`shopify app dev`,
197
+ `shopify app build`, `shopify app function run`). In the app's folder, `--app` can be left out,
198
+ and the store data defaults to the app's `shopify-local/`. Guide: [checkout](checkout.md).
199
+
200
+ | Command | What it does |
201
+ |---|---|
202
+ | [`app dev`](#app-dev) | Serves the app alone: its checkout, its customer account and its Admin API. |
203
+ | [`app init`](#app-init) | Writes checkout scenarios made for the app. |
204
+ | [`app build`](#app-build) | Builds its extensions, without Node. |
205
+ | [`app check`](#app-check) | Runs the Functions of every scenario and checks their `expect`, for CI. |
206
+ | [`app function run`](#app-function-run), [`app function build`](#app-function-build), [`app function schema`](#app-function-schema) | Runs or builds its Functions, without a server, or writes the schema of a Function's API. |
207
+ | [`app webhook trigger`](#app-webhook-trigger) | Sends one of its webhooks to its backend, signed as Shopify signs it. |
208
+
209
+ ### app dev
210
+
211
+ `npx local-storefront app dev --app <dir>`
212
+
213
+ Serves a Shopify app alone, without a theme: `serve --app --only checkout,admin`. `/` is the
214
+ [home page](checkout.md#the-home-page), which lists the extensions of the app and the checkout
215
+ scenarios and opens a checkout for each; `/checkout` starts a checkout, where the Functions run
216
+ and the checkout UI extensions show; `/account` is the
217
+ [customer account](checkout.md#customer-account), where the customer account UI extensions
218
+ show. The Admin API, the control API and `/cdn` are there, other pages answer `404`. It takes
219
+ the options of [`serve`](#serve); `--only checkout` or `--only admin` runs one of its two parts
220
+ ([what runs](#what-runs)), and `--only theme` is an error.
221
+
222
+ ```bash
223
+ npx local-storefront app dev --app ../my-app --extension volume-discount --variant shop
224
+ ```
225
+
226
+ ### app init
227
+
228
+ `npx local-storefront app init [--app <dir>]`
229
+
230
+ Writes checkout scenarios, the files of `checkouts/` in the store data, made for the Functions
231
+ and checkout UI extensions of the app (a generic set outside an app, or when nothing matches).
232
+ It gives each Function the settings its input query reads (`[extensions.input.variables]`) when
233
+ the store data has none: those of the app's own `shopify-local/`, or empty values of the type of
234
+ each variable, in `app-settings.json` (to fill in). It gives each UI extension that declares
235
+ settings (`[[extensions.settings.fields]]`) and has none in the store data a value of each
236
+ field's type there too: its first choice, `false`, `0` or `""`. It creates the data folder with the demo store when it does not exist, refreshes `schema/`, never
237
+ overwrites a scenario or a data file without `--force`, and prints what it wrote, what it kept
238
+ and what it skipped and why, then what to run next (`next: lsf app dev`, with `--app` when
239
+ the app is not the one of the current folder). It writes to the same data folder as `init`, `--data` and
240
+ `LSF_DATA` included. Guide: [checkout](checkout.md#scenarios).
241
+
242
+ ```bash
243
+ npx local-storefront app init --app ../my-app
244
+ ```
245
+
246
+ | Option | Environment variable | What it does |
247
+ |---|---|---|
248
+ | `--app <dir>` | `LSF_APP` | The app the scenarios are made for. Defaults to the app the current folder is in. |
249
+ | `--force` | | Overwrites the scenarios that exist. |
250
+
251
+ ### app build
252
+
253
+ `npx local-storefront app build [handle]... --app <dir>`
254
+
255
+ Builds the extensions of an app, as `shopify app build` does, without Node: its Functions (as
256
+ `app function build`) and its UI extensions, of the checkout and of the customer account, that
257
+ name their sources (`module`), bundled into `dist/<handle>.js`. Guide:
258
+ [checkout](checkout.md#what-lsf-builds-without-node).
259
+
260
+ A theme app extension has nothing to build: it is checked as Shopify checks it when the app is
261
+ deployed. Its folders (`assets/`, `blocks/`, `snippets/`, `locales/`, no other), its limits (10
262
+ MB of files, 30 blocks, 100 KB of Liquid, 100 locale files of 15 KB at most; a warning past 100
263
+ KB of CSS or 10 KB of JavaScript, gzipped, named by a schema), the schema of each block
264
+ (`name`, `target`, settings with a `type` and a unique `id`, the files of `javascript` and
265
+ `stylesheet`, `available_if`, not both `enabled_on` and `disabled_on`), and its Liquid, with
266
+ Shopify's strict parser. Each one says `checked: <handle>: ...`; an error exits with `1`.
267
+ `validate` and `app check` run the same checks.
268
+
269
+ | Option | Environment variable | What it does |
270
+ |---|---|---|
271
+ | `[handle]...` | | The extensions to build or check. Every extension of the app when none is given. |
272
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in. |
273
+
274
+ ### app check
275
+
276
+ `npx local-storefront app check [--app <dir>]`
277
+
278
+ Runs the Functions of every checkout scenario, as the home page checks them, without a server:
279
+ for CI. Prints each scenario that does not validate, each Function that fails, with the
280
+ scenarios it fails in, and each expectation of a scenario's [`expect`](checkout.md#expected-results)
281
+ that its checkout does not meet. Exits with `1` when there is one, when the time ran out, or
282
+ when there is no scenario at all (`app init` writes a set). No request of a `.fetch` target is
283
+ sent.
284
+ It builds nothing without `--build`: in a fresh clone, where `dist/` is not committed, give
285
+ `--build` (or run `npx local-storefront app build` first).
286
+
287
+ It also fails, with why, on:
288
+
289
+ - an extension the checkout does not offer: an `api_version` lsf does not run (it runs
290
+ `2026-10` and `2027-01`), a Function with no module, a checkout UI extension with no bundle,
291
+ and, for one `--extension`, a scenario's `extensions` or a discount names, targets the local
292
+ checkout does not have. A UI extension with no checkout or customer account target (an admin
293
+ one) is left out unless named;
294
+ - a Function whose files changed after its module was built: its module runs the old code. Give
295
+ `--build`, or build it (`app build`). lsf records what each build it makes was built from;
296
+ a module another tool built is stale when a file is newer than it by more than 2 seconds;
297
+ - what Shopify rejects in a theme app extension, as `app build` checks it. `--extension` naming
298
+ other extensions only leaves them out.
299
+
300
+ ```text
301
+ fails ui-preact: api_version "2026-07" is not supported (supported: 2026-10, 2027-01): the extension is not offered to the checkout page
302
+ stale discount: extensions/discount/src/run.ts changed after its module was built, which runs the old code: run with --build (or `lsf app build discount`)
303
+ invalid badges: extensions/badges/blocks/stars.liquid: a block needs a {% schema %}
304
+ checked 11 of 11 scenarios in 0.1 s: 0 failures, 0 invalid scenarios, 1 extension not offered, 1 stale module, 1 error in theme app extensions
305
+ ```
306
+
307
+ A trap ends with the last message the Function logged, such as the one of a `throw`:
308
+
309
+ ```text
310
+ fails my-discount · cart.lines.discounts.generate.run: the Function trapped: Error: boom (in b2b)
311
+ ```
312
+
313
+ A Function that ran in no scenario, such as a discount Function no scenario's discount names, is
314
+ a warning: nothing checked it, and the exit code stays the same. The last line, the summary,
315
+ counts it too, rather than saying that every Function ran:
316
+
317
+ ```text
318
+ warning volume-discount: ran in no scenario, so nothing checks it (a discount Function runs only for a discount that names it: …)
319
+ checked 8 of 8 scenarios in 0.4 s: no Function failed, 1 Function ran in no scenario
320
+ ```
321
+
322
+ A payment or delivery customization, validation or cart transform Function gets
323
+ ``(it runs where the shop has an enabled owner of it, as on Shopify, or where a scenario names
324
+ it: add it to the `extensions` of one)`` instead.
325
+
326
+ An expectation that is not met prints the scenario and the assertion (the list and the rule,
327
+ or the amount), what the scenario expects, and what the checkout has, as the scenario writes it.
328
+ The Functions run for it as the order is placed, unless the `expect` says
329
+ `"step": "interaction"`, which the first line then names:
330
+
331
+ ```text
332
+ expect express-buttons: express_methods.excludes
333
+ expected: no entry matching "paypal"
334
+ actual: [{"handle": "paypal", "name": "PayPal"}, {"handle": "shop_pay", "name": "Shop Pay"}]
335
+ expect express-buttons: totals.total
336
+ expected: "96.00"
337
+ actual: "86.40"
338
+ ```
339
+
340
+ When every Function ran somewhere and none failed, the summary ends with `every Function ran`,
341
+ then `12 expectations met` when scenarios have an `expect`. When something failed, it counts the
342
+ failures, the invalid scenarios and the expectations not met:
343
+ `1 failure, 0 invalid scenarios, 2 of 12 expectations unmet`.
344
+
345
+ Exiting with `0` means no Function failed where it ran, and every `expect` holds. A scenario
346
+ without `expect` checks only that its Functions run. CI: [checkout](checkout.md#in-ci).
347
+
348
+ `--extension` keeps some extensions of the app. An `expect` describes the checkout of the
349
+ Functions its scenario names (every Function of the app when it names none): when
350
+ `--extension` leaves one of them out, that checkout is another one, so the `expect` is not
351
+ checked. The scenario still runs, for the Functions that fail in it, and the summary counts
352
+ it:
353
+
354
+ ```text
355
+ checked 8 of 8 scenarios in 0.1 s: every Function ran, the `expect` of 2 scenarios not checked (--extension leaves out a Function they run)
356
+ ```
357
+
358
+ | Option | Environment variable | What it does |
359
+ |---|---|---|
360
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in. |
361
+ | `--extension <handle>` | | Only these extensions. Repeat it for several. |
362
+ | `--build` | | Builds the extensions first, as `app build` does. |
363
+ | `--format text` | | Default. One line per problem, then a summary. |
364
+ | `--format json` | | `{ok, state, checked, total, elapsedMs, failures: [{handle, target, error, logs, scenarios}], invalid: [{scenario, file, problems}], expectations: {checked, unchecked: [scenario], unmet: [{scenario, file, step, assertion, expected, actual}]}, ranInNoScenario: [handle], notOffered: [{handle, reason}], stale: [{handle, file}], themeExtensions: [{handle, severity, message}]}`, and `error` when there is no scenario. `state` is `done`, or `partial` when the time ran out. A Function target that fails in several ways has a failure for each error, with the scenarios of that error; `logs` is what the Function logged in the first of them. `expectations.checked` counts the expectations of the scenarios' `expect`, met or not; `unchecked` names the scenarios whose `expect` was not checked, as `--extension` leaves out a Function they run; `assertion` is the list and the rule (`express_methods.excludes`) or the amount (`totals.total`), `expected` what the scenario says (the entry, the list, the amount), `actual` what the checkout has. `ranInNoScenario` is filled only when every scenario ran. `notOffered` has the extensions the checkout does not offer, `stale` the Functions whose `file` changed after their module was built, `themeExtensions` what Shopify rejects (`error`) or advises against (`warning`) in the theme app extensions. |
365
+ | `--budget <seconds>` | | How long the Functions may run in all. Default 300. |
366
+
367
+ ### app function run
368
+
369
+ `npx local-storefront app function run [handle]... --app <dir>`
370
+
371
+ Runs the Functions of an app once, without a server. Builds the input of each from the store
372
+ data, runs them in checkout order, and prints each input, output and log with the checkout that
373
+ results. Guide: [checkout](checkout.md#run-a-function-without-a-server).
374
+
375
+ A Function whose files changed after its module was built is built first, as `shopify app
376
+ function run` builds before it runs: `build: <handle>: <file> changed after its module was
377
+ built: building it first`. One lsf cannot build (no build command) runs its old module, with a
378
+ warning. Before the buyer gives a shipping address, no delivery rate is selected, as on Shopify:
379
+ the shipping of the totals is `0.00`, and a line says so.
380
+
381
+ ```bash
382
+ npx local-storefront app function run volume-discount --app ../my-app --session '{"cart":{"items":[{"variant":"TEE-BLK-M","quantity":3}]}}'
383
+ ```
384
+
385
+ | Option | Environment variable | What it does |
386
+ |---|---|---|
387
+ | `[handle]...` | | The Functions to run, whether or not the shop has an owner of them (a payment customization, …): one the checkout of the store data would leave out is said first (`Named: …`, `forced` in JSON). It reads the store data on disk, not a running lsf. Without them, what the checkout runs: the Functions with an enabled owner, those of a discount, or those the scenario names; the others are listed as not run, with why. |
388
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in; the store data to its `shopify-local/` when the current folder has none. |
389
+ | `--build` | | Builds the extensions first, as `app build` does: the Functions, then the UI extensions that name their sources. |
390
+ | `--scenario <name>` | | A [checkout scenario](data-format.md#checkout-scenarios) of the store data (`checkouts/<name>.json`): its cart, buyer, language, `checkout` settings and the extensions it names. `--session`, `--checkout` and `--locale` go on top of it. |
391
+ | `--session <file or JSON>` | | The session to run for, as `PUT /__lsf/session` takes it: its `data` goes on top of the store data for this run (a discount for a discount Function, settings in `checkout.functions`). Defaults to the `session` of the store data. |
392
+ | `--checkout <file or JSON>` | | What the buyer entered: `email`, `phone`, `shippingAddress`, `selectedDelivery`, `selectedPayment`, `discountCodes`, `metafields`. |
393
+ | `--locale <iso>` | | The language of the buyer, one of the store's (`fr`, `pt-BR`): Functions read it as `localization.language`. Defaults to the primary language. |
394
+ | `--step <step>` | | `interaction` or `completion` (the default): the step of the buyer journey, what validation Functions read as `buyerJourney.step`. |
395
+ | `--format text` | | Default. Each run, then the checkout. |
396
+ | `--format json` | | The checkout as the checkout page reads it, with every run in `runs`. With `--input`, the run: `{handle, target, export, input, output, logs, error, instructions}`, and `mismatches: [{path, expected, actual}]` when a fixture expects another output. With `--input <folder>`: `{ok, handle, passed, failed, fixtures: [{file, ok, run, error}]}`. |
397
+ | `--input <file>`, `-i` | | Runs one Function on this input, as `shopify app function run --input` does, instead of one built from the store data: a JSON file, `-` for stdin, or a folder, whose `*.json` fixtures each run, with a line each and a summary (`ran 4 fixtures of discount: 3 passed, 1 failed`). The Function is the one named, else the one whose folder holds the input or the current folder, else the app's only Function. A fixture of the unit tests of Shopify's templates (`{"payload": {"export", "target", "input", "output"}}`) runs its target: its `target` and `export` must be the Function's (and `--export`'s), and its `output` the run's; where it is not, each path that differs is printed, with what the fixture expects and what the run answered. The output is checked against the schema of the Function's API and the instruction limit, as in a checkout. |
398
+ | `--export <name>`, `-e` | | The target to run, by its Wasm export (`cart-lines-discounts-generate-run`), as Shopify's `--export`, or by its name (`cart.lines.discounts.generate.run`): for `--input` and `--save-input`, when the Function has several targets. |
399
+ | `--save-input <file>` | | Writes the input lsf built for the run of the Function named (and of `--export`) to this file, for `--input` or for a unit test's fixture. Its folders are created. |
400
+
401
+ Exits with `1` when a Function fails to run, when a fixture of `--input` is not met or names
402
+ a target or an export the Function does not have, or when a Function named on the command line did not
403
+ run: it says why (no discount names a discount Function, the cart is empty, the `extensions` of
404
+ the scenario leave it out, an error of the app…). A handle or a scenario that is close to an existing one is suggested. Errors a validation
405
+ Function reports are part of the checkout and do not change the exit code.
406
+
407
+ ### app function build
408
+
409
+ `npx local-storefront app function build [handle]... --app <dir>`
410
+
411
+ Builds the Functions of an app, as `shopify app build` does. A Function written in JavaScript or
412
+ TypeScript without a build command is built by lsf, without Node: its types
413
+ (`generated/api.ts`), `dist/function.js`, then its Wasm module. The others run their
414
+ `[extensions.build] command`. Guide: [checkout](checkout.md#javascript-and-typescript-functions).
415
+
416
+ | Option | Environment variable | What it does |
417
+ |---|---|---|
418
+ | `[handle]...` | | The Functions to build. Every Function of the app when none is given. |
419
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in. |
420
+
421
+ ### app function typegen
422
+
423
+ `npx local-storefront app function typegen [handle]... --app <dir>`
424
+
425
+ Writes the GraphQL types of a JavaScript or TypeScript Function, as `shopify app function
426
+ typegen` does (the `typegen` script of Shopify's templates): the `codegen` section of its
427
+ `package.json`, written as graphql-codegen writes it (`generated/api.ts`), from its
428
+ `schema.graphql` or lsf's schema of its API version, offline. `app function build` writes them
429
+ too. A Rust Function's types come from the `typegen` macro at its build: it writes nothing.
430
+
431
+ | Option | Environment variable | What it does |
432
+ |---|---|---|
433
+ | `[handle]...` | | The Functions. Without them, the one the current folder is in, else every JavaScript Function. |
434
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in. |
435
+
436
+ ### app function schema
437
+
438
+ `npx local-storefront app function schema [handle] [--path <dir>] [--stdout]`
439
+
440
+ Writes the GraphQL schema of a Function's API, at the `api_version` of its
441
+ `shopify.extension.toml`, to its `schema.graphql`, as `shopify app function schema` does: the
442
+ schema a Rust Function's `#[typegen("schema.graphql")]` and a JavaScript Function's
443
+ graphql-codegen read. It comes from the schemas lsf carries (the checkout's Function APIs:
444
+ cart transform, discounts, delivery and payment customizations, validations, at 2026-10 and
445
+ 2027-01), without the network or a Partner account. Guide:
446
+ [checkout](checkout.md#the-schema-of-a-function).
447
+
448
+ | Option | Environment variable | What it does |
449
+ |---|---|---|
450
+ | `[handle]` | | The Function. Without it, the Function whose folder `--path` names, or the one the current folder is in. |
451
+ | `--path <dir>` | | The folder of the Function, as Shopify's `--path`. |
452
+ | `--app <dir>` | `LSF_APP` | The app. Defaults to the app `--path` or the current folder is in. |
453
+ | `--stdout` | | Prints the schema instead of writing `schema.graphql`. |
454
+
455
+ ### app webhook trigger
456
+
457
+ `npx local-storefront app webhook trigger --topic <topic> [--app <dir> --app-url <url> | --address <url>]`
458
+
459
+ Sends one webhook, as `shopify app webhook trigger` does, without Shopify: lsf builds the
460
+ payload of the topic (the REST representation Shopify sends) from a record of the store data, as
461
+ the [Admin API](admin.md) seeds it (`--data`, else the `shopify-local/` of the theme or of the
462
+ app, else the demo store): the first product for `products/update`, the first order for
463
+ `orders/create`, the shop for `app/uninstalled`. A topic whose type the data has no record of
464
+ (a refund, a draft order) is built from lsf's sample, modelled on Shopify's example payload. lsf
465
+ signs it with the client secret (`X-Shopify-Hmac-Sha256`), and posts it with Shopify's headers,
466
+ `X-Shopify-Shop-Domain` the shop of the data. It prints the request it sent and the answer of the
467
+ backend. To send the webhook of a given record, use `POST /__lsf/webhooks/trigger` with its
468
+ `id` while a server runs. In the demo store, the first order, `#1001`, is fulfilled already: to
469
+ test a fulfillment handler, [place an order](admin.md#place-an-order), or trigger
470
+ `{"topic": "orders/create", "id": "gid://shopify/Order/7888001690128"}` (`#1002`).
471
+
472
+ ```bash
473
+ npx local-storefront app webhook trigger --topic products/update --app ../my-app --app-url http://localhost:3000
474
+ npx local-storefront app webhook trigger --topic orders/create --address http://localhost:3000/webhooks --client-secret "$SHOPIFY_API_SECRET"
475
+ ```
476
+
477
+ Without `--address`, it goes to every subscription of `shopify.app.toml` to the topic, as an
478
+ event of the shop would: each subscription's `uri` joined to the app URL, its `include_fields`
479
+ applied. Relative `uri`s join `--app-url` (else `application_url`), and an absolute one on the
480
+ origin of `application_url` moves to `--app-url`, since `application_url` is often the tunnel of
481
+ a `shopify app dev` that is not running. As Shopify's, a triggered webhook is not retried.
482
+
483
+ Webhooks stay on this machine. One goes to another host only when that host is the scheme,
484
+ host and port of `--app-url`, or the `--address` given: otherwise the command sends nothing,
485
+ prints `error: lsf sends webhooks to this machine or to --app-url only: <url> is neither; give
486
+ --app-url http://localhost:<port> (your backend) or --address <url>` and exits with `2`. A
487
+ server logs the same delivery `not_sent`, and says to start lsf with `--app-url`. So
488
+ `application_url`, often your production backend or a dead tunnel, never gets one by accident.
489
+
490
+ The secret is the app's. Without the app (no `--app`, and run outside its folder), its `.env`
491
+ is not read: give `--client-secret` or set `SHOPIFY_API_SECRET`. Without either, lsf signs with
492
+ its own secret, and the backend answers `401`. A `401` or `403` says which secret lsf signed
493
+ with: `failed: answered 401 (1 attempt): lsf signed it with lsf-local-client-secret, its own:
494
+ give lsf the backend's secret (--client-secret, or --app for the SHOPIFY_API_SECRET of its
495
+ .env)`.
496
+
497
+ | Option | Environment variable | What it does |
498
+ |---|---|---|
499
+ | `--topic <topic>` | | The topic, as Shopify writes it (`orders/create`) or as the GraphQL enum does (`ORDERS_CREATE`). Every topic of the API version (`--api-version`) and the compliance topics (`customers/data_request`, `customers/redact`, `shop/redact`). A topic close to an existing one is suggested. |
500
+ | `--address <url>` | | Where to send it: `https://…`, or `http://localhost:<port>/<path>` (any address of this machine: `localhost`, `*.localhost`, `127.0.0.0/8`, `::1`). |
501
+ | `--api-version <version>` | | The version the payload is built in: `2026-10` (the default) or another lsf knows (`2027-01`), for `--address` and the subscriptions of `shopify.app.toml` alike, as Shopify's CLI sends the sample of that version. A subscription whose `api_version` differs gets it all the same, and a `note:` says so. A topic the version does not have is refused. |
502
+ | `--delivery-method <method>` | | `http`, `google-pub-sub` or `event-bridge`; without it, what the address says. lsf delivers over HTTP only: the other two are refused. |
503
+ | `--client-secret <secret>` | `LSF_CLIENT_SECRET` | The secret of the signature. Default: `SHOPIFY_API_SECRET` of the environment, else of the app's `.env`, else lsf's local secret, `lsf-local-client-secret`. |
504
+ | `--app <dir>`, `--path <dir>` | `LSF_APP` | The app. Defaults to the app the current folder is in; may be left out with `--address`. |
505
+ | `--app-url <url>` | `LSF_APP_URL` | Where the backend of the app listens. Without it, `application_url`, when it is on this machine. |
506
+ | `--format text` | | Default. The request it sent, the answer, the state. |
507
+ | `--format json` | | Prints the deliveries as JSON: `{ok, deliveries: [...]}`, each as [`GET /__lsf/webhooks`](control-api.md#webhook-deliveries) lists it (`topic`, `url`, `request`, `attempts`, `state`, `reason`, …). `--json` and `-j`, from before `--format`, still work. |
508
+
509
+ lsf builds the payloads of the topics of products, collections, customers, orders, draft
510
+ orders, fulfillments, refunds, inventory items and levels, locations, metaobjects, discounts,
511
+ the shop, the app (`app/uninstalled`, `app/scopes_update`, `app_subscriptions/update`), bulk
512
+ operations, and the compliance topics. Another topic is refused, rather than sent with a body
513
+ made up for it.
514
+
515
+ Exits with `1` when a delivery failed: an answer out of `2xx` (a redirect included, as on
516
+ Shopify), no answer within 5 seconds, a backend that is not listening. Exits with `2` when
517
+ nothing could be sent: an unknown topic, a topic lsf does not build, no address and no
518
+ subscription to the topic, a Pub/Sub or EventBridge address, an address on another machine
519
+ that `--app-url` or `--address` does not name.
520
+
521
+ ## render
522
+
523
+ `npx local-storefront render <path>`
524
+
525
+ Prints the HTML of one page, without a server.
526
+
527
+ ```bash
528
+ npx local-storefront render '/collections/all?sort_by=price-ascending' --strict > collection.html
529
+ ```
530
+
531
+ | Option | What it does |
532
+ |---|---|
533
+ | `<path>` | The path, with its query string. Default `/`. |
534
+ | `--section-id <id>` | Renders one section, as the Section Rendering API does. |
535
+ | `--host <host>` | The host of the URLs in the page. Default `localhost:9292`. |
536
+ | `--strict` | Exits with `1` when the page has Liquid errors, or when nothing is at the path (the theme's 404 page). |
537
+ | `--customer <email>` | Renders with this customer logged in: an email of the data, `default` for the first customer, or `none`. Also `LSF_CUSTOMER`. |
538
+ | `--cart <variant>[=<quantity>]` | Puts a variant in the cart: its SKU, its id, or a product handle for its first variant. Repeatable: `--cart TEE-BLK-M=2 --cart ceramic-mug`. |
539
+ | `--session <json>` | The session as `PUT /__lsf/session` takes it (`customer`, `cart`, `country`, `company_location`), as JSON or the path of a JSON file. `--customer` and `--cart` apply on top. A name the data does not have exits with `2`. |
540
+ | `--format github` | Prints each Liquid error as a [GitHub Actions annotation](#github-annotations), on stderr, with its file and line. Default `text`. |
541
+
542
+ `render` answers a path as `serve` does. A `section_id` or `sections` in the query renders those
543
+ sections, as the Section Rendering API does. `/account` (lsf's page of the customer account),
544
+ `/recommendations/products` and `/search/suggest` are answered by the server, as `serve`
545
+ answers them.
546
+
547
+ The HTML goes to stdout. On stderr: each Liquid error and each warning, then a summary.
548
+
549
+ ```text
550
+ Liquid error (layout/theme line 34): divided by 0
551
+ status 200 · template product · 1 Liquid error · 0 warnings
552
+ ```
553
+
554
+ With `--strict`, a path with nothing at it adds `error: nothing at /x: the page is the theme's
555
+ 404`.
556
+
557
+ ## profile
558
+
559
+ `npx local-storefront profile <path>`
560
+
561
+ Prints what the render of one page spends its time on: sections, blocks, snippets. See
562
+ [performance](performance.md#profile-a-page).
563
+
564
+ | Option | What it does |
565
+ |---|---|
566
+ | `<path>` | The path, with its query string. Default `/`. |
567
+ | `--points` | Counts in points, what the page would cost Shopify, instead of time. |
568
+ | `--cost <kind>=<points>,...` | Changes what a kind of thing costs in points. |
569
+ | `--lines` | Adds every tag and every output, by file and line. |
570
+ | `--all` | Shows every row. Without it, the smallest rows are summed up. |
571
+ | `--runs <n>` | Renders the page `n` times and keeps the one in the middle. Default `15`. |
572
+ | `--section-id <id>` | Profiles one section alone. |
573
+ | `--json` | Prints the profile in speedscope's format, for a flame graph. |
574
+
575
+ ## validate
576
+
577
+ `npx local-storefront validate`
578
+
579
+ Checks the store data and explains every problem, its checkout scenarios included.
580
+
581
+ The data is the one `app dev` reads: `--data`, or the folder's `shopify-local/`, or the
582
+ app's (`<app>/shopify-local`), or the built-in demo store. In a folder that is not a theme (an
583
+ app, such as [the checkout playground](https://github.com/0xtlt/local-storefront/tree/main/examples/checkout-playground)), it checks the data
584
+ alone, with the store's own languages; an app without any is checked with the demo store, as its
585
+ checkout runs. A folder that is neither a theme nor an app, and has no store data, is an error.
586
+
587
+ In a Shopify app (or with `--app`), it checks the app too: what its Functions and UI extensions
588
+ get wrong (no build, an API version lsf does not run, an unknown target, an `owner_type`), and
589
+ its theme app extensions as `app build` checks them (`app: badges: extensions/badges/blocks/stars.liquid:
590
+ a block needs a {% schema %}`), and exits with `1` on an error of it.
591
+
592
+ | Option | What it does |
593
+ |---|---|
594
+ | `--app <dir>` | The app to check too (`LSF_APP`). Defaults to the app the current folder is in. |
595
+ | `--format text` | Default. One block per problem, for a person. |
596
+ | `--format json` | One JSON document, for a tool: `errors`, `warnings` and `diagnostics` are those of the data and of the app together (an app's has the code `app` and its extension as `file`), `app` has the app's own when there is one. |
597
+ | `--format github` | One [GitHub Actions annotation](#github-annotations) per problem, on stdout: the file of the store data, and the line of the value. |
598
+
599
+ The text format:
600
+
601
+ ```text
602
+ error[unknown_field]: unknown field "titel"
603
+ --> products/shirt.json at /titel
604
+ hint: did you mean "title"?
605
+ ```
606
+
607
+ The JSON format:
608
+
609
+ ```json
610
+ {
611
+ "ok": false,
612
+ "errors": 1,
613
+ "warnings": 0,
614
+ "data": "./shopify-local",
615
+ "diagnostics": [
616
+ {
617
+ "severity": "error",
618
+ "code": "unknown_field",
619
+ "file": "products/shirt.json",
620
+ "path": "/titel",
621
+ "message": "unknown field \"titel\"",
622
+ "hint": "did you mean \"title\"?"
623
+ }
624
+ ],
625
+ "app": null
626
+ }
627
+ ```
628
+
629
+ `data` is the folder read, and `file` is in it. `path` is a JSON Pointer into `file`. `app` is
630
+ `null` outside a Shopify app. The codes are listed in the
631
+ [data guide](data-format.md#error-codes). Exits with `1` when there is an error; warnings
632
+ alone exit with `0`.
633
+
634
+ ## check
635
+
636
+ `npx local-storefront check`
637
+
638
+ Parses every Liquid file of the theme and reports what would not render on Shopify, or what
639
+ Shopify refuses on upload:
640
+
641
+ - syntax errors, unknown filters, and what Shopify's strict parser rejects;
642
+ - the `{% schema %}` of a section or a theme block that Shopify refuses: JSON that does not
643
+ parse, a setting type Shopify does not have, a setting without its `id` or `label`, two
644
+ settings with one id, a `select` or `radio` whose `default` is not one of its `options`, a
645
+ `range` whose `default` is outside `min` and `max` or not one of its steps, or that has more
646
+ than 101 steps (`invalid_schema`, `unknown_setting_type`, `duplicate_setting_id`);
647
+ - what the theme names but does not have: a snippet of `render` or `include`
648
+ (`missing_snippet`), a section of `{% section %}` or of a JSON template (`missing_section`), a
649
+ group of `{% sections %}` (`missing_section_group`), a theme block of a schema, of a JSON
650
+ template, of a section group or of `{% content_for 'block', type: … %}` (`missing_block`), a
651
+ key of the `t` filter that the default locale does not have (`missing_translation`), a `t:` key
652
+ of a schema that the default schema locale does not have (`missing_schema_translation`);
653
+ - as a warning, a file of `assets/` that `asset_url` names and the theme does not have
654
+ (`missing_asset`).
655
+
656
+ Only names written as literals are checked. It reads `layout/`, `templates/`, `sections/`,
657
+ `blocks/`, `snippets/`, and the `liquid` settings of the JSON templates, section groups and
658
+ `config/settings_data.json`. Guide: [strict Liquid](strict-liquid.md).
659
+
660
+ | Option | What it does |
661
+ |---|---|
662
+ | `--fix` | Rewrites what the strict parser rejects. A file is written only if it has not changed since it was read. Run twice, the second run writes nothing. |
663
+ | `--app <dir>` | Checks the app blocks and app embed blocks the theme names against the [theme app extensions](storefront.md#theme-app-extensions) of this app. A block that renders nothing is a problem, and the check exits with `1`: a block the app does not have (with the name it probably meant), an app embed block in a section, a block its `enabled_on` or `disabled_on` keeps out of the template or section group. A block of another app is a note. Without it, a note says how many the theme names, not checked. |
664
+ | `--format text` | Default. One block per problem, for a person. |
665
+ | `--format json` | One JSON document, for a tool. |
666
+ | `--format github` | One [GitHub Actions annotation](#github-annotations) per problem, on stdout. |
667
+
668
+ ### The text format
669
+
670
+ Each problem is a line `<file>:<line>: <message>`, followed by indented details. A warning,
671
+ which is not counted as a problem, has `warning: ` before its message, and a note `note: `
672
+ (`"severity": "info"` in JSON).
673
+
674
+ | Detail | Meaning |
675
+ |---|---|
676
+ | `was:` | The markup as written. Long markup shows only the words that change. |
677
+ | `fix:` | The rewrite `--fix` writes. After `--fix`, it is `now:`. |
678
+ | `before it:` | Tags the fix adds on the lines above, separated by `;`. |
679
+ | `its body goes in:` | The `if` the fix puts the body of a `when` in. |
680
+ | `changes the output:` | What renders differently with the fix. Absent when the output stays the same. |
681
+ | `left out:` | Arguments the fix drops, which the snippet does not read. |
682
+ | `no automatic fix:` | Why there is no fix. |
683
+
684
+ For a `liquid` setting, the location is `<file>:<line>: <path of the setting>`, and
685
+ `, line <n>` when the problem is not on the first line of the value. The summary goes to
686
+ stderr.
687
+
688
+ ### The JSON format
689
+
690
+ ```json
691
+ {
692
+ "ok": false,
693
+ "files": 191,
694
+ "problems": 2,
695
+ "fixable": 2,
696
+ "fixed": 0,
697
+ "changes_output": 1,
698
+ "diagnostics": [
699
+ {
700
+ "severity": "error",
701
+ "code": "strict_liquid_rejected",
702
+ "file": "snippets/card.liquid",
703
+ "line": 1,
704
+ "message": "Shopify's strict Liquid parser rejects this {% render %}: Expected end_of_string but found pipe",
705
+ "subject": "{% render %}",
706
+ "markup": "'image', alt: image.alt | default: shop.name",
707
+ "fix": {
708
+ "written": false,
709
+ "markup": "'image', alt: image_alt",
710
+ "before": ["assign image_alt = image.alt | default: shop.name"],
711
+ "changes_output": "`alt` gets `image.alt | default: shop.name`"
712
+ }
713
+ },
714
+ {
715
+ "severity": "error",
716
+ "code": "strict_liquid_rejected",
717
+ "file": "templates/index.json",
718
+ "line": 3,
719
+ "setting": "sections.custom.settings.custom_liquid",
720
+ "setting_line": 2,
721
+ "message": "Shopify's strict Liquid parser rejects this {{ }}: Expected end_of_string but found id",
722
+ "subject": "{{ }}",
723
+ "markup": "product.title product.vendor",
724
+ "fix": { "written": false, "markup": "product.title" }
725
+ }
726
+ ]
727
+ }
728
+ ```
729
+
730
+ | Key | Meaning |
731
+ |---|---|
732
+ | `ok` | `true` when no problem is left. |
733
+ | `files` | How many files were read. |
734
+ | `problems` | Problems left: not fixed, or without a fix. |
735
+ | `fixable` | Problems left that `--fix` would fix. |
736
+ | `fixed` | Problems `--fix` fixed in this run. |
737
+ | `changes_output` | Fixes, written or not, that change what renders. |
738
+
739
+ Each diagnostic has a `severity` (`error`, or `warning` for what does not count as a
740
+ problem), a `code`, a `file`, a `line` when there is one, and a `message`.
741
+
742
+ Without `--fix`, the report is a dry run: each `fix` is what `--fix` writes. After `--fix`, the
743
+ problems it fixed stay in `diagnostics`, with `fix.written: true` and still `error`; `problems`
744
+ counts the others.
745
+
746
+ | `code` | What it is |
747
+ |---|---|
748
+ | `strict_liquid_rejected` | Markup Shopify's strict parser rejects. |
749
+ | `strict_liquid_reinterpreted` | Markup Shopify's strict parser reads differently from today. |
750
+ | `liquid_syntax` | A file that does not parse at all. |
751
+ | `unknown_filter` | A filter local-storefront does not know. Either a typo: Shopify skips an unknown filter, and the value comes out unchanged. A filter of Shopify one or two letters away, with the same letters, or a name of another template language (`uppercase`) is in the `hint`. Or a filter of Shopify that local-storefront lacks: [report it](https://github.com/0xtlt/local-storefront/issues). |
752
+ | `block_outside_template` | `{% block %}` outside `layout/` and `templates/` (with `--preview liquid-july-26`). |
753
+ | `invalid_json` | A JSON file that does not parse. |
754
+ | `invalid_schema` | A `{% schema %}` Shopify refuses: JSON that does not parse, a setting without a required key (`id`, `label`, `options` of a `select`, `min`, `max` and `default` of a `range`), a `default` its setting does not take. A section or a block whose schema does not parse loses the defaults of its settings. |
755
+ | `unknown_setting_type` | A setting of a schema whose `type` Shopify does not have. The `hint` gives the closest one. |
756
+ | `duplicate_setting_id` | Two settings of one schema with the same `id`. |
757
+ | `missing_block` | A theme block the theme names and `blocks/` does not have: in the `blocks` or the `presets` of a schema, in a JSON template or a section group, or in `{% content_for 'block', type: '…' %}`. A section with blocks of its own (with a `name`) names its own types. |
758
+ | `missing_schema_translation` | A `t:` key of a schema that the default schema locale (`locales/en.default.schema.json`) does not have: the theme editor shows the key. |
759
+ | `missing_asset` | A warning: `'x.js' \| asset_url` names a file `assets/` does not have (nor `assets/x.js.liquid`). The URL leads to nothing. |
760
+ | `app_block` | With `--app`, an app block that renders nothing (an error), or a block of another app (a note). Without `--app`, a note. |
761
+ | `fix_fallback` | A warning: what the code meant could not be written for this file, so only the fixes that keep the output were. |
762
+ | `classic_customer_accounts` | A warning: the theme has `templates/customers`, the pages of classic customer accounts, which lsf never renders (`/account` stands for Shopify's new customer accounts). The theme is fine on Shopify. |
763
+
764
+ The diagnostics of the strict parser have more keys:
765
+
766
+ | Key | Meaning |
767
+ |---|---|
768
+ | `setting`, `setting_line` | For Liquid in a JSON file: the setting, as its keys joined with dots, and the line in its value. `line` is then where the value starts in the file. |
769
+ | `subject` | `{% <tag> %}`, or `{{ }}` for an output. |
770
+ | `markup` | The markup as written. |
771
+ | `fix.written` | Whether `--fix` wrote the fix. |
772
+ | `fix.markup` | What replaces the markup, or the whole tag when the fix rewrites the tags around it (a `case` that becomes `if`). |
773
+ | `fix.before` | Tags the fix puts on the lines above, without their delimiters. |
774
+ | `fix.body_in_if` | The condition of the `if` the body of a `when` goes in. |
775
+ | `fix.left_out` | `{ variables, snippet }`: arguments the fix drops, which the snippet does not read. |
776
+ | `fix.changes_output` | What renders differently with the fix. Absent when the output stays the same. |
777
+ | `no_fix` | Why there is no fix, when there is none. |
778
+ | `hint` | For an `unknown_filter`, the filter of Shopify it is probably a typo of: `did you mean "upcase"?`. Absent when none is close. |
779
+
780
+ Exits with `1` when a problem is left, whatever the format.
781
+
782
+ ### GitHub annotations
783
+
784
+ `--format github` prints each problem as a workflow command of GitHub Actions, which shows it on
785
+ its line in the pull request:
786
+
787
+ ```text
788
+ ::error file=sections/hero.liquid,line=40,title=lsf check%3A unknown_setting_type::setting "align" is of type "selct", which Shopify does not have: Shopify refuses the file%0Ahint: did you mean "select"?
789
+ ```
790
+
791
+ The file is relative to the working directory, where the workflow runs. `validate` and
792
+ `render` take `--format github` too: `validate` names the file of the store data and the line
793
+ of the value, `render` the file and the line of each Liquid error.
794
+
795
+ ## fmt
796
+
797
+ `npx local-storefront fmt [paths]`
798
+
799
+ Formats Liquid files as Prettier does with Shopify's Liquid plugin. Guide:
800
+ [formatting](formatting.md).
801
+
802
+ | Option | What it does |
803
+ |---|---|
804
+ | `[paths]` | Files and folders to format. Default: the theme folder. |
805
+ | `--check` | Writes nothing. Lists the files that are not formatted on stdout, the summary on stderr, and exits with `1` if there are any. |
806
+ | `--stdin` | Formats the standard input and prints it. For an editor. |
807
+ | `--stdin-filepath <path>` | With `--stdin`: the file the source comes from, for its configuration and `.prettierignore`. |
808
+ | `--option <name>=<value>`, `-o` | Sets a Prettier option: `-o printWidth=100`. Wins over the configuration files. |
809
+ | `--no-config` | Does not read `.prettierrc`, `.editorconfig` nor `.prettierignore`. |
810
+
811
+ A file that cannot be read, written or formatted is reported, the others are formatted, and
812
+ the command exits with `2`.
813
+
814
+ ## init
815
+
816
+ `npx local-storefront init`
817
+
818
+ Creates the data folder with a demo store, JSON Schemas for editors, and the data guide as
819
+ `README.md`. For the checkout of a Shopify app, [`app init`](#app-init) writes the
820
+ scenarios.
821
+
822
+ It keeps the files that exist, so it is safe to run again, and refreshes `schema/`. It prints
823
+ on stderr, and exits with `0`:
824
+
825
+ ```text
826
+ ./shopify-local: wrote 16 files, kept 0 existing ones, refreshed schema/
827
+ next: the `serve` command shows the theme, and `validate` checks the data after you edit it
828
+ for a Shopify app: `app init --app <dir>` writes checkout scenarios to try its Functions and extensions with
829
+ ```
830
+
831
+ | Option | What it does |
832
+ |---|---|
833
+ | `--force` | Overwrites files that exist, your edits included. |
834
+
835
+ ## schema
836
+
837
+ `npx local-storefront schema [kind]`
838
+
839
+ Prints the JSON Schema of the store data.
840
+
841
+ | Option | What it does |
842
+ |---|---|
843
+ | `[kind]` | `store` (default, a root data file), `product`, `collection`, `page`, `blog`, `customer`, `menu`, `session` or `checkout-scenario` (a file of `checkouts/`). |
844
+ | `--out <dir>` | Writes every schema into the folder as `<kind>.schema.json`. |
845
+
846
+ ## routes
847
+
848
+ `npx local-storefront routes`
849
+
850
+ Lists the URLs the store data gives a page to, one per line. Each one renders: a loop of
851
+ `render --strict` over them checks every page. `/account`, a page of lsf's rather than of the
852
+ theme, is not listed; `GET /__lsf/status` lists the same `routes`.
853
+
854
+ | Option | What it does |
855
+ |---|---|
856
+ | `--all` | Adds the states a page has beyond its URL: a search that finds the first product (`/search?q=…`), the sections the theme's JavaScript fetches when the theme shows them (product recommendations of the product page, `/recommendations/products?…&section_id=…`; predictive search, `/search/suggest?…&section_id=predictive-search`), and then every route again in each other language of the store (`/fr/…`). |
857
+
858
+ ```bash
859
+ npx local-storefront routes --all > routes.txt
860
+ while read -r route; do
861
+ npx local-storefront render "$route" --strict > /dev/null || echo "failed: $route"
862
+ done < routes.txt
863
+ ```
864
+
865
+ ## docs
866
+
867
+ `npx local-storefront docs`
868
+
869
+ Prints the data guide, with the reference of every field.
870
+
871
+ | Option | What it does |
872
+ |---|---|
873
+ | `--reference` | Prints only the field reference. |