local-storefront 0.2.0 → 0.3.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1384 -0
- package/README.md +14 -9
- package/bin/lsf.js +4 -1
- package/docs/README.md +59 -0
- package/docs/admin-reference.md +1412 -0
- package/docs/admin.md +1751 -0
- package/docs/agents.md +366 -0
- package/docs/architecture.md +303 -0
- package/docs/checkout.md +1612 -0
- package/docs/cli.md +917 -0
- package/docs/compatibility.md +502 -0
- package/docs/control-api.md +251 -0
- package/docs/data-format.md +1161 -0
- package/docs/data-reference.md +1272 -0
- package/docs/formatting.md +119 -0
- package/docs/performance.md +267 -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 +251 -0
- package/docs/strict-liquid.md +178 -0
- package/docs/testing.md +590 -0
- package/llms.txt +39 -0
- package/package.json +18 -9
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Storefront
|
|
2
|
+
|
|
3
|
+
What you see when you open the local storefront in a browser, and the options that change it.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx local-storefront serve --live-reload
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Open <http://127.0.0.1:9292>. If that port is taken, local-storefront uses the next free one and
|
|
10
|
+
prints the address.
|
|
11
|
+
|
|
12
|
+
## Reload as you edit
|
|
13
|
+
|
|
14
|
+
`--live-reload` reloads open pages as soon as you edit the theme or the data. The page listens
|
|
15
|
+
on a WebSocket, and asks the server every 700 ms where a socket cannot be opened. It adds a
|
|
16
|
+
script to the pages: leave it off for tests. Only HTML pages get it: a `{% layout none %}`
|
|
17
|
+
template that writes JSON is left as it is.
|
|
18
|
+
|
|
19
|
+
An edit of `assets/` needs no restart either, with or without `--live-reload`: a stylesheet, a
|
|
20
|
+
script, an image, a `.css.liquid` or `.js.liquid` file, the bundle of the `{% stylesheet %}` and
|
|
21
|
+
`{% javascript %}` tags, and the assets of a [theme app extension](#theme-app-extensions). The
|
|
22
|
+
page links each file at a new `?v=` once it changes, as Shopify does, and the browser fetches
|
|
23
|
+
it there. A file the theme loads without its `?v=` (a `url()` in a stylesheet, an `import` in a
|
|
24
|
+
script) is kept a year by the browser, as from Shopify's CDN: link it with `asset_url`, or
|
|
25
|
+
reload without the cache. `--live-reload` reloads the whole page when an asset changes; it does
|
|
26
|
+
not swap a stylesheet in place.
|
|
27
|
+
|
|
28
|
+
With `--live-reload`, a page with problems also shows a small panel: the errors of the store
|
|
29
|
+
data, the Liquid errors of the page, and why a page is the 404 page. Its button closes it. The
|
|
30
|
+
terminal has the same problems.
|
|
31
|
+
|
|
32
|
+
The panel also has notes, which are not problems. A page that renders one of
|
|
33
|
+
[lsf's default menus](data-format.md#lsfs-default-menus) gets a note: its links are lsf's,
|
|
34
|
+
since the data has no menu with that handle. Without problems, the panel has a grey border
|
|
35
|
+
instead of a red one.
|
|
36
|
+
|
|
37
|
+
## Log in as a customer
|
|
38
|
+
|
|
39
|
+
Open <http://127.0.0.1:9292/account>. The page lists the customers of your data: click one to
|
|
40
|
+
be logged in as them, or "Nobody" to be logged out. Every account link of your theme leads to
|
|
41
|
+
this page, because Shopify hosts customer accounts outside the theme.
|
|
42
|
+
|
|
43
|
+
To start logged in:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx local-storefront serve --customer default
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`default` is the first customer of your data. You can also give an email, or `none`.
|
|
50
|
+
|
|
51
|
+
A link can log a browser in too: `/__lsf/login?customer=jane.doe@example.com&return_to=/cart`.
|
|
52
|
+
|
|
53
|
+
With `--app`, `/account` is the customer account of the app instead: the orders of the customer
|
|
54
|
+
logged in, their profile, and the pages where the app's customer account UI extensions render.
|
|
55
|
+
Logged out, it lists the customers of your data too. See
|
|
56
|
+
[checkout](checkout.md#customer-account).
|
|
57
|
+
|
|
58
|
+
Classic customer accounts are not simulated: the theme's `templates/customers` are never
|
|
59
|
+
rendered, and its login and register forms lead to `/account` too. See the
|
|
60
|
+
[data guide](data-format.md#logging-in).
|
|
61
|
+
|
|
62
|
+
### B2B
|
|
63
|
+
|
|
64
|
+
A customer with a company is a B2B customer: `customer.b2b?` is true, and
|
|
65
|
+
`customer.current_company` and `customer.current_location` are filled. The account page lets
|
|
66
|
+
you change location. Company prices (catalogs) are not simulated. How to write companies:
|
|
67
|
+
[data guide](data-format.md#b2b-customers).
|
|
68
|
+
|
|
69
|
+
## The password page
|
|
70
|
+
|
|
71
|
+
Open <http://127.0.0.1:9292/password>. The password is `password`, or the `password` of the
|
|
72
|
+
`shop` in your data. A wrong password stays on the page with the error. The storefront is
|
|
73
|
+
never locked: nothing redirects to that page.
|
|
74
|
+
|
|
75
|
+
## Images and videos
|
|
76
|
+
|
|
77
|
+
`image_url` and `image_tag` point to your machine, which resizes and crops like Shopify's CDN,
|
|
78
|
+
and sends each browser the lightest format it reads: AVIF, WebP, or the one of the file. An
|
|
79
|
+
image with `preload: true` is preloaded through the `Link` header, as on Shopify.
|
|
80
|
+
|
|
81
|
+
Put the files in `shopify-local/files/`. An image that is not there becomes a placeholder of
|
|
82
|
+
the right size, marked with the header `x-lsf-placeholder: 1`.
|
|
83
|
+
|
|
84
|
+
A video of `files/` (a `video` setting, or a metafield or a metaobject field that points to
|
|
85
|
+
an `.mp4`) is a video object, with its sources and its poster, the `.jpg` of the same name.
|
|
86
|
+
Videos are served in the parts a browser asks for, so they play in Safari and can be sought
|
|
87
|
+
in.
|
|
88
|
+
|
|
89
|
+
## What else is served as on Shopify
|
|
90
|
+
|
|
91
|
+
- **Styles and scripts are minified**, with a source map, as Shopify minifies them: when that
|
|
92
|
+
makes the file lighter, and without merging declarations that cascade apart, such as
|
|
93
|
+
`translate` into `transform` ([the rules](compatibility.md#minification)). `--no-minify`
|
|
94
|
+
serves them as written.
|
|
95
|
+
- **Responses are compressed** with Brotli or gzip, with Shopify's `Cache-Control`, so that an
|
|
96
|
+
audit of your theme sees what it would see on the store. `--no-compression` turns it off.
|
|
97
|
+
- **Pages have Shopify's scripts:** the `Shopify` object, `Shopify.actions`,
|
|
98
|
+
`Shopify.loadFeatures`, the Customer Privacy API and `ShopifyAnalytics.meta`.
|
|
99
|
+
- **The Ajax APIs a theme calls** answer as Shopify's do: cart, Section Rendering, predictive
|
|
100
|
+
search, recommendations, forms. The list: [testing](testing.md#what-the-server-answers).
|
|
101
|
+
- **The app proxy of an app:** with `--app` and an `[app_proxy]` in `shopify.app.toml`,
|
|
102
|
+
`/apps/<subpath>` (or the `prefix` it names) is forwarded to the backend of the app, signed as
|
|
103
|
+
Shopify signs it, and a Liquid answer is rendered in the theme's layout. See the
|
|
104
|
+
[Admin API guide](admin.md#app-proxy).
|
|
105
|
+
|
|
106
|
+
## Countries and markets in the URL
|
|
107
|
+
|
|
108
|
+
A visitor's country comes from the URL as on a Shopify storefront, besides the localization form
|
|
109
|
+
and `PUT /__lsf/session`:
|
|
110
|
+
|
|
111
|
+
- **`?country=FR`** on any page picks a country the store sells to, for the rest of the session.
|
|
112
|
+
When the country's market is served under subfolders of its own, the answer is a `301` to the
|
|
113
|
+
same page there, the query kept: `/products/tee?country=BE` → `/en-be/products/tee?country=BE`,
|
|
114
|
+
in the page's language when the market offers it, else in the market's default.
|
|
115
|
+
- **A market's subfolders**: a market with a `subfolder_suffix` (`"be"`, see
|
|
116
|
+
[localization](data-format.md#localization)) is served under `/<language>-<suffix>/`, one per
|
|
117
|
+
language of the market (`/en-be/`, `/fr-be/`; every language of the store when it lists no
|
|
118
|
+
`languages`). A subfolder in a language the market does not offer is not found, as on Shopify. A page there is in the visitor's country when the market has
|
|
119
|
+
it, else in the market's first country, in the language of the prefix; every link the theme
|
|
120
|
+
makes stays under it (`routes`, `request.locale.root_url`, the `root_url` of the languages,
|
|
121
|
+
`Shopify.routes.root`). A page at `/` is in the visitor's country when `/` serves it, else in
|
|
122
|
+
the shop's: a country picked under a subfolder does not follow the visitor to `/`.
|
|
123
|
+
- **The localization form** (`{% form 'localization' %}`) sends the visitor to the same page on
|
|
124
|
+
the web presence of the country they picked: `country_code=BE` → `/en-be/...`, and back to `/`
|
|
125
|
+
for a country served there, in a language the country's market offers.
|
|
126
|
+
- **The currency follows the country**: prices, the cart (`cart.currency`, `/cart.js`), the money
|
|
127
|
+
filters and the checkout are in the country's currency, at its market's rate or fixed prices
|
|
128
|
+
(see [localization](data-format.md#localization)), and `Shopify.currency` gives the currency
|
|
129
|
+
and the rate (`{"active":"EUR","rate":"0.92"}`).
|
|
130
|
+
|
|
131
|
+
## Theme app extensions
|
|
132
|
+
|
|
133
|
+
With `--app`, the theme renders the app blocks and app embed blocks of the app's theme app
|
|
134
|
+
extensions, as Shopify does once the app is installed. An extension is a folder of the app
|
|
135
|
+
whose `shopify.extension.toml` says `type = "theme"`, with `blocks/`, `snippets/`, `assets/`
|
|
136
|
+
and `locales/`.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx local-storefront serve --app ../my-app --live-reload
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `app:` line counts them: `(0 Functions, 0 UI extensions, 1 theme extension (2 app blocks,
|
|
143
|
+
1 app embed block))`.
|
|
144
|
+
|
|
145
|
+
### App blocks
|
|
146
|
+
|
|
147
|
+
A JSON template or a section group names an app block by its type, as the theme editor writes
|
|
148
|
+
it:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
"reviews": {
|
|
152
|
+
"type": "shopify://apps/product-reviews/blocks/reviews/5b1c7a52-2d0e-4c8a-9a3e-6f1d2b7c9e01",
|
|
153
|
+
"settings": { "heading": "Loved by customers" }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`product-reviews` is the `handle` of `shopify.app.toml`, or its `name` as a handle. `reviews` is
|
|
158
|
+
the file `blocks/reviews.liquid` of one of its extensions. The last part is the `uid` of the
|
|
159
|
+
extension. A type copied from another store has another id: the block is found by its name all
|
|
160
|
+
the same.
|
|
161
|
+
|
|
162
|
+
The block renders where the theme renders blocks:
|
|
163
|
+
|
|
164
|
+
- `{% render block %}`, for a block of `section.blocks` whose `block.type` is `@app`, as in Dawn;
|
|
165
|
+
- `{% content_for 'blocks' %}`, in a section or a theme block that accepts `@app` blocks, as in
|
|
166
|
+
Horizon;
|
|
167
|
+
- a section of type `apps`, which the theme editor adds around an app block: the theme's
|
|
168
|
+
`sections/apps.liquid`, or Shopify's own wrapper when the theme has none.
|
|
169
|
+
|
|
170
|
+
Its output is wrapped as on Shopify, in `<div id="shopify-block-<id>" class="shopify-block
|
|
171
|
+
shopify-app-block">`, with the `class` and the `tag` of its schema. Its Liquid reads:
|
|
172
|
+
|
|
173
|
+
- `block.settings`, with the defaults of its schema; a `t:` default is read from the
|
|
174
|
+
extension's `locales/*.schema.json`;
|
|
175
|
+
- `block.id`, `block.type` (`@app`), `section`, `closest`, and the objects of the page
|
|
176
|
+
(`product` on a product page);
|
|
177
|
+
- `app.metafields`: the app-data metafields of the store data (`app.metafields`, below).
|
|
178
|
+
|
|
179
|
+
In the Liquid of an extension, `{% render 'stars' %}` renders its `snippets/stars.liquid`,
|
|
180
|
+
`asset_url` points to its `assets/` (served at `/cdn/extensions/<uid>/assets/`), and `t` reads
|
|
181
|
+
its `locales/`, never the theme's. The `javascript` and the `stylesheet` of a schema load once
|
|
182
|
+
per page, before `</head>`, when a block that names them renders. `enabled_on` and
|
|
183
|
+
`disabled_on` keep a block out of templates and section groups; `available_if` keeps it out
|
|
184
|
+
until its metafield is `true`.
|
|
185
|
+
|
|
186
|
+
### App embed blocks
|
|
187
|
+
|
|
188
|
+
An app embed block (`"target": "head"`, `"compliance_head"` or `"body"`) renders on every page
|
|
189
|
+
where it is turned on: `compliance_head` then `head` before `</head>`, `body` before `</body>`.
|
|
190
|
+
The theme editor turns one on in `config/settings_data.json`, under `current.blocks`, with
|
|
191
|
+
`"disabled": false`. To turn one on without changing the theme, or to set the app-data
|
|
192
|
+
metafields, write `app` in the store data:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"app": {
|
|
197
|
+
"embeds": {
|
|
198
|
+
"chat-bubble": true,
|
|
199
|
+
"reviews-loader": { "settings": { "api_key": "test-key" } }
|
|
200
|
+
},
|
|
201
|
+
"metafields": { "reviews": { "badge_enabled": true } }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
An embed is named by its file in `blocks/`. `true` turns it on, with the settings of
|
|
207
|
+
`config/settings_data.json` or its defaults; `false` turns it off; `{"settings": {...}}` turns
|
|
208
|
+
it on with these. A test does it for its session alone with `PUT /__lsf/session` and
|
|
209
|
+
`{"data": {"app": ...}}` ([testing](testing.md#data-for-one-test-only)).
|
|
210
|
+
|
|
211
|
+
### When a block renders nothing
|
|
212
|
+
|
|
213
|
+
lsf says why, once in the terminal and in the `X-LSF-Reason` header of the page: the app has no
|
|
214
|
+
block of that name, the block is another app's, no app is served (`--app`), `enabled_on` leaves
|
|
215
|
+
the page out, `available_if` is not `true`. An app embed block the theme turns on says so too
|
|
216
|
+
when no app is served. When several blocks of a page render nothing, `X-LSF-Reason` gives them
|
|
217
|
+
all: `3 reasons: … | … | …`. A Liquid error in a file of an extension is reported
|
|
218
|
+
as the theme's are, with its path: `Liquid syntax error (extensions/reviews/snippets/stars line
|
|
219
|
+
1): Unknown tag 'unknowntag'`.
|
|
220
|
+
|
|
221
|
+
At start, `serve` prints the blocks the theme names that the app does not have
|
|
222
|
+
(`app: warning: ...`, with the name it probably meant), and the blocks no template uses or the
|
|
223
|
+
app embed blocks that are off (`app: note: ...`). The home page of the app
|
|
224
|
+
(`/__lsf/checkouts`) lists its extensions and their blocks. `check --app ../my-app` checks the
|
|
225
|
+
app blocks of the theme, and exits with `1` for one that renders nothing (a block the app does
|
|
226
|
+
not have, one its `enabled_on` keeps out of the template). `app build` checks the extensions as
|
|
227
|
+
Shopify does when it deploys them ([CLI reference](cli.md#app-build)).
|
|
228
|
+
|
|
229
|
+
With `--live-reload`, open pages reload when a file of an extension changes. The Section
|
|
230
|
+
Rendering API (`?sections=`) renders app blocks as the page does. What differs from Shopify is
|
|
231
|
+
in [compatibility](compatibility.md#known-differences).
|
|
232
|
+
|
|
233
|
+
## Feature previews
|
|
234
|
+
|
|
235
|
+
Shopify tries changes on development stores before it ships them. A store turns them on in its
|
|
236
|
+
list of feature previews. local-storefront renders what a live store renders, so a theme that
|
|
237
|
+
uses a preview fails as it would there, and local-storefront says which preview to turn on:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx local-storefront serve --preview liquid-july-26
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`--preview` can be repeated, takes several names separated by commas, and is also read from
|
|
244
|
+
`LSF_PREVIEW`. Every command accepts it.
|
|
245
|
+
|
|
246
|
+
| Preview | Shopify's name | What it adds |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `liquid-july-26` | Liquid July '26 changes | `{% block 'name' %}...{% endblock %}`, to call a theme block from a template or a layout; `{% partial 'name' %}...{% endpartial %}`, and `@shopify/partial-rendering` to refresh it. Shopify's [Skeleton theme](https://github.com/Shopify/skeleton-theme) needs it. |
|
|
249
|
+
|
|
250
|
+
A preview can still change on Shopify's side. [Compatibility](compatibility.md) says what
|
|
251
|
+
local-storefront does where Shopify has not said yet.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Strict Liquid
|
|
2
|
+
|
|
3
|
+
Shopify is moving every store from its lenient Liquid parser to a strict one. A file with
|
|
4
|
+
markup the strict parser rejects can no longer be uploaded:
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
Liquid syntax error (line 12): Expected end_of_string but found pipe
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`npx local-storefront check` finds that markup in your theme, and
|
|
11
|
+
`npx local-storefront check --fix` rewrites it.
|
|
12
|
+
|
|
13
|
+
## Find what the strict parser rejects
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx local-storefront check
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
snippets/card.liquid:1: Shopify's strict Liquid parser rejects this {% render %}: Expected end_of_string but found pipe
|
|
21
|
+
was: … alt: image.alt | default: shop.name
|
|
22
|
+
fix: … alt: image_alt
|
|
23
|
+
before it: assign image_alt = image.alt | default: shop.name
|
|
24
|
+
changes the output: `alt` gets `image.alt | default: shop.name`
|
|
25
|
+
snippets/card.liquid:8: Shopify's strict Liquid parser rejects this {% when %}: Expected end_of_string but found id
|
|
26
|
+
was: 'collection' and collection.handle
|
|
27
|
+
fix: 'collection'
|
|
28
|
+
its body goes in: {% if collection.handle %}
|
|
29
|
+
changes the output: the body renders only when `collection.handle` is true
|
|
30
|
+
2 files checked, 2 problems
|
|
31
|
+
2 of them can be fixed: run again with `--fix`
|
|
32
|
+
2 of the fixes change the output, where the code now does what it says: review the diff
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each problem gives the message Shopify gives, the markup, and the fix. `changes the output`
|
|
36
|
+
marks the fixes that render something else than today. Every line of the report is described
|
|
37
|
+
in the [CLI reference](cli.md#check).
|
|
38
|
+
|
|
39
|
+
`npx local-storefront check` reads every `.liquid` file of `layout/`, `templates/`, `sections/`,
|
|
40
|
+
`blocks/` and `snippets/`, and the `liquid` settings stored in JSON: a "Custom Liquid" section
|
|
41
|
+
of a template, a section group, `config/settings_data.json`.
|
|
42
|
+
|
|
43
|
+
The report lists unknown filters too:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
snippets/title.liquid:5: unknown filter `uppercas` (Shopify ignores unknown filters; this one is not implemented here)
|
|
47
|
+
hint: did you mean "upcase"?
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Either it is a typo: Shopify skips a filter it does not know, and prints the value unchanged, so
|
|
51
|
+
the page looks almost right. Fix the name: the `hint` gives it when a filter of Shopify is one or
|
|
52
|
+
two letters away, has the same letters, or does what a filter of another template language
|
|
53
|
+
does (`uppercase`). Or it is a filter of Shopify that local-storefront lacks:
|
|
54
|
+
[report it](https://github.com/0xtlt/local-storefront/issues).
|
|
55
|
+
|
|
56
|
+
## Fix it
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx local-storefront check --fix
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then review the diff before you push it:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git diff
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A fix does one of two things.
|
|
69
|
+
|
|
70
|
+
### What the code meant, where that is clear
|
|
71
|
+
|
|
72
|
+
The lenient parser silently ignores what it does not understand. A theme can carry code for
|
|
73
|
+
years that never did what it says. Where the intent is obvious, the fix makes the code do it:
|
|
74
|
+
|
|
75
|
+
```liquid
|
|
76
|
+
{% render 'image', alt: image.alt | default: shop.name %}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
becomes
|
|
80
|
+
|
|
81
|
+
```liquid
|
|
82
|
+
{% assign image_alt = image.alt | default: shop.name -%}
|
|
83
|
+
{% render 'image', alt: image_alt %}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The lenient parser never applied that filter. Now an image without alt text gets the shop's
|
|
87
|
+
name. The `-%}` removes the line break the fix adds, so nothing else changes in the HTML.
|
|
88
|
+
|
|
89
|
+
| Written | Fixed | What changes |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `{% render 'card', on_sale: a and b %}` | A `{% liquid %}` block above the tag computes `true` or `false`, then `on_sale: card_on_sale` | `on_sale` is false when `b` is |
|
|
92
|
+
| `{% assign label = ('X' \| downcase) %}` | `{% assign label = 'X' \| downcase %}` | `label` is `x`. The lenient parser read the variable `X`. |
|
|
93
|
+
| `{% for i in items limit: 2 reversed %}` | `{% for i in items reversed limit: 2 %}` | The loop runs in reverse |
|
|
94
|
+
| `{% if a = b %}` | `{% if a == b %}` | The condition is evaluated. It rendered an error. |
|
|
95
|
+
| `{% when 'x' and y %}…` | `{% when 'x' %}{% if y %}…{% endif %}` | The body renders only when `y` is true |
|
|
96
|
+
|
|
97
|
+
A `when` with `and`, in a `case` that has an `{% else %}`, meant the `else` to run when the
|
|
98
|
+
condition is false. An `if` inside the `when` would not do that, so the `case` becomes
|
|
99
|
+
`if`/`elsif`/`else`:
|
|
100
|
+
|
|
101
|
+
```liquid
|
|
102
|
+
{%- case template.name -%}
|
|
103
|
+
{%- when 'product' -%}P
|
|
104
|
+
{%- when 'collection' and collection.handle -%}C
|
|
105
|
+
{%- else -%}Z
|
|
106
|
+
{%- endcase -%}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
becomes
|
|
110
|
+
|
|
111
|
+
```liquid
|
|
112
|
+
{%- if template.name == 'product' -%}P
|
|
113
|
+
{%- elsif template.name == 'collection' and collection.handle -%}C
|
|
114
|
+
{%- else -%}Z
|
|
115
|
+
{%- endif -%}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This happens only when the `when`s compare with distinct literals, since a `case` renders
|
|
119
|
+
every `when` that matches and `if` only the first.
|
|
120
|
+
|
|
121
|
+
### Today's output, everywhere else
|
|
122
|
+
|
|
123
|
+
Where the intent is not clear, the fix keeps exactly what renders today:
|
|
124
|
+
|
|
125
|
+
| Written | Fixed |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `{{ product.title product.vendor }}` | `{{ product.title }}` |
|
|
128
|
+
| `{{ 'hi' \| append, '!' }}` | `{{ 'hi' \| append: '!' }}` |
|
|
129
|
+
| `{{ [key].title }}` | `{{ self[key].title }}` |
|
|
130
|
+
|
|
131
|
+
## What a fix guarantees
|
|
132
|
+
|
|
133
|
+
- **The strict parser accepts it.** Each rewritten file is parsed again before it is written.
|
|
134
|
+
- **A fix that keeps the output is read the same by both parsers** as the original is read
|
|
135
|
+
today. These fixes were rendered by Shopify's reference implementation on 1,548 templates:
|
|
136
|
+
the HTML is identical.
|
|
137
|
+
- **Nothing else in the file changes.** Arguments written one per line stay one per line, and
|
|
138
|
+
the tags around keep their `{%-` and `-%}`. In JSON, only the value of the setting changes,
|
|
139
|
+
with the escaping of the file (`<`, `\/`).
|
|
140
|
+
- **A variable a fix adds gets a name the file does not use.** `image_alt` becomes
|
|
141
|
+
`image_alt_2` if the file already has an `image_alt`.
|
|
142
|
+
- **A file is written only if it has not changed** since `npx local-storefront check` read it.
|
|
143
|
+
|
|
144
|
+
## Fix by hand
|
|
145
|
+
|
|
146
|
+
Some markup has no fix, and the report says why after `no automatic fix:`. Fix it by hand:
|
|
147
|
+
|
|
148
|
+
| The report says | Write instead |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `` `a.b` is not a name the strict parser reads `` | For `{% assign a.b = 1 %}`, a plain name: `{% assign a_b = 1 %}`, and read `a_b`. A theme cannot write into `settings` or into an object. |
|
|
151
|
+
| `` `=<` is read as an operator, which renders an error today `` | The condition never worked: write the operator it meant (`<=`), or remove it. |
|
|
152
|
+
| Another reason | Rewrite the markup so that `check` accepts it, then compare the page before and after: `npx local-storefront render <path>`, into a file each time, and `diff`. |
|
|
153
|
+
|
|
154
|
+
## In CI
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- run: npx local-storefront check --format github
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`npx local-storefront check` exits with `1` while a problem is left, so the build fails before
|
|
161
|
+
Shopify refuses the upload. `--format github` shows each problem on its line of the pull
|
|
162
|
+
request. It reads the files and renders none: an error at render time (a
|
|
163
|
+
division by zero) needs `render --strict`. A complete workflow:
|
|
164
|
+
[agents](agents.md#run-the-checks-in-ci); with browser tests: [testing](testing.md#in-ci).
|
|
165
|
+
|
|
166
|
+
## How it is checked
|
|
167
|
+
|
|
168
|
+
The rules come from Shopify's reference implementation, the `liquid` gem 5.14, in its
|
|
169
|
+
`strict2` mode. They are checked against Shopify's test suite of the language: 6,314 specs,
|
|
170
|
+
every rejection with the gem's message. Details:
|
|
171
|
+
[compatibility](compatibility.md#shopifys-strict-liquid-parser).
|
|
172
|
+
|
|
173
|
+
## From Rust
|
|
174
|
+
|
|
175
|
+
The audit is in the `lsf-liquid` crate, in `lsf_liquid::strict2`: `audit` returns the findings
|
|
176
|
+
of a template, `apply` writes the fixes that keep the output, and `apply_intents` writes what
|
|
177
|
+
`npx local-storefront check --fix` writes. A tag of your own says how the strict parser reads it
|
|
178
|
+
with `Environment::register_syntax`.
|