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.
@@ -0,0 +1,210 @@
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
+ With `--live-reload`, a page with problems also shows a small panel: the errors of the store
20
+ data, the Liquid errors of the page, and why a page is the 404 page. Its button closes it. The
21
+ terminal has the same problems.
22
+
23
+ ## Log in as a customer
24
+
25
+ Open <http://127.0.0.1:9292/account>. The page lists the customers of your data: click one to
26
+ be logged in as them, or "Nobody" to be logged out. Every account link of your theme leads to
27
+ this page, because Shopify hosts customer accounts outside the theme.
28
+
29
+ To start logged in:
30
+
31
+ ```bash
32
+ npx local-storefront serve --customer default
33
+ ```
34
+
35
+ `default` is the first customer of your data. You can also give an email, or `none`.
36
+
37
+ A link can log a browser in too: `/__lsf/login?customer=jane.doe@example.com&return_to=/cart`.
38
+
39
+ With `--app`, `/account` is the customer account of the app instead: the orders of the customer
40
+ logged in, their profile, and the pages where the app's customer account UI extensions render.
41
+ Logged out, it lists the customers of your data too. See
42
+ [checkout](checkout.md#customer-account).
43
+
44
+ Classic customer accounts are not simulated: the theme's `templates/customers` are never
45
+ rendered, and its login and register forms lead to `/account` too. See the
46
+ [data guide](data-format.md#logging-in).
47
+
48
+ ### B2B
49
+
50
+ A customer with a company is a B2B customer: `customer.b2b?` is true, and
51
+ `customer.current_company` and `customer.current_location` are filled. The account page lets
52
+ you change location. Company prices (catalogs) are not simulated. How to write companies:
53
+ [data guide](data-format.md#b2b-customers).
54
+
55
+ ## The password page
56
+
57
+ Open <http://127.0.0.1:9292/password>. The password is `password`, or the `password` of the
58
+ `shop` in your data. A wrong password stays on the page with the error. The storefront is
59
+ never locked: nothing redirects to that page.
60
+
61
+ ## Images and videos
62
+
63
+ `image_url` and `image_tag` point to your machine, which resizes and crops like Shopify's CDN,
64
+ and sends each browser the lightest format it reads: AVIF, WebP, or the one of the file. An
65
+ image with `preload: true` is preloaded through the `Link` header, as on Shopify.
66
+
67
+ Put the files in `shopify-local/files/`. An image that is not there becomes a placeholder of
68
+ the right size, marked with the header `x-lsf-placeholder: 1`.
69
+
70
+ A video of `files/` (a `video` setting, or a metafield or a metaobject field that points to
71
+ an `.mp4`) is a video object, with its sources and its poster, the `.jpg` of the same name.
72
+ Videos are served in the parts a browser asks for, so they play in Safari and can be sought
73
+ in.
74
+
75
+ ## What else is served as on Shopify
76
+
77
+ - **Styles and scripts are minified**, with a source map, as Shopify minifies them.
78
+ `--no-minify` serves them as written.
79
+ - **Responses are compressed** with Brotli or gzip, with Shopify's `Cache-Control`, so that an
80
+ audit of your theme sees what it would see on the store. `--no-compression` turns it off.
81
+ - **Pages have Shopify's scripts:** the `Shopify` object, `Shopify.actions`,
82
+ `Shopify.loadFeatures`, the Customer Privacy API and `ShopifyAnalytics.meta`.
83
+ - **The Ajax APIs a theme calls** answer as Shopify's do: cart, Section Rendering, predictive
84
+ search, recommendations, forms. The list: [testing](testing.md#what-the-server-answers).
85
+ - **The app proxy of an app:** with `--app` and an `[app_proxy]` in `shopify.app.toml`,
86
+ `/apps/<subpath>` (or the `prefix` it names) is forwarded to the backend of the app, signed as
87
+ Shopify signs it, and a Liquid answer is rendered in the theme's layout. See the
88
+ [Admin API guide](admin.md#app-proxy).
89
+
90
+ ## Theme app extensions
91
+
92
+ With `--app`, the theme renders the app blocks and app embed blocks of the app's theme app
93
+ extensions, as Shopify does once the app is installed. An extension is a folder of the app
94
+ whose `shopify.extension.toml` says `type = "theme"`, with `blocks/`, `snippets/`, `assets/`
95
+ and `locales/`.
96
+
97
+ ```bash
98
+ npx local-storefront serve --app ../my-app --live-reload
99
+ ```
100
+
101
+ The `app:` line counts them: `(0 Functions, 0 UI extensions, 1 theme extension (2 app blocks,
102
+ 1 app embed block))`.
103
+
104
+ ### App blocks
105
+
106
+ A JSON template or a section group names an app block by its type, as the theme editor writes
107
+ it:
108
+
109
+ ```json
110
+ "reviews": {
111
+ "type": "shopify://apps/product-reviews/blocks/reviews/5b1c7a52-2d0e-4c8a-9a3e-6f1d2b7c9e01",
112
+ "settings": { "heading": "Loved by customers" }
113
+ }
114
+ ```
115
+
116
+ `product-reviews` is the `handle` of `shopify.app.toml`, or its `name` as a handle. `reviews` is
117
+ the file `blocks/reviews.liquid` of one of its extensions. The last part is the `uid` of the
118
+ extension. A type copied from another store has another id: the block is found by its name all
119
+ the same.
120
+
121
+ The block renders where the theme renders blocks:
122
+
123
+ - `{% render block %}`, for a block of `section.blocks` whose `block.type` is `@app`, as in Dawn;
124
+ - `{% content_for 'blocks' %}`, in a section or a theme block that accepts `@app` blocks, as in
125
+ Horizon;
126
+ - a section of type `apps`, which the theme editor adds around an app block: the theme's
127
+ `sections/apps.liquid`, or Shopify's own wrapper when the theme has none.
128
+
129
+ Its output is wrapped as on Shopify, in `<div id="shopify-block-<id>" class="shopify-block
130
+ shopify-app-block">`, with the `class` and the `tag` of its schema. Its Liquid reads:
131
+
132
+ - `block.settings`, with the defaults of its schema; a `t:` default is read from the
133
+ extension's `locales/*.schema.json`;
134
+ - `block.id`, `block.type` (`@app`), `section`, `closest`, and the objects of the page
135
+ (`product` on a product page);
136
+ - `app.metafields`: the app-data metafields of the store data (`app.metafields`, below).
137
+
138
+ In the Liquid of an extension, `{% render 'stars' %}` renders its `snippets/stars.liquid`,
139
+ `asset_url` points to its `assets/` (served at `/cdn/extensions/<uid>/assets/`), and `t` reads
140
+ its `locales/`, never the theme's. The `javascript` and the `stylesheet` of a schema load once
141
+ per page, before `</head>`, when a block that names them renders. `enabled_on` and
142
+ `disabled_on` keep a block out of templates and section groups; `available_if` keeps it out
143
+ until its metafield is `true`.
144
+
145
+ ### App embed blocks
146
+
147
+ An app embed block (`"target": "head"`, `"compliance_head"` or `"body"`) renders on every page
148
+ where it is turned on: `compliance_head` then `head` before `</head>`, `body` before `</body>`.
149
+ The theme editor turns one on in `config/settings_data.json`, under `current.blocks`, with
150
+ `"disabled": false`. To turn one on without changing the theme, or to set the app-data
151
+ metafields, write `app` in the store data:
152
+
153
+ ```json
154
+ {
155
+ "app": {
156
+ "embeds": {
157
+ "chat-bubble": true,
158
+ "reviews-loader": { "settings": { "api_key": "test-key" } }
159
+ },
160
+ "metafields": { "reviews": { "badge_enabled": true } }
161
+ }
162
+ }
163
+ ```
164
+
165
+ An embed is named by its file in `blocks/`. `true` turns it on, with the settings of
166
+ `config/settings_data.json` or its defaults; `false` turns it off; `{"settings": {...}}` turns
167
+ it on with these. A test does it for its session alone with `PUT /__lsf/session` and
168
+ `{"data": {"app": ...}}` ([testing](testing.md#data-for-one-test-only)).
169
+
170
+ ### When a block renders nothing
171
+
172
+ lsf says why, once in the terminal and in the `X-LSF-Reason` header of the page: the app has no
173
+ block of that name, the block is another app's, no app is served (`--app`), `enabled_on` leaves
174
+ the page out, `available_if` is not `true`. An app embed block the theme turns on says so too
175
+ when no app is served. When several blocks of a page render nothing, `X-LSF-Reason` gives them
176
+ all: `3 reasons: … | … | …`. A Liquid error in a file of an extension is reported
177
+ as the theme's are, with its path: `Liquid syntax error (extensions/reviews/snippets/stars line
178
+ 1): Unknown tag 'unknowntag'`.
179
+
180
+ At start, `serve` prints the blocks the theme names that the app does not have
181
+ (`app: warning: ...`, with the name it probably meant), and the blocks no template uses or the
182
+ app embed blocks that are off (`app: note: ...`). The home page of the app
183
+ (`/__lsf/checkouts`) lists its extensions and their blocks. `check --app ../my-app` checks the
184
+ app blocks of the theme, and exits with `1` for one that renders nothing (a block the app does
185
+ not have, one its `enabled_on` keeps out of the template). `app build` checks the extensions as
186
+ Shopify does when it deploys them ([CLI reference](cli.md#app-build)).
187
+
188
+ With `--live-reload`, open pages reload when a file of an extension changes. The Section
189
+ Rendering API (`?sections=`) renders app blocks as the page does. What differs from Shopify is
190
+ in [compatibility](compatibility.md#known-differences).
191
+
192
+ ## Feature previews
193
+
194
+ Shopify tries changes on development stores before it ships them. A store turns them on in its
195
+ list of feature previews. local-storefront renders what a live store renders, so a theme that
196
+ uses a preview fails as it would there, and local-storefront says which preview to turn on:
197
+
198
+ ```bash
199
+ npx local-storefront serve --preview liquid-july-26
200
+ ```
201
+
202
+ `--preview` can be repeated, takes several names separated by commas, and is also read from
203
+ `LSF_PREVIEW`. Every command accepts it.
204
+
205
+ | Preview | Shopify's name | What it adds |
206
+ |---|---|---|
207
+ | `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. |
208
+
209
+ A preview can still change on Shopify's side. [Compatibility](compatibility.md) says what
210
+ 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`.