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