local-storefront 0.2.0 → 0.3.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1299 -0
- package/README.md +14 -9
- package/bin/lsf.js +4 -1
- package/docs/README.md +59 -0
- package/docs/admin-reference.md +1406 -0
- package/docs/admin.md +1751 -0
- package/docs/agents.md +366 -0
- package/docs/architecture.md +303 -0
- package/docs/checkout.md +1609 -0
- package/docs/cli.md +873 -0
- package/docs/compatibility.md +475 -0
- package/docs/control-api.md +251 -0
- package/docs/data-format.md +943 -0
- package/docs/data-reference.md +1247 -0
- package/docs/formatting.md +119 -0
- package/docs/performance.md +266 -0
- package/docs/quickstart-admin-api.md +211 -0
- package/docs/quickstart-app.md +271 -0
- package/docs/quickstart-functions.md +207 -0
- package/docs/quickstart-theme.md +177 -0
- package/docs/storefront.md +210 -0
- package/docs/strict-liquid.md +178 -0
- package/docs/testing.md +590 -0
- package/llms.txt +39 -0
- package/package.json +11 -8
|
@@ -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.
|