local-storefront 0.2.0 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ # Quickstart: a public app (React Router template)
2
+
3
+ Shopify's [React Router app template](https://github.com/Shopify/shopify-app-template-react-router)
4
+ run end to end on your machine: the embedded app in a local admin, its webhooks and Events,
5
+ billing, a theme app extension in a theme, the app proxy, and a Playwright test. No development
6
+ store, no tunnel, no Partner account.
7
+
8
+ ## 1. Set up the template
9
+
10
+ ```bash
11
+ git clone https://github.com/Shopify/shopify-app-template-react-router my-app # or `shopify app init`
12
+ cd my-app
13
+ npm install
14
+ npm run setup # the Prisma session storage
15
+ npm install --save-dev local-storefront
16
+ ```
17
+
18
+ lsf and the backend both read the app's `.env`. `shopify app env pull` writes it; by hand:
19
+
20
+ ```ini
21
+ SHOPIFY_API_KEY=0123456789abcdef0123456789abcdef
22
+ SHOPIFY_API_SECRET=local-secret
23
+ SCOPES=write_products
24
+ ```
25
+
26
+ `shopify app config link` fills `client_id`, `name` and `handle` in `shopify.app.toml`. Without
27
+ it, add `name = "My app"` and `handle = "my-app"` there: the admin's URL and the plan page use
28
+ the handle.
29
+
30
+ ## 2. Make the one edit
31
+
32
+ The library sends the Admin API and the token exchange to `https://<shop>`. Send them to lsf, in
33
+ the body of `app/shopify.server.ts`, after the imports:
34
+
35
+ ```ts
36
+ import { setAbstractFetchFunc } from "@shopify/shopify-api/runtime";
37
+
38
+ // Local development against lsf: the Admin API and OAuth of every shop go to it.
39
+ const lsf = process.env.LSF_URL; // http://127.0.0.1:9292
40
+ if (lsf) {
41
+ setAbstractFetchFunc((input, init) => {
42
+ const url = new URL(input instanceof Request ? input.url : String(input));
43
+ if (!url.hostname.endsWith(".myshopify.com")) return fetch(input, init);
44
+ const headers = new Headers(init?.headers);
45
+ headers.set("x-forwarded-host", url.host); // the shop, for OAuth
46
+ return fetch(`${lsf}${url.pathname}${url.search}`, { ...init, headers });
47
+ });
48
+ }
49
+ ```
50
+
51
+ Without `LSF_URL`, the app talks to Shopify as before. Until the edit is there, lsf's admin
52
+ says so; it reads the file again at each load, so the banner goes once the edit is saved. A
53
+ backend started without `LSF_URL` sends its calls to Shopify: when a page of it fails and lsf has
54
+ received nothing from it since the admin opened the app, lsf's admin and its terminal name
55
+ `LSF_URL`. Nothing else changes: lsf's admin gives the pages its App Bridge and Polaris.
56
+
57
+ ## 3. Run lsf and the backend
58
+
59
+ ```bash
60
+ npx local-storefront app dev --app . --app-url http://localhost:3000
61
+ LSF_URL=http://127.0.0.1:9292 SHOPIFY_APP_URL=http://localhost:3000 PORT=3000 npx react-router dev
62
+ ```
63
+
64
+ Not `npm run dev`: that is `shopify app dev`, which talks to Shopify. Open the `embedded:` URL
65
+ lsf prints, <http://127.0.0.1:9292/admin/apps/my-app>. The app opens in a local admin, with its
66
+ navigation and title bar. Click **Generate a product**: the admin shows "Product created".
67
+ **Edit product** (`shopify.intents.invoke("edit:shopify/Product")`) shows what lsf has of the
68
+ product; lsf has no product editor, so closing it answers `{code: "closed"}`.
69
+
70
+ ## 4. Webhooks and Events
71
+
72
+ The template's `[events]` subscription runs on the price change of **Generate a product**: the
73
+ backend prints `Received PRODUCT Event for local-supply-co.myshopify.com`. The webhooks of
74
+ `shopify.app.toml` go to `--app-url`, signed with `SHOPIFY_API_SECRET`:
75
+
76
+ ```bash
77
+ curl -X POST http://127.0.0.1:9292/__lsf/admin/uninstall # app/uninstalled: the template deletes its sessions
78
+ curl -X POST http://127.0.0.1:9292/__lsf/admin/install
79
+ curl http://127.0.0.1:9292/__lsf/webhooks # every delivery, and what the backend answered
80
+ ```
81
+
82
+ The uninstall revokes every token of the app. The install brings back the token lsf printed at
83
+ start, so that a script or a test that read it keeps working. The tokens of the backend's token
84
+ exchange stay revoked: the next page of the app exchanges a session token again, as on Shopify.
85
+
86
+ ## 5. Billing
87
+
88
+ With managed pricing, declare the plans of the Partner Dashboard in a file:
89
+
90
+ ```toml
91
+ # pricing.toml
92
+ [[plans]]
93
+ handle = "free"
94
+ name = "Free"
95
+ price = 0
96
+
97
+ [[plans]]
98
+ handle = "pro"
99
+ name = "Pro"
100
+ price = 9
101
+ trial_days = 7
102
+ welcome_link = "/app"
103
+ ```
104
+
105
+ Start lsf with `--pricing-plans pricing.toml`, and send the merchant to the plan page as on
106
+ Shopify:
107
+
108
+ ```ts
109
+ // app/routes/app.plans.tsx
110
+ import type { LoaderFunctionArgs } from "react-router";
111
+ import { authenticate } from "../shopify.server";
112
+
113
+ export const loader = async ({ request }: LoaderFunctionArgs) => {
114
+ const { redirect } = await authenticate.admin(request);
115
+ return redirect("shopify://admin/charges/my-app/pricing_plans", { target: "_top" });
116
+ };
117
+ ```
118
+
119
+ `/admin/apps/my-app/app/plans` lands on lsf's plan page. **Choose Pro**, then **Approve**: the
120
+ shop gets an active `Pro` subscription, and the browser goes to `/app` with `charge_id` and
121
+ `plan_handle`. A test skips the page: `POST /__lsf/admin/pricing-plans/pro/select`. The Billing
122
+ API (`billing.request`): [approve a charge](admin.md#approve-a-charge).
123
+
124
+ ## 6. A theme app extension and the app proxy
125
+
126
+ An app block is a file of a theme app extension: `extensions/rating/shopify.extension.toml` with
127
+ `name = "Rating"` and `type = "theme"`, and `extensions/rating/blocks/rating.liquid`:
128
+
129
+ ```liquid
130
+ <p class="my-app-rating">{{ block.settings.heading }} ★★★★☆</p>
131
+
132
+ {% schema %}
133
+ {
134
+ "name": "Rating",
135
+ "target": "section",
136
+ "settings": [{ "type": "text", "id": "heading", "label": "Heading", "default": "Rated" }]
137
+ }
138
+ {% endschema %}
139
+ ```
140
+
141
+ A theme names it in a JSON template, as the theme editor writes it. In Horizon's
142
+ `templates/product.json`, add this section (`_blocks` is Horizon's section that takes app
143
+ blocks) and put `"rating"` in `order`:
144
+
145
+ ```json
146
+ "rating": {
147
+ "type": "_blocks",
148
+ "blocks": {
149
+ "rating_1": { "type": "shopify://apps/my-app/blocks/rating/0", "settings": { "heading": "Loved by customers" } }
150
+ },
151
+ "block_order": ["rating_1"]
152
+ }
153
+ ```
154
+
155
+ For the app proxy, add to `shopify.app.toml`, and a route that answers Liquid:
156
+
157
+ ```toml
158
+ [app_proxy]
159
+ url = "/proxy"
160
+ prefix = "apps"
161
+ subpath = "my-app"
162
+ ```
163
+
164
+ ```ts
165
+ // app/routes/proxy.$.tsx
166
+ import type { LoaderFunctionArgs } from "react-router";
167
+ import { authenticate } from "../shopify.server";
168
+
169
+ export const loader = async ({ request }: LoaderFunctionArgs) => {
170
+ const { liquid } = await authenticate.public.appProxy(request);
171
+ return liquid("<h1>Hello from my app</h1><p>{{ shop.name }}</p>");
172
+ };
173
+ ```
174
+
175
+ Serve the theme with the app, in place of `app dev`:
176
+
177
+ ```bash
178
+ npx local-storefront serve --theme ../horizon --app . --app-url http://localhost:3000
179
+ ```
180
+
181
+ A product page shows the block, and <http://127.0.0.1:9292/apps/my-app> shows the proxy's answer
182
+ in the theme's layout. In the app's folder, `npx local-storefront check --theme ../horizon --app .`
183
+ checks the app blocks the theme names, and `npx local-storefront app build` checks the extension
184
+ as Shopify does.
185
+
186
+ ## 7. A Playwright test
187
+
188
+ ```bash
189
+ npm install --save-dev @playwright/test && npx playwright install chromium
190
+ ```
191
+
192
+ ```ts
193
+ // playwright.config.ts
194
+ import { defineConfig } from '@playwright/test';
195
+
196
+ // The app's values (SHOPIFY_API_KEY, SHOPIFY_API_SECRET, SCOPES): react-router-serve reads no .env.
197
+ process.loadEnvFile('.env');
198
+
199
+ export default defineConfig({
200
+ testDir: 'e2e',
201
+ workers: 1, // one lsf, one shop: the tests change it in turn
202
+ use: {
203
+ // For the test's own requests to the backend: the template answers 410 to HeadlessChrome.
204
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
205
+ },
206
+ webServer: [
207
+ {
208
+ command: 'npx local-storefront app dev --app . --port 9393 --app-url http://localhost:3001 --polaris offline --static --quiet',
209
+ url: 'http://127.0.0.1:9393/__lsf/status',
210
+ },
211
+ {
212
+ // A build, not the dev server: on a cold start, Vite's dev server optimizes the
213
+ // dependencies a page asks for and reloads the page, under the test.
214
+ command: 'npx prisma migrate deploy && npx react-router build && npx react-router-serve ./build/server/index.js',
215
+ url: 'http://localhost:3001/',
216
+ env: { PORT: '3001', SHOPIFY_APP_URL: 'http://localhost:3001', LSF_URL: 'http://127.0.0.1:9393' },
217
+ timeout: 120_000,
218
+ },
219
+ ],
220
+ });
221
+ ```
222
+
223
+ ```ts
224
+ // e2e/app.spec.ts
225
+ import { test, expect } from '@playwright/test';
226
+
227
+ const LSF = 'http://127.0.0.1:9393';
228
+
229
+ test('the app generates a product', async ({ page, request }) => {
230
+ const errors: string[] = [];
231
+ page.on('console', (message) => message.type() === 'error' && errors.push(message.text()));
232
+ const { last } = await (await request.get(`${LSF}/__lsf/webhooks`)).json();
233
+
234
+ await page.goto(`${LSF}/admin/apps/my-app/app`);
235
+ await expect(page.frameLocator('iframe').getByText('Get started with products')).toBeVisible();
236
+ await page.getByRole('button', { name: 'Generate a product' }).click(); // the admin's title bar
237
+ await expect(page.getByText('Product created')).toBeVisible(); // the admin's toast
238
+
239
+ // The price change sends the template's Product Event to webhooks.products.update.tsx.
240
+ const events = await request.get(`${LSF}/__lsf/webhooks?topic=Product&since=${last}&wait=1`);
241
+ const { deliveries } = await events.json();
242
+ expect(deliveries[0].state).toBe('delivered');
243
+ expect(errors).toEqual([]); // nothing failed in the page
244
+ });
245
+ ```
246
+
247
+ `npx playwright test` builds the app and runs it with `react-router-serve`. Not the dev server:
248
+ on a cold checkout, as in CI, Vite optimizes the dependencies the first page asks for, answers
249
+ `504 Outdated Optimize Dep` and reloads the page, and the test clicks a page that never
250
+ hydrates. The build takes a few seconds, has no HMR port, and runs next to your dev server.
251
+ `--polaris offline` serves Polaris from lsf: no request leaves the machine.
252
+
253
+ In CI, write the `.env` first (it is not committed): the values of step 1 are lsf's, not
254
+ secrets.
255
+
256
+ ```yaml
257
+ - run: npm ci && npm run setup
258
+ - run: printf 'SHOPIFY_API_KEY=0123456789abcdef0123456789abcdef\nSHOPIFY_API_SECRET=local-secret\nSCOPES=write_products\n' > .env
259
+ - run: npx playwright install --with-deps chromium && npx playwright test
260
+ ```
261
+
262
+ ## Next
263
+
264
+ - [Admin API](admin.md): the embedded admin, App Bridge, tokens, webhooks, billing, managed
265
+ pricing and the Partner API, the app proxy.
266
+ - [Storefront](storefront.md#theme-app-extensions): app embed blocks, app-data metafields, why a
267
+ block renders nothing.
268
+ - [Quickstart: Functions and checkout UI extensions](quickstart-functions.md), for the app's
269
+ extensions.
270
+ - [`examples/admin-backend`](https://github.com/0xtlt/local-storefront/tree/main/examples/admin-backend):
271
+ a backend with Shopify's libraries, run against lsf end to end.
@@ -0,0 +1,207 @@
1
+ # Quickstart: Functions and checkout UI extensions
2
+
3
+ From a Shopify app with a discount Function and a checkout UI extension to a local checkout that
4
+ runs them, and a check that fails in CI when the Function decides wrong. No store, no Partner
5
+ account, no network.
6
+
7
+ The examples use an app with two extensions, as `shopify app generate extension` makes them: a
8
+ discount Function in TypeScript (`discount-typescript`) and a checkout UI extension in Preact
9
+ (`ui-preact`). Without an app, copy these two folders of the
10
+ [playground](https://github.com/0xtlt/local-storefront/tree/main/examples/checkout-playground/extensions)
11
+ into the `extensions/` of a folder that has a `shopify.app.toml`.
12
+
13
+ lsf runs the extensions whose `api_version` is `2026-10` or `2027-01`. On another version, a
14
+ Function does not run and a UI extension is not offered to the checkout: `validate` says so, and
15
+ `app check` fails with the reason (`api_version "2026-07" is not supported (supported: 2026-10,
16
+ 2027-01)`). Change the `api_version` of its `shopify.extension.toml` to one of the two.
17
+
18
+ ## 1. Install and write scenarios
19
+
20
+ In the app's folder:
21
+
22
+ ```bash
23
+ npm install --save-dev local-storefront
24
+ npx local-storefront app init
25
+ ```
26
+
27
+ ```text
28
+ ./shopify-local: created, with the demo store
29
+ wrote checkouts/guest.json
30
+ …
31
+ wrote checkouts/discount-automatic.json
32
+ wrote checkouts/discount-code.json
33
+ wrote checkouts/all-extensions.json
34
+ skipped cart-transform: the app has no cart transform Function
35
+ …
36
+ ./shopify-local/checkouts: wrote 11 scenarios, kept 0 existing ones, for 1 Function and 1 checkout UI extension of My app
37
+ ```
38
+
39
+ A scenario is a cart, a buyer, a country and the extensions to run, in one file of
40
+ `shopify-local/checkouts/`. `app init` writes a set made for the app's extensions.
41
+
42
+ ## 2. Give the discount its settings
43
+
44
+ A discount Function runs only for a discount that names it. `discount-automatic.json` has one;
45
+ give it the metafield the Function reads its settings from. The playground's reads
46
+ `$app:config` / `settings`; yours reads what its input query names:
47
+
48
+ ```json
49
+ "checkout": {
50
+ "discounts": [
51
+ {
52
+ "function": "discount-typescript",
53
+ "title": "Quantity discount (TypeScript)",
54
+ "code": null,
55
+ "metafields": {
56
+ "$app:config.settings": { "type": "json", "value": { "percentage": 10, "minimumQuantity": 3 } }
57
+ }
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ## 3. Build and serve
64
+
65
+ ```bash
66
+ npx local-storefront app build # every Function and UI extension, without Node
67
+ npx local-storefront app dev --watch
68
+ ```
69
+
70
+ Open <http://127.0.0.1:9292/>: the home page lists what is wrong first, then the scenarios.
71
+ Open one to get its checkout, with the Function's discount on the lines and the extension's
72
+ banner. `--watch` builds an extension again when you save it, and reloads the page:
73
+
74
+ ```text
75
+ ↻ building discount-typescript…
76
+ ✓ discount-typescript built in 0.1s
77
+ ```
78
+
79
+ ## 4. Run the Function once
80
+
81
+ ```bash
82
+ npx local-storefront app function run discount-typescript --scenario discount-automatic
83
+ ```
84
+
85
+ It prints the input, the output, the logs and the instructions of the run, then the checkout:
86
+
87
+ ```text
88
+ == checkout
89
+ 6 × Canvas Tote Bag 144.00 USD → 129.60 USD
90
+ 3 × Ceramic Mug (Oat) 66.00 USD → 59.40 USD
91
+ 2 × Linen Overshirt (S) 136.00 USD
92
+ discount 10% off 3 or more: -21.00 USD (Product, discount-typescript)
93
+ …
94
+ subtotal 346.00 USD · discounts -21.00 USD · shipping 0.00 USD · tax 0.00 USD · total 325.00 USD
95
+ ```
96
+
97
+ When a file of the Function changed after its module was built, `app function run` builds it
98
+ first, as `shopify app function run` does (`build: discount-typescript: … changed after its
99
+ module was built: building it first`).
100
+
101
+ Keep the input lsf built, and run the Function on it later, as `shopify app function run
102
+ --input` does:
103
+
104
+ ```bash
105
+ npx local-storefront app function run discount-typescript --scenario discount-automatic \
106
+ --save-input tests/fixtures/six-totes.json
107
+ npx local-storefront app function run discount-typescript --input tests/fixtures/six-totes.json
108
+ ```
109
+
110
+ `--save-input` creates the folders of its file. `--input -` reads the input from stdin, and
111
+ `--format json` prints the run as JSON. A Function with several targets needs `--export <name>`.
112
+ In an app with one Function, the handle can be left out.
113
+
114
+ A fixture of the unit tests of Shopify's templates (`{"payload": {"export", "target", "input",
115
+ "output"}}`) is checked: its `output` must be the run's, and its `target` and `export` the
116
+ Function's. `--input` with a folder runs each of its `*.json` fixtures:
117
+
118
+ ```bash
119
+ npx local-storefront app function run --input tests/fixtures
120
+ ```
121
+
122
+ ```text
123
+ pass tests/fixtures/six-totes.json
124
+ fails tests/fixtures/fifteen.json: the output is not the one the fixture expects:
125
+ operations[0].productDiscountsAdd.candidates[0].value.percentage.value
126
+ expected: 15
127
+ actual: 10
128
+ ran 2 fixtures of discount-typescript: 1 passed, 1 failed
129
+ ```
130
+
131
+ ## 5. Say what the checkout must look like
132
+
133
+ Add `expect` to the scenario:
134
+
135
+ ```json
136
+ "expect": {
137
+ "discounts": { "includes": [{ "title": "10% off 3 or more", "amount": "21.00" }] },
138
+ "lines": { "includes": [{ "sku": "TOTE-NAT", "total": "129.60" }] },
139
+ "totals": { "total": "325.00" }
140
+ }
141
+ ```
142
+
143
+ ```bash
144
+ npx local-storefront app check
145
+ ```
146
+
147
+ ```text
148
+ checked 11 of 11 scenarios in 0.0 s: every Function ran, 3 expectations met
149
+ ```
150
+
151
+ When the Function decides wrong, `app check` says where, and exits with `1`:
152
+
153
+ ```text
154
+ expect discount-automatic: totals.total
155
+ expected: "300.00"
156
+ actual: "325.00"
157
+ checked 11 of 11 scenarios in 0.0 s: 0 failures, 0 invalid scenarios, 1 of 3 expectations unmet
158
+ ```
159
+
160
+ `expect` also checks payment methods, express buttons, delivery options and errors, each with
161
+ `includes`, `excludes` and `equals` ([expected results](checkout.md#expected-results)).
162
+
163
+ ## 6. Run it in CI
164
+
165
+ Commit `package.json`, `package-lock.json` and `shopify-local/`. `dist/` is not committed:
166
+ `--build` builds the extensions first.
167
+
168
+ ```yaml
169
+ # .github/workflows/checkout.yml
170
+ name: checkout
171
+ on: [push, pull_request]
172
+ jobs:
173
+ checkout:
174
+ runs-on: ubuntu-latest
175
+ steps:
176
+ - uses: actions/checkout@v7
177
+ - uses: actions/setup-node@v7
178
+ with:
179
+ node-version: 24
180
+ cache: npm
181
+ - run: npm ci
182
+ - run: npx local-storefront validate # the store data, the scenarios and the app
183
+ - run: npx local-storefront app check --build # every Function of every scenario, and each `expect`
184
+ ```
185
+
186
+ `app check` fails on a Function that traps, runs past Shopify's instruction limit or answers an
187
+ output its schema refuses, and on an `expect` that is not met. It also fails on an extension
188
+ the checkout does not offer (an `api_version` lsf does not run, no module or bundle), on a
189
+ Function whose files changed after its module was built (`stale`: give `--build`), and on what
190
+ Shopify rejects in a theme app extension. It needs no store and no network.
191
+
192
+ ## Coming from Shopify's CLI
193
+
194
+ `lsf app dev --watch` stands for `shopify app dev`, `lsf app build` for `shopify app build`, and
195
+ `lsf app function run --input` for `shopify app function run` (which builds a changed Function
196
+ first too). `app function typegen` and
197
+ `app function schema` work offline. There is no development store, tunnel or deploy. The full
198
+ table, with what lsf does not do: [coming from Shopify's CLI](checkout.md#coming-from-shopifys-cli).
199
+
200
+ ## Next
201
+
202
+ - [Checkout](checkout.md): scenarios, what runs and why, discounts, network access, the
203
+ customer account.
204
+ - [Set up the checkout](checkout.md#set-up-the-checkout): shipping rates, payment methods, and
205
+ the settings of a UI extension.
206
+ - [What to test on a real store](checkout.md#what-to-test-on-a-real-store) before a release.
207
+ - [Quickstart: an Admin API backend](quickstart-admin-api.md), for the app's backend.
@@ -0,0 +1,177 @@
1
+ # Quickstart: a theme
2
+
3
+ From a theme folder to a storefront you edit live and a test that runs in CI. The examples use
4
+ Shopify's Horizon theme; any theme works the same way.
5
+
6
+ ## 1. Serve the theme
7
+
8
+ In the theme folder:
9
+
10
+ ```bash
11
+ npm install --save-dev local-storefront
12
+ npx local-storefront serve --live-reload
13
+ ```
14
+
15
+ ```text
16
+ theme: .
17
+ data: built-in demo store (the `init` command writes an editable copy)
18
+ serving: theme
19
+ ready: http://127.0.0.1:9292/ (status and control API: http://127.0.0.1:9292/__lsf)
20
+ ```
21
+
22
+ Open <http://127.0.0.1:9292>. With `--live-reload`, a page reloads when you save a file of the
23
+ theme or of the store data, and a small panel lists the errors of the page. Leave it off in
24
+ tests.
25
+
26
+ Write `npx local-storefront`, not `npx lsf`: `lsf` is another package on npm.
27
+
28
+ ## 2. Write the store data
29
+
30
+ ```bash
31
+ npx local-storefront init
32
+ ```
33
+
34
+ ```text
35
+ ./shopify-local: wrote 16 files, kept 0 existing ones, refreshed schema/
36
+ ```
37
+
38
+ `shopify-local/` is the store: `store.json`, one file per product in `products/`,
39
+ `customers.json`, `collections.json`, menus, pages and blogs. Edit them, or replace the demo's
40
+ products with yours. Then check them:
41
+
42
+ ```bash
43
+ npx local-storefront validate
44
+ ```
45
+
46
+ ```text
47
+ ./shopify-local: valid (8 products, 7 collections, 3 pages, 1 blog, 4 customers, 2 menus, 0 checkout scenarios)
48
+ ```
49
+
50
+ A mistake has its file, its place and a hint, and the command exits with `1`:
51
+
52
+ ```text
53
+ error[unknown_field]: unknown field "titel"
54
+ --> products/blue-shirt.json at /titel
55
+ hint: did you mean "title"?
56
+ ```
57
+
58
+ ## 3. Check and format the Liquid
59
+
60
+ ```bash
61
+ npx local-storefront check # 383 files checked, 0 problems
62
+ npx local-storefront fmt --check # 28 files to format, 256 already formatted
63
+ ```
64
+
65
+ `check` finds the Liquid Shopify's strict parser rejects (`check --fix` rewrites it), the filters
66
+ that do not exist, the `{% schema %}` Shopify refuses on upload (an unknown setting type, a
67
+ `select` default that is not an option), and the snippets, sections, blocks, assets and
68
+ translation keys the theme names but does not have. `fmt --check` exits with `1` on a theme nobody formatted, as Horizon is: run
69
+ `npx local-storefront fmt` once, in a commit of its own.
70
+
71
+ ## 4. Write a browser test
72
+
73
+ Each test gets its own cart and customer. One request puts the session in the state the test
74
+ needs.
75
+
76
+ ```bash
77
+ npm install --save-dev @playwright/test
78
+ ```
79
+
80
+ ```ts
81
+ // playwright.config.ts
82
+ import { defineConfig } from '@playwright/test';
83
+
84
+ export default defineConfig({
85
+ fullyParallel: true,
86
+ use: { baseURL: 'http://127.0.0.1:9393' },
87
+ webServer: {
88
+ command: 'npx local-storefront serve --port 9393 --static --quiet --strict',
89
+ url: 'http://127.0.0.1:9393/__lsf/status',
90
+ reuseExistingServer: !process.env.CI,
91
+ },
92
+ });
93
+ ```
94
+
95
+ ```ts
96
+ // tests/cart.spec.ts
97
+ import { test, expect } from '@playwright/test';
98
+
99
+ test('a logged-in customer sees their cart', async ({ page }) => {
100
+ // The demo store's customer and SKU: put your own.
101
+ const response = await page.request.put('/__lsf/session', {
102
+ data: {
103
+ customer: 'jane.doe@example.com',
104
+ cart: { items: [{ variant: 'TEE-BLK-M', quantity: 2 }] },
105
+ },
106
+ });
107
+ expect(response.ok()).toBeTruthy(); // 422, with the reason, for a SKU the data does not have
108
+
109
+ await page.goto('/cart');
110
+ const lines = page.getByRole('main').getByRole('table');
111
+ await expect(lines.getByRole('link', { name: 'Organic Cotton T-Shirt', exact: true })).toBeVisible();
112
+ });
113
+ ```
114
+
115
+ ```bash
116
+ npx playwright test
117
+ ```
118
+
119
+ `--static` reads the files once, `--strict` refuses to start on store data with errors, and
120
+ port `9393` keeps the tests away from your dev server on `9292`.
121
+
122
+ ## 5. Run it in CI
123
+
124
+ Commit `package.json`, `package-lock.json` and `shopify-local/`.
125
+
126
+ ```yaml
127
+ # .github/workflows/storefront.yml
128
+ name: storefront
129
+ on: [push, pull_request]
130
+ jobs:
131
+ storefront:
132
+ runs-on: ubuntu-latest
133
+ steps:
134
+ - uses: actions/checkout@v7
135
+ - uses: actions/setup-node@v7
136
+ with:
137
+ node-version: 24
138
+ cache: npm
139
+ - run: npm ci
140
+ - run: npx local-storefront validate --format github
141
+ - run: npx local-storefront check --format github
142
+ # - run: npx local-storefront fmt --check # once the theme is formatted
143
+ - name: Render every page, and fail on a Liquid error
144
+ run: |
145
+ npx local-storefront routes --all > routes.txt
146
+ failed=0
147
+ while read -r route; do
148
+ npx local-storefront render "$route" --strict --format github > /dev/null || failed=1
149
+ done < routes.txt
150
+ exit $failed
151
+ - run: npx playwright install --with-deps chromium
152
+ - run: npx playwright test
153
+ ```
154
+
155
+ `--format github` shows each problem on its line of the pull request. `routes --all` lists
156
+ every page, each again in the other languages of the store, a search, and the sections the
157
+ theme fetches (product recommendations, predictive search). The step stops at once if `routes`
158
+ cannot run (a wrong `--theme`), then renders every route, and fails at the end if one of them
159
+ has a Liquid error or is the theme's 404.
160
+
161
+ ## What `shopify theme dev` does that lsf does not
162
+
163
+ - **The theme editor.** There is none: edit the JSON templates and `config/settings_data.json`
164
+ by hand. `request.design_mode` is always `false`.
165
+ - **A real store.** The products, customers and orders are your JSON files, not a development
166
+ store's. Nothing is uploaded, and there is no `theme push` or `theme pull`.
167
+ - **Shopify's checkout.** Without an app, `/checkout` sums up the cart.
168
+ - **Theme Check's other rules** (performance, accessibility, deprecated tags): use
169
+ `shopify theme check` for those.
170
+
171
+ ## Next
172
+
173
+ - [Data format](data-format.md): products, customers, menus, metafields, markets.
174
+ - [Testing](testing.md): sessions, data for one test, slow networks.
175
+ - [Strict Liquid](strict-liquid.md), [formatting](formatting.md), [performance](performance.md).
176
+ - [Storefront](storefront.md): accounts, the password page, theme app extensions.
177
+ - [CLI reference](cli.md): every option and exit code.