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,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`.