local-storefront 0.3.0-rc.1 → 0.3.0-rc.3
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 +127 -0
- package/docs/admin-reference.md +13 -4
- package/docs/admin.md +22 -4
- package/docs/checkout.md +62 -4
- package/docs/cli.md +47 -3
- package/docs/compatibility.md +40 -13
- package/docs/control-api.md +2 -1
- package/docs/data-format.md +233 -15
- package/docs/data-reference.md +27 -2
- package/docs/performance.md +2 -1
- package/docs/storefront.md +43 -2
- package/docs/testing.md +2 -2
- package/package.json +14 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,133 @@
|
|
|
3
3
|
What changed in each version of `lsf`, for someone who uses it. The section of a version is
|
|
4
4
|
what its [GitHub release](https://github.com/0xtlt/local-storefront/releases) says.
|
|
5
5
|
|
|
6
|
+
## 0.3.0-rc.3 - 2026-11-12
|
|
7
|
+
|
|
8
|
+
Third release candidate of 0.3.0. The local checkout applies the code discounts a backend makes
|
|
9
|
+
through the Admin API, and the session token of checkout and customer account UI extensions is
|
|
10
|
+
the one Shopify gives, signed with the client secret. A backend that read lsf's old token needs
|
|
11
|
+
a change: see Breaking changes.
|
|
12
|
+
|
|
13
|
+
### Breaking changes
|
|
14
|
+
|
|
15
|
+
What to change when you upgrade from 0.3.0-rc.2.
|
|
16
|
+
|
|
17
|
+
- **The session token of a UI extension has Shopify's claims.** `dest` is the shop's bare
|
|
18
|
+
domain (`local-supply-co.myshopify.com`), no longer lsf's origin; there is no `iss` and no
|
|
19
|
+
`sid`; it lives five minutes; `sub` is there only when a customer is logged in. It is signed
|
|
20
|
+
with HS256 and the client secret, so a backend now verifies it as it verifies Shopify's, and
|
|
21
|
+
one that skipped the verification for lsf can stop doing so.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
#### The checkout of an app
|
|
26
|
+
|
|
27
|
+
- **`DiscountCodeBasic` and `DiscountCodeFreeShipping` codes apply at the checkout.** A code made
|
|
28
|
+
with `discountCodeBasicCreate` or `discountCodeFreeShippingCreate` takes an amount or a
|
|
29
|
+
percentage off the order, off listed products, variants or collections, or off each item, or
|
|
30
|
+
makes the rates up to `maximumShippingPrice` free in the countries of `destination`. It is taken
|
|
31
|
+
while the discount is active by its dates, under its `usageLimit`, for a buyer of its `context`
|
|
32
|
+
(all, listed customers, members of listed segments) who has not used it when it
|
|
33
|
+
`appliesOncePerCustomer`, and for a cart that meets its `minimumRequirement`. One such code
|
|
34
|
+
applies per checkout, with the Function discounts. The order and its `orders/create` and
|
|
35
|
+
`orders/paid` carry it in `discount_codes` and `discount_applications`, and its
|
|
36
|
+
`asyncUsageCount` goes up. `combinesWith`, buy X get Y codes and a `context` of markets are not
|
|
37
|
+
read.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
#### The checkout of an app
|
|
42
|
+
|
|
43
|
+
- **`shopify.sessionToken.get()` gives a token a backend can verify.** The page asks lsf for it
|
|
44
|
+
(`POST /__lsf/extensions/<handle>/session-token`), and lsf signs it with the client secret of
|
|
45
|
+
the Admin API (`--client-secret`, `SHOPIFY_API_SECRET`, the app's `.env`), which stays on the
|
|
46
|
+
server. `get()` rejects with the reason when lsf gives no token.
|
|
47
|
+
|
|
48
|
+
## 0.3.0-rc.2 - 2026-11-12
|
|
49
|
+
|
|
50
|
+
Second release candidate of 0.3.0. It fixes what was reported on 0.2.0 and 0.3.0-rc.1, each
|
|
51
|
+
change checked against what Shopify does, and the store data gains provinces, currencies,
|
|
52
|
+
market languages, URLs per market and translated metaobjects. Some theme tests and store data
|
|
53
|
+
need a change: see Breaking changes.
|
|
54
|
+
|
|
55
|
+
### Breaking changes
|
|
56
|
+
|
|
57
|
+
What to change when you upgrade from 0.3.0-rc.1.
|
|
58
|
+
|
|
59
|
+
- **A theme block rendered more than once on a page gets Shopify's numbered ids.** The first
|
|
60
|
+
render keeps `<prefix>__<key>`, the next ones get `-1`, `-2`, … in `block.id` and in
|
|
61
|
+
`id="shopify-block-…"`. A block rendered once is unchanged. Theme tests that snapshot HTML with
|
|
62
|
+
a repeated block need their snapshots updated.
|
|
63
|
+
- **A `rating` metafield's `value`, `scale_min` and `scale_max` are strings, as on Shopify.**
|
|
64
|
+
`{{ m.value.scale_max | json }}` gives `"5.0"`, no longer `5.0`. A number written in the store
|
|
65
|
+
data is kept as written (`5` gives `"5"`).
|
|
66
|
+
- **A reference metafield lsf can't resolve is a `validate` error.** A GID now names the entity
|
|
67
|
+
with that id; a GID or any other value that names nothing, which used to pass and render `nil`,
|
|
68
|
+
fails `validate` with the form to write (`use "faq/size"`).
|
|
69
|
+
- **A country's `currency` is what its buyers pay in.** It was only displayed. With no `rate`,
|
|
70
|
+
prices are converted at a rate of 1: give the market a `rate` or fixed `prices`.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
#### Store data and the storefront
|
|
75
|
+
|
|
76
|
+
- **Provinces.** `data-provinces` of `country_option_tags` and `all_country_option_tags` lists
|
|
77
|
+
the provinces Shopify lists for a country (37 countries), as Shopify prints them.
|
|
78
|
+
`localization.countries[].provinces` limits a country to those the store ships to. Addresses
|
|
79
|
+
get their province's name from its code and the reverse, the checkout has a province
|
|
80
|
+
selector, and `shopify_common.js` has `Shopify.CountryProvinceSelector`.
|
|
81
|
+
- **Currencies per market.** `localization.markets[].currency`, `rate` and fixed `prices` price
|
|
82
|
+
the storefront, the cart, `/cart.js`, the money filters, `Shopify.currency` and the checkout
|
|
83
|
+
in the currency of the visitor's country. Functions read the rate as
|
|
84
|
+
`presentmentCurrencyRate`.
|
|
85
|
+
- **The URL picks the country.** `?country=XX` sets it (a `301` to the country's subfolder when
|
|
86
|
+
its market has one). `localization.markets[].subfolder_suffix` serves a market under
|
|
87
|
+
`/<language>-<suffix>/` (`/en-ca/`), with its links, `request.locale.root_url` and
|
|
88
|
+
`Shopify.routes.root` under it. The localization form moves the visitor to that subfolder.
|
|
89
|
+
- **Languages per market.** `localization.markets[].languages` (default first) drives
|
|
90
|
+
`country.available_languages` and `localization.available_languages`, and the subfolders a
|
|
91
|
+
market has. Switching country keeps the current language when the new market offers it, and
|
|
92
|
+
uses the market's default otherwise. Without `languages`, every country gets every language.
|
|
93
|
+
- **Translated metaobjects.** A metaobject entry takes `translations`, by language then field
|
|
94
|
+
key: localized routes show them, field by field, with `fr-CA` falling back to `fr`. `validate`
|
|
95
|
+
checks them, and the Admin API's `translationsRegister` and `translationsRemove` read and write
|
|
96
|
+
them. Translations into a locale the Admin API unpublishes leave the storefront until it is
|
|
97
|
+
published again.
|
|
98
|
+
|
|
99
|
+
#### The command line
|
|
100
|
+
|
|
101
|
+
- **`validate` warns when the theme names a menu the store data lacks.** `default_menu` for
|
|
102
|
+
`main-menu` and `footer`, which lsf makes up, and `missing_menu` for the others. `render` warns
|
|
103
|
+
and the `--live-reload` panel shows a note when a page renders one of lsf's default menus.
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
#### Themes and the storefront
|
|
108
|
+
|
|
109
|
+
- **Cart lines have `index`, their 0-based position**, so Horizon's remove button removes the
|
|
110
|
+
line clicked, not the first one.
|
|
111
|
+
- **`/cart/change.js` keeps to the stock, as `/cart/add.js` does.** More than a tracked variant's
|
|
112
|
+
stock caps the line and answers `422`, with Shopify's 2025 descriptions ("Only N items were
|
|
113
|
+
added to your cart due to availability.", "The maximum quantity of this item is already in
|
|
114
|
+
your cart.").
|
|
115
|
+
- **Minified stylesheets mean what their source means.** `translate`, `rotate` and `scale` stay
|
|
116
|
+
apart from `transform`, `font` and `transition` stay before what they reset, and
|
|
117
|
+
`border-image: none` is kept.
|
|
118
|
+
- **A minified asset links its source map at its own `?v=`**, as Shopify's CDN does, so the map
|
|
119
|
+
of an edited file is fetched again.
|
|
120
|
+
- **A reference metafield given as a GID renders the entity it names.** It rendered `nil`.
|
|
121
|
+
- **An object or list metafield can be written as its Admin API JSON text** (the `value` the
|
|
122
|
+
Admin API gives), so a value can be pasted from the API as is.
|
|
123
|
+
|
|
124
|
+
#### The command line
|
|
125
|
+
|
|
126
|
+
- **Paths in messages no longer start with `\\?\` on Windows.**
|
|
127
|
+
- **On Windows, path options read a Git Bash path (`/c/Users/me`) as `C:\Users\me`.** A missing
|
|
128
|
+
data directory given that way no longer suggests `init` with it. `render` and `profile` refuse
|
|
129
|
+
a route Git Bash rewrote into `C:/Program Files/Git/…`, and name `MSYS_NO_PATHCONV=1`.
|
|
130
|
+
- **`lsf app check` with no scenario names the extensions that are not built** in its summary,
|
|
131
|
+
with `lsf app build`, instead of a `fails` line for each. It still exits with `1`.
|
|
132
|
+
|
|
6
133
|
## 0.3.0-rc.1 - 2026-11-12
|
|
7
134
|
|
|
8
135
|
lsf now serves a local GraphQL Admin API (version 2026-10) and delivers an app's webhooks to its
|
package/docs/admin-reference.md
CHANGED
|
@@ -877,8 +877,14 @@ simulate: give a test the bytes through `stagedUploadsCreate`.
|
|
|
877
877
|
of the resources. lsf translates products, collections, pages, blogs, articles, metafields and
|
|
878
878
|
metaobject fields of translatable types, policies, product options and their values, and menus.
|
|
879
879
|
The digest is the SHA-256 of the content, as Shopify's; the locale must be one of the shop's
|
|
880
|
-
other than its primary one. A translation is `outdated` once its resource changed.
|
|
881
|
-
are
|
|
880
|
+
other than its primary one. A translation is `outdated` once its resource changed. The
|
|
881
|
+
translations of a metaobject are those of the store data ([metaobject
|
|
882
|
+
translations](data-format.md#translations-of-metaobjects)): `translatableResource` reads them,
|
|
883
|
+
`translationsRegister` and `translationsRemove` change them, and the storefront shows them on the
|
|
884
|
+
routes of their language (`/fr/...`). Those of a market, and of the other resources, are not
|
|
885
|
+
shown on the storefront. A locale unpublished (`shopLocaleUpdate`) keeps its translations in the
|
|
886
|
+
Admin API but leaves the storefront with them, until it is published again; `shopLocaleDisable`
|
|
887
|
+
deletes them.
|
|
882
888
|
|
|
883
889
|
**Webhooks.** `metafield_definitions/create`, `/update` (also pin and unpin), `/delete`;
|
|
884
890
|
`metaobjects/create`, `/update`, `/delete`; `products/update` when a product's or a variant's
|
|
@@ -1073,8 +1079,11 @@ Function by `functionHandle` (or the deprecated `functionId`), which must be a d
|
|
|
1073
1079
|
of the app; their `metafields` are the Function's configuration, checked as `metafieldsSet`
|
|
1074
1080
|
checks them (a definition of the `DISCOUNT` owner type applies). The store sync writes an active
|
|
1075
1081
|
app discount back to the store data's `checkout.discounts` (its Function, title, first code,
|
|
1076
|
-
classes and metafields), so the local checkout runs it. Shopify's own types
|
|
1077
|
-
|
|
1082
|
+
classes and metafields), so the local checkout runs it. Of Shopify's own types, the local
|
|
1083
|
+
checkout applies the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` itself, and
|
|
1084
|
+
counts the orders placed with them in `asyncUsageCount`
|
|
1085
|
+
([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)); buy X get Y and
|
|
1086
|
+
the automatic ones are kept as records.
|
|
1078
1087
|
Their `summary` is the admin's: what the discount takes off, then its minimum, in the shop's
|
|
1079
1088
|
money format (`$5.00 off entire order • Minimum purchase of $50.00`, `10% off 2 products`, `Buy
|
|
1080
1089
|
2 items, get 1 item free`, `Free shipping on all products`), and `shortSummary` its first part.
|
package/docs/admin.md
CHANGED
|
@@ -655,7 +655,7 @@ and delivery customizations, validations, cart transforms, fulfillment constrain
|
|
|
655
655
|
app discounts (`DiscountAutomaticApp`, `DiscountCodeApp`), whether the app created them or the
|
|
656
656
|
store data declared them (`checkout.functions.<handle>.enabled`, `checkout.discounts`). The
|
|
657
657
|
answer counts them (`removedFunctionOwners`). The discounts of Shopify's own types (a
|
|
658
|
-
`DiscountCodeBasic` the app made) stay. While the app is uninstalled, the shop's checkout runs
|
|
658
|
+
`DiscountCodeBasic` the app made) stay, and their codes still apply at the checkout. While the app is uninstalled, the shop's checkout runs
|
|
659
659
|
none of its Functions, whatever a scenario declares or names in its `extensions`: the
|
|
660
660
|
checkout's Functions dialog lists each with "the app is not installed on the shop". A reinstall
|
|
661
661
|
gets none of those objects back, as on Shopify, where a reinstalled app "had to register them
|
|
@@ -1624,6 +1624,24 @@ What the backend changes through the Admin API is what the storefront and the
|
|
|
1624
1624
|
extensions on the order status page (`appMetafields`, `owner_type = "ORDER"`);
|
|
1625
1625
|
- an app discount it creates (`discountAutomaticAppCreate`, `discountCodeAppCreate`) runs its
|
|
1626
1626
|
Function at the checkout, with the metafields it was given, while it is active;
|
|
1627
|
+
- a code discount of Shopify's own types it creates (`discountCodeBasicCreate`,
|
|
1628
|
+
`discountCodeFreeShippingCreate`) applies at the checkout once the buyer enters one of its
|
|
1629
|
+
codes, without a Function: an amount or a percentage off the order, off the products,
|
|
1630
|
+
variants and collections it lists (`appliesOnEachItem` included), or free shipping. The
|
|
1631
|
+
checkout takes the code while the discount is `ACTIVE` by its dates, its `usageLimit` is not
|
|
1632
|
+
reached, the buyer is one of its `context` (all buyers, the customers it lists, the members of
|
|
1633
|
+
the segments it lists: a guest is the customer whose email they gave), has not used it when it
|
|
1634
|
+
`appliesOncePerCustomer`, and the cart meets its `minimumRequirement` (the subtotal or the
|
|
1635
|
+
quantity of the items it applies to, before discounts); free shipping makes free the rates up
|
|
1636
|
+
to its `maximumShippingPrice`, to the countries of its `destination`. Otherwise the page says
|
|
1637
|
+
"Enter a valid discount code", and the state's `discountCodes` says why (`reason`). The order
|
|
1638
|
+
carries the code (`discountCodes`, a `DiscountCodeApplication`, the allocations), so do the
|
|
1639
|
+
`discount_codes` and `discount_applications` of `orders/create` and `orders/paid`, and the
|
|
1640
|
+
discount's `asyncUsageCount` (and its code's) goes up by one at once. One code of these types
|
|
1641
|
+
applies per checkout (lsf does not read `combinesWith`), and it stacks with the discounts of
|
|
1642
|
+
the Functions; of the shipping discounts, the best one applies. Not read: a `context` of
|
|
1643
|
+
markets (every buyer), `appliesOnOneTimePurchase` and `appliesOnSubscription`. Buy X get Y
|
|
1644
|
+
codes and Shopify's automatic discounts stay records;
|
|
1627
1645
|
- the shipping rates of the general delivery profile it changes are the checkout's delivery
|
|
1628
1646
|
options (see [delivery](admin-reference.md#inventory-transfers-fulfillment-and-delivery));
|
|
1629
1647
|
- a theme file it writes to the main theme (`themeFilesUpsert`, `themeFilesCopy`,
|
|
@@ -1637,10 +1655,10 @@ What the backend changes through the Admin API is what the storefront and the
|
|
|
1637
1655
|
| Syncs | Does not sync |
|
|
1638
1656
|
|---|---|
|
|
1639
1657
|
| Products: title, handle, description, vendor, type, tags, status and publication, options, variants (price, SKU, barcode, inventory policy, weight), stock per location, media from `files/`, collections and selling plan groups they are in | Orders and what belongs to them (`orderCreate`, draft orders, fulfillments, refunds, returns): the account pages show the orders of the data and of the checkout |
|
|
1640
|
-
| Collections: title, handle, description, image, products in order | Discounts
|
|
1658
|
+
| Collections: title, handle, description, image, products in order | Discounts of Shopify's own types: the local checkout reads the code ones (`DiscountCodeBasic`, `DiscountCodeFreeShipping`) from the Admin API itself (above), and leaves the others (`…Bxgy`, the automatic ones) out; of an app discount, `combinesWith`, usage limits and every code but the first |
|
|
1641
1659
|
| Customers: email, names, phone, tags, marketing consent, account state, addresses, their company | Gift cards the Admin API creates (changes to the data's cards sync) |
|
|
1642
|
-
| Companies and their locations, pages, blogs and articles, menus, locations, metaobjects, selling plan groups | SEO, product categories, bundle components and combined listings (a bundle is a product of its own there), collection rules and sources (a changed automated collection becomes a manual one), product feeds, point of sale |
|
|
1643
|
-
| Metafields of all of these, of the shop and of markets; markets, their countries and their currency; the published languages | Metafield and metaobject definitions, files, translations, publications other than the Online Store; catalogs, price lists, quantity rules, web presences, channels |
|
|
1660
|
+
| Companies and their locations, pages, blogs and articles, menus, locations, metaobjects and their translations, selling plan groups | SEO, product categories, bundle components and combined listings (a bundle is a product of its own there), collection rules and sources (a changed automated collection becomes a manual one), product feeds, point of sale |
|
|
1661
|
+
| Metafields of all of these, of the shop and of markets; markets, their countries and their currency; the published languages | Metafield and metaobject definitions, files, translations of other resources or for a market, publications other than the Online Store; catalogs, price lists, quantity rules, web presences, channels |
|
|
1644
1662
|
| Shop: name, email, description, policies | Domains, currency, time zone, plan |
|
|
1645
1663
|
|
|
1646
1664
|
Each family of the [reference](admin-reference.md#what-lsf-simulates) says what of it syncs.
|
package/docs/checkout.md
CHANGED
|
@@ -1117,6 +1117,22 @@ discounts on the subtotal they left, then shipping. Within a class:
|
|
|
1117
1117
|
off −€0.00" while its settings are not set. A candidate of more than 0 that finds nothing left
|
|
1118
1118
|
to take off (a line another discount made free) is not applied.
|
|
1119
1119
|
|
|
1120
|
+
**Codes of Shopify's own discount types.** With the [Admin API](admin.md), a code of a
|
|
1121
|
+
`DiscountCodeBasic` or a `DiscountCodeFreeShipping` the backend created applies without a
|
|
1122
|
+
Function ([what the checkout sees](admin.md#what-the-storefront-and-the-checkout-see)): an
|
|
1123
|
+
amount off the order is an order discount, one off listed products, variants or collections (or
|
|
1124
|
+
off each item) a product discount, free shipping a shipping discount, and each takes its place
|
|
1125
|
+
among the discounts of the Functions by the rules above. The code is checked when the
|
|
1126
|
+
checkout is computed: the discount's dates and usage limit, its buyers, one use per customer,
|
|
1127
|
+
its minimum (on the items it applies to, before discounts), and for free shipping the country
|
|
1128
|
+
of the address (a code is taken before there is one). A refused code shows "Enter a valid
|
|
1129
|
+
discount code", and its entry of `discountCodes` in the state has a `reason`
|
|
1130
|
+
(`"its minimum purchase is 50.00 USD, and the items it applies to come to 32.00 USD"`). One code
|
|
1131
|
+
of these types applies per checkout, the first one entered that qualifies: a second is refused,
|
|
1132
|
+
as on Shopify when a discount combines with no other (lsf does not read `combinesWith`). Its
|
|
1133
|
+
`DiscountApplication` is `ACROSS` (`EACH` off each item), `ALL` for the order and `ENTITLED`
|
|
1134
|
+
for listed items; free shipping is 100% off the shipping line, `EACH`.
|
|
1135
|
+
|
|
1120
1136
|
An amount is shared between the lines in proportion to what they cost, to the cent, and the
|
|
1121
1137
|
allocations of the lines (`discountAllocations`, in the page and for the Functions that run
|
|
1122
1138
|
later) add up to the discount.
|
|
@@ -1298,6 +1314,39 @@ of checkout UI extensions has no such type): an `ORDER` declaration reads nothin
|
|
|
1298
1314
|
a warning. Shopify lists `CART` as an owner type of checkout UI extensions only and `ORDER` of
|
|
1299
1315
|
customer account UI extensions only; lsf takes both on either, as one extension can target both.
|
|
1300
1316
|
|
|
1317
|
+
### Session tokens
|
|
1318
|
+
|
|
1319
|
+
`shopify.sessionToken.get()`, in a checkout or a customer account extension, gives the token
|
|
1320
|
+
Shopify gives, which the backend verifies as it verifies Shopify's (`authenticate.public.checkout`
|
|
1321
|
+
and `authenticate.public.customerAccount` of Shopify's libraries, `decodeSessionToken`): an
|
|
1322
|
+
HS256 JWT signed with the client secret of the [Admin API](admin.md#start)
|
|
1323
|
+
(`--client-secret`, `SHOPIFY_API_SECRET` of the environment or of the app's `.env`, else
|
|
1324
|
+
`lsf-local-client-secret`; the same without the admin part, without `--client-secret`). Its
|
|
1325
|
+
claims are those of shopify.dev's Session Token API, for
|
|
1326
|
+
[checkout](https://shopify.dev/docs/api/checkout-ui-extensions/latest/target-apis/platform-apis/session-token-api)
|
|
1327
|
+
and [customer account](https://shopify.dev/docs/api/customer-account-ui-extensions/latest/target-apis/platform-apis/session-token-api)
|
|
1328
|
+
extensions:
|
|
1329
|
+
|
|
1330
|
+
```json
|
|
1331
|
+
{
|
|
1332
|
+
"dest": "local-supply-co.myshopify.com",
|
|
1333
|
+
"aud": "<client id>",
|
|
1334
|
+
"exp": 1794511949,
|
|
1335
|
+
"nbf": 1794511649,
|
|
1336
|
+
"iat": 1794511649,
|
|
1337
|
+
"jti": "7f63880b-b45d-49d4-beb1-af926c85653c",
|
|
1338
|
+
"sub": "gid://shopify/Customer/6523834743460"
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
`dest` is the shop's domain, bare, as in shopify.dev's example (the session token of the admin
|
|
1343
|
+
has `https://`): the `permanent_domain` of the store data, the default shop of the Admin API.
|
|
1344
|
+
`aud` is the client id, the token lives five minutes, `jti` is a new UUID each time, and `sub`
|
|
1345
|
+
is there only when a customer is logged in (whether the app may read customer data is not
|
|
1346
|
+
checked). There is no `iss` and no `sid`. The page asks lsf for the token
|
|
1347
|
+
(`POST /__lsf/extensions/<handle>/session-token`, [control API](control-api.md#apps-checkout-and-admin-api))
|
|
1348
|
+
and hands it to the extension: the secret never reaches the browser.
|
|
1349
|
+
|
|
1301
1350
|
## Run a Function without a server
|
|
1302
1351
|
|
|
1303
1352
|
```bash
|
|
@@ -1505,7 +1554,7 @@ customization runs on them.
|
|
|
1505
1554
|
| `shippingAddress` | `address`: `firstName`, `lastName`, `company`, `address1`, `address2`, `city`, `countryCode`, `provinceCode`, `zip`, `phone` | Sets the address. A country of the store becomes the country of the session. A `countryCode` that is not a country (ISO 3166-1 alpha-2, or `XK`, `AC`, `TA` as Shopify takes them; not `ZZ`) is refused and changes nothing. The store data has no shipping zones: any real country is taken. |
|
|
1506
1555
|
| `deliveryOption` | `group` (`group-1`), `handle` | Chooses a shipping rate. |
|
|
1507
1556
|
| `paymentMethod` | `handle` | Chooses a payment method. No handle clears it. |
|
|
1508
|
-
| `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. |
|
|
1557
|
+
| `discountCode` | `action` (`add` or `remove`), `code` | Enters or takes back a code. Each entered code is in the `discountCodes` of the state, `applicable` or not; a code of a discount of Shopify's own types that was refused has a `reason` ([how discounts stack](#how-discounts-stack)). |
|
|
1509
1558
|
| `cartLines` | `change`: `addCartLine`, `removeCartLine` or `updateCartLine` of the UI extension API | Changes the cart of the session, the one the theme shows. |
|
|
1510
1559
|
| `attribute` | `change`: `updateAttribute` or `removeAttribute`, with `key` and `value` | Changes the attributes of the cart. |
|
|
1511
1560
|
| `note` | `change`: `updateNote` (`note`) or `removeNote` | Changes the note of the cart. |
|
|
@@ -1564,7 +1613,10 @@ The browser tests of a theme in CI: [testing](testing.md#in-ci).
|
|
|
1564
1613
|
- Discounts have no `conditions` and no combinations: all of them stack, as described in
|
|
1565
1614
|
[how discounts stack](#how-discounts-stack). Shopify lets the merchant choose which discounts
|
|
1566
1615
|
combine, and applies the best of those that cannot; here all the product and order discounts
|
|
1567
|
-
combine, and of the shipping discounts the best one applies.
|
|
1616
|
+
combine, and of the shipping discounts the best one applies. Of Shopify's own discount types,
|
|
1617
|
+
only the codes of `DiscountCodeBasic` and `DiscountCodeFreeShipping` apply, one per checkout,
|
|
1618
|
+
and only with the Admin API (which makes them): not buy X get Y, not the automatic ones, not a
|
|
1619
|
+
`context` of markets nor `appliesOnSubscription`.
|
|
1568
1620
|
- Tax is one rate on the discounted subtotal, with no tax lines.
|
|
1569
1621
|
- No Storefront API: `query` in an extension rejects. No Customer Account API either: a customer
|
|
1570
1622
|
account extension reads the account through its `shopify` global only.
|
|
@@ -1592,8 +1644,14 @@ store before a release:
|
|
|
1592
1644
|
|
|
1593
1645
|
- **Discount combinations and conditions**: lsf combines every product and order discount
|
|
1594
1646
|
([how discounts stack](#how-discounts-stack)).
|
|
1595
|
-
- **
|
|
1596
|
-
|
|
1647
|
+
- **Codes of Shopify's own discount types**: one applies per checkout whatever its
|
|
1648
|
+
`combinesWith`, its minimum is checked on the items it applies to before any discount, and a
|
|
1649
|
+
refused code says lsf's reason, where Shopify's checkout has messages of its own.
|
|
1650
|
+
- **Currencies and price lists**: a buyer pays in the currency of the country of the shipping
|
|
1651
|
+
address (else of the one selected on the storefront), at its market's `rate` or fixed `prices`
|
|
1652
|
+
([localization](data-format.md#localization)); the Functions read that rate as
|
|
1653
|
+
`presentmentCurrencyRate`. Shopify's rounding rules of converted prices and B2B catalogs are
|
|
1654
|
+
not applied ([markets](compatibility.md#known-differences)).
|
|
1597
1655
|
- **B2B catalogs, pickup points, local pickup, fulfillment constraints and order routing**: not
|
|
1598
1656
|
run.
|
|
1599
1657
|
- **Network access**: the request JWT and Shopify's cache of responses
|
package/docs/cli.md
CHANGED
|
@@ -14,6 +14,14 @@ Installed with mise, from a release or from source, the command is `lsf`: `lsf s
|
|
|
14
14
|
| `--data <dir>` | `LSF_DATA` | `<theme>/shopify-local` | The store data folder. Without one, the built-in demo store. |
|
|
15
15
|
| `--preview <name>` | `LSF_PREVIEW` | none | Turns on a Shopify feature preview. Repeat it, or separate names with commas. See [storefront](storefront.md#feature-previews). |
|
|
16
16
|
|
|
17
|
+
On Windows, a path written as Git Bash writes it, `/c/Users/me/project`, is the folder it names,
|
|
18
|
+
`C:\Users\me\project`: for every option that takes a path (`--theme`, `--data`, `--app`, the files
|
|
19
|
+
of `fmt`, `--session`, `--input`, ...), and in its environment variable. Git Bash converts such a
|
|
20
|
+
path itself, except when `MSYS_NO_PATHCONV` is set (see [render](#render)) or when lsf is started
|
|
21
|
+
by another program, such as an npm script run by cmd.exe. On another system it is a path like any
|
|
22
|
+
other; when that folder does not exist, the error names the Windows form instead of offering
|
|
23
|
+
`init` with it.
|
|
24
|
+
|
|
17
25
|
## Exit codes
|
|
18
26
|
|
|
19
27
|
| Code | Meaning |
|
|
@@ -48,7 +56,7 @@ npx local-storefront serve --port 9292 --static --quiet --strict
|
|
|
48
56
|
| `--static` | | Reads the theme and the data once. The fastest mode, for tests. |
|
|
49
57
|
| `--strict` | | Refuses to start when the store data has errors. |
|
|
50
58
|
| `--quiet`, `-q` | | Does not log requests. |
|
|
51
|
-
| `--live-reload` | | Reloads open pages when the theme or the data change. It adds a script to the pages: leave it off for tests. |
|
|
59
|
+
| `--live-reload` | | Reloads open pages when the theme (its `assets/` included) or the data change. It adds a script to the pages: leave it off for tests. Without it, an edit shows at the next load: see [reload as you edit](storefront.md#reload-as-you-edit). |
|
|
52
60
|
| `--customer <email>` | `LSF_CUSTOMER` | Visitors arrive logged in as this customer. Also `default` (the first customer) or `none`. |
|
|
53
61
|
| `--throttle <rules>` | `LSF_THROTTLE` | Answers late, to see loading states: `simulated`, `slow`, `300ms`, `cart-add=1s`. See [testing](testing.md#simulating-a-slow-network). |
|
|
54
62
|
| `--timings` | | Names every section and theme block in the `Server-Timing` header. See [performance](performance.md#render-times). |
|
|
@@ -304,6 +312,15 @@ invalid badges: extensions/badges/blocks/stars.liquid: a block needs a {% schem
|
|
|
304
312
|
checked 11 of 11 scenarios in 0.1 s: 0 failures, 0 invalid scenarios, 1 extension not offered, 1 stale module, 1 error in theme app extensions
|
|
305
313
|
```
|
|
306
314
|
|
|
315
|
+
With no scenario, nothing runs, so an extension that is only not built fails nothing: rather
|
|
316
|
+
than a `fails` line, the last line names it with the command that builds it, as it names
|
|
317
|
+
`app init`. It still exits with `1`. What is wrong with the app itself, such as an `api_version`
|
|
318
|
+
lsf does not run, is still printed, and counted after it:
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
checked 0 of 0 scenarios in 0.0 s: no scenario to check: `lsf app init` writes a set for the app in `shopify-local/checkouts/`; 2 extensions are not built (payment-rules, order-notice): `lsf app build` builds them (or `--build`)
|
|
322
|
+
```
|
|
323
|
+
|
|
307
324
|
A trap ends with the last message the Function logged, such as the one of a `throw`:
|
|
308
325
|
|
|
309
326
|
```text
|
|
@@ -361,7 +378,7 @@ checked 8 of 8 scenarios in 0.1 s: every Function ran, the `expect` of 2 scenari
|
|
|
361
378
|
| `--extension <handle>` | | Only these extensions. Repeat it for several. |
|
|
362
379
|
| `--build` | | Builds the extensions first, as `app build` does. |
|
|
363
380
|
| `--format text` | | Default. One line per problem, then a summary. |
|
|
364
|
-
| `--format json` | | `{ok, state, checked, total, elapsedMs, failures: [{handle, target, error, logs, scenarios}], invalid: [{scenario, file, problems}], expectations: {checked, unchecked: [scenario], unmet: [{scenario, file, step, assertion, expected, actual}]}, ranInNoScenario: [handle], notOffered: [{handle, reason}], stale: [{handle, file}], themeExtensions: [{handle, severity, message}]}`, and `error` when there is no scenario. `state` is `done`, or `partial` when the time ran out. A Function target that fails in several ways has a failure for each error, with the scenarios of that error; `logs` is what the Function logged in the first of them. `expectations.checked` counts the expectations of the scenarios' `expect`, met or not; `unchecked` names the scenarios whose `expect` was not checked, as `--extension` leaves out a Function they run; `assertion` is the list and the rule (`express_methods.excludes`) or the amount (`totals.total`), `expected` what the scenario says (the entry, the list, the amount), `actual` what the checkout has. `ranInNoScenario` is filled only when every scenario ran. `notOffered` has the extensions the checkout does not offer, `stale` the Functions whose `file` changed after their module was built, `themeExtensions` what Shopify rejects (`error`) or advises against (`warning`) in the theme app extensions. |
|
|
381
|
+
| `--format json` | | `{ok, state, checked, total, elapsedMs, failures: [{handle, target, error, logs, scenarios}], invalid: [{scenario, file, problems}], expectations: {checked, unchecked: [scenario], unmet: [{scenario, file, step, assertion, expected, actual}]}, ranInNoScenario: [handle], notOffered: [{handle, reason}], stale: [{handle, file}], themeExtensions: [{handle, severity, message}]}`, and `error` when there is no scenario, which names the extensions not built too, as the last line of the text does (`notOffered` still lists them). `state` is `done`, or `partial` when the time ran out. A Function target that fails in several ways has a failure for each error, with the scenarios of that error; `logs` is what the Function logged in the first of them. `expectations.checked` counts the expectations of the scenarios' `expect`, met or not; `unchecked` names the scenarios whose `expect` was not checked, as `--extension` leaves out a Function they run; `assertion` is the list and the rule (`express_methods.excludes`) or the amount (`totals.total`), `expected` what the scenario says (the entry, the list, the amount), `actual` what the checkout has. `ranInNoScenario` is filled only when every scenario ran. `notOffered` has the extensions the checkout does not offer, `stale` the Functions whose `file` changed after their module was built, `themeExtensions` what Shopify rejects (`error`) or advises against (`warning`) in the theme app extensions. |
|
|
365
382
|
| `--budget <seconds>` | | How long the Functions may run in all. Default 300. |
|
|
366
383
|
|
|
367
384
|
### app function run
|
|
@@ -544,6 +561,12 @@ sections, as the Section Rendering API does. `/account` (lsf's page of the custo
|
|
|
544
561
|
`/recommendations/products` and `/search/suggest` are answered by the server, as `serve`
|
|
545
562
|
answers them.
|
|
546
563
|
|
|
564
|
+
In Git Bash, a path that starts with `/` and has no query string reaches lsf as a Windows path,
|
|
565
|
+
under the folder Git Bash is installed in (`C:/Program Files/Git/`). `render` and `profile` refuse such a path with an error
|
|
566
|
+
(`2`) rather than render the 404 page. Keep the path as typed with
|
|
567
|
+
`MSYS_NO_PATHCONV=1 npx local-storefront render /products/shirt`, or start it with `//`
|
|
568
|
+
(`render //products/shirt`): leading slashes count as one.
|
|
569
|
+
|
|
547
570
|
The HTML goes to stdout. On stderr: each Liquid error and each warning, then a summary.
|
|
548
571
|
|
|
549
572
|
```text
|
|
@@ -551,6 +574,15 @@ Liquid error (layout/theme line 34): divided by 0
|
|
|
551
574
|
status 200 · template product · 1 Liquid error · 0 warnings
|
|
552
575
|
```
|
|
553
576
|
|
|
577
|
+
A page that renders one of [lsf's default menus](data-format.md#lsfs-default-menus) gets a
|
|
578
|
+
warning for it. With `--format github` it is a `::warning` annotation. It never fails
|
|
579
|
+
`--strict`: a Shopify store cannot delete that menu, so the page is not wrong, only its links
|
|
580
|
+
are lsf's.
|
|
581
|
+
|
|
582
|
+
```text
|
|
583
|
+
warning: menu "main-menu" is lsf's default (Home, Catalog), not the store's: the store data has none with this handle. Add it to "menus" to render the store's links
|
|
584
|
+
```
|
|
585
|
+
|
|
554
586
|
With `--strict`, a path with nothing at it adds `error: nothing at /x: the page is the theme's
|
|
555
587
|
404`.
|
|
556
588
|
|
|
@@ -563,7 +595,7 @@ Prints what the render of one page spends its time on: sections, blocks, snippet
|
|
|
563
595
|
|
|
564
596
|
| Option | What it does |
|
|
565
597
|
|---|---|
|
|
566
|
-
| `<path>` | The path, with its query string. Default `/`. |
|
|
598
|
+
| `<path>` | The path, with its query string. Default `/`. In Git Bash, see [render](#render) for a path Git Bash turned into a Windows one. |
|
|
567
599
|
| `--points` | Counts in points, what the page would cost Shopify, instead of time. |
|
|
568
600
|
| `--cost <kind>=<points>,...` | Changes what a kind of thing costs in points. |
|
|
569
601
|
| `--lines` | Adds every tag and every output, by file and line. |
|
|
@@ -584,6 +616,18 @@ app, such as [the checkout playground](https://github.com/0xtlt/local-storefront
|
|
|
584
616
|
alone, with the store's own languages; an app without any is checked with the demo store, as its
|
|
585
617
|
checkout runs. A folder that is neither a theme nor an app, and has no store data, is an error.
|
|
586
618
|
|
|
619
|
+
With a theme, it also checks the menus the theme names, in its `link_list` settings (of
|
|
620
|
+
`config/settings_data.json`, of the sections of its JSON templates and section groups, of their
|
|
621
|
+
blocks) and in Liquid (`linklists['main-menu']`). A `main-menu` or `footer` that the data does
|
|
622
|
+
not have is a `default_menu` warning, since [lsf renders its own](data-format.md#lsfs-default-menus).
|
|
623
|
+
Another menu the data does not have is a `missing_menu` warning: lsf renders no menu there.
|
|
624
|
+
|
|
625
|
+
```text
|
|
626
|
+
warning[default_menu]: the store data has no "main-menu" menu, which the theme names (sections/header-group.json at /sections/header/settings/menu): lsf renders its default in its place (Home, Catalog), not the store's links
|
|
627
|
+
--> (data) at /menus/main-menu
|
|
628
|
+
hint: add the store's "main-menu" to "menus" (or a menus/main-menu.json file); with "links": [] it renders no links
|
|
629
|
+
```
|
|
630
|
+
|
|
587
631
|
In a Shopify app (or with `--app`), it checks the app too: what its Functions and UI extensions
|
|
588
632
|
get wrong (no build, an API version lsf does not run, an unknown target, an `owner_type`), and
|
|
589
633
|
its theme app extensions as `app build` checks them (`app: badges: extensions/badges/blocks/stars.liquid:
|
package/docs/compatibility.md
CHANGED
|
@@ -113,11 +113,13 @@ metaobject type), `product`, `collection`, `page`, `blog`, `article`, `policy`,
|
|
|
113
113
|
|
|
114
114
|
With their related types: variants, options and option values (with swatches), media (images,
|
|
115
115
|
videos, external videos, 3D models), image presentation and focal points, collection filters
|
|
116
|
-
(with swatches) and sort options, links, addresses, orders and line items
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
(with swatches) and sort options, links, addresses, orders and line items (a cart line has the
|
|
117
|
+
`index` shopify.dev leaves out: its 0-based position, which Horizon posts plus 1 as the `line` of
|
|
118
|
+
`/cart/change.js`; `/cart.js` has no such field), comments, metafields of every type (a
|
|
119
|
+
`color` is a color, a measurement has `type`, `value` and `unit`, a `money` prints as cents,
|
|
120
|
+
has the `currency` it is written in, and the money filters and `metafield_text` format it in
|
|
121
|
+
that currency, a reference is a GID or the key the data gives the entity
|
|
122
|
+
([reference values](data-format.md#reference-values)), a `file_reference` is an image, a video or a file by its extension),
|
|
121
123
|
metaobjects, countries, languages (with `direction`, from the script of the language),
|
|
122
124
|
currencies, markets, fonts, colors, color schemes, quantity rules and quantity price breaks,
|
|
123
125
|
unit prices, selling plan groups, selling plans and their allocations, store availabilities and
|
|
@@ -132,6 +134,19 @@ locations, companies, company locations and company addresses.
|
|
|
132
134
|
defaults from the schema, `disabled` sections and blocks.
|
|
133
135
|
- Theme blocks (`blocks/*.liquid`), nested blocks, static blocks, `{% content_for 'blocks' %}`
|
|
134
136
|
and `{% content_for 'block' %}`, `closest.<resource>`.
|
|
137
|
+
- The id of a theme block (`block.id`, and its wrapper's `id="shopify-block-<id>"`) is
|
|
138
|
+
`<prefix>__<key>`, the key being its key in the JSON template or the `id` of a static block.
|
|
139
|
+
A block rendered again on the same page is numbered as on Shopify: the first render is
|
|
140
|
+
`<prefix>__<key>`, the second `<prefix>__<key>-1`, the third `<prefix>__<key>-2`. This
|
|
141
|
+
happens with a static block in a `for` loop (Horizon's product card), the blocks it holds,
|
|
142
|
+
or a second `{% content_for 'blocks' %}`. `section.blocks` gives the ids without a number.
|
|
143
|
+
A section rendered alone (`?section_id=`, `?sections=`) starts again from the first, and a
|
|
144
|
+
section asked for twice in `?sections=` is rendered once. The prefix is not Shopify's: do
|
|
145
|
+
not rely on its value. Shopify gives each block a prefix of its own, local-storefront one
|
|
146
|
+
per section, so two blocks that give a static block the same id (two collection links with
|
|
147
|
+
their `title`) give it the same id, and neither is numbered. A block defined in a section's
|
|
148
|
+
schema keeps its key as its id, and app blocks and blocks called with `{% block %}` are not
|
|
149
|
+
numbered.
|
|
135
150
|
- With `--preview liquid-july-26`, blocks called from a template or a layout, as in Shopify's
|
|
136
151
|
Skeleton theme: `{% block 'name', parameter: value %}content{% endblock %}`. The parameters
|
|
137
152
|
are read when the tag opens. The block sees them and its body, as `content` (the Liquid
|
|
@@ -163,7 +178,10 @@ locations, companies, company locations and company addresses.
|
|
|
163
178
|
`gid://shopify/Metaobject/<id>`. Dynamic sources are evaluated (`{{ product.title }}`,
|
|
164
179
|
`{{ closest.product.metafields.custom.x }}`).
|
|
165
180
|
- Locales: storefront translations and schema translations, pluralization rules by language,
|
|
166
|
-
the fallback to the default locale, Shopify's built-in translations.
|
|
181
|
+
the fallback to the default locale, Shopify's built-in translations. The translated content
|
|
182
|
+
of metaobjects, from the store data, on the routes of their language
|
|
183
|
+
([data format](data-format.md#translations-of-metaobjects)); not yet that of products,
|
|
184
|
+
collections, pages or articles.
|
|
167
185
|
- Assets: `asset_url` and friends, `.liquid` assets, the `{% stylesheet %}` and
|
|
168
186
|
`{% javascript %}` bundles, `{% style %}`, preload headers.
|
|
169
187
|
- With `--app`, theme app extensions: app blocks (`{% render block %}`, `{% content_for 'blocks'
|
|
@@ -237,11 +255,20 @@ local-storefront. The rules are the ones read from Shopify's answers in October
|
|
|
237
255
|
color functions get a fallback, and vendor prefixes are added.
|
|
238
256
|
- This applies to the assets, `.liquid` ones included once they are rendered, and to the
|
|
239
257
|
bundles built from the `{% stylesheet %}` and `{% javascript %}` tags.
|
|
258
|
+
- A declaration is merged into another only when the result sets the same properties: four
|
|
259
|
+
sides into `margin`, for instance. `translate`, `rotate` and `scale` stay apart from
|
|
260
|
+
`transform`, since they are applied before it and cascade on their own, and `font` and
|
|
261
|
+
`transition` stay where they are written, before or after `font-variant-numeric` or
|
|
262
|
+
`transition-behavior`, which they reset.
|
|
240
263
|
- A minified file ends with a link to its source map, which is served at the path of the
|
|
241
|
-
file followed by `.map`: the tools of a browser show the file as it is written.
|
|
264
|
+
file followed by `.map`: the tools of a browser show the file as it is written. The link
|
|
265
|
+
carries the `?v=` the file was asked at (`base.css?v=123` links `base.css.map?v=123`), as
|
|
266
|
+
Shopify's does, so that the map's URL changes with the file.
|
|
242
267
|
- A file is served as written when its name ends with `.min.js` or `.min.css`, when the
|
|
243
|
-
minified file would not be lighter, or when it cannot be read as CSS or
|
|
244
|
-
|
|
268
|
+
minified file, link included, would not be lighter, or when it cannot be read as CSS or
|
|
269
|
+
JavaScript. There is no size limit below which a file is left alone: a file of a few hundred
|
|
270
|
+
bytes is minified when that saves bytes. In a stylesheet, a rule that cannot be read is kept
|
|
271
|
+
as written and the rest is minified.
|
|
245
272
|
|
|
246
273
|
`--no-minify` serves every file as it is written.
|
|
247
274
|
|
|
@@ -429,19 +456,19 @@ These are deliberate or not done yet. None of them raises a Liquid error.
|
|
|
429
456
|
| Customer accounts | New customer accounts are hosted by Shopify, outside the theme. `/account` is a page of local-storefront that stands in for them: it shows who is logged in and lets you become anyone in the data. With `--app`, it is the customer account of the app instead: orders, profile and the pages of its customer account UI extensions ([checkout](checkout.md#customer-account)). Every account URL (`/account/login`, `/account/register`, `/customer_authentication/login`, ...) leads to it. Classic customer accounts are not simulated: the theme's `templates/customers` are never rendered, and its account forms (login, register, password reset, addresses) lead to `/account`. |
|
|
430
457
|
| Password page | `/password` always shows the theme's password page, and accepts `shop.password` (`password` by default). The storefront is never locked: nothing redirects to that page. On Shopify, a protected store redirects every page to it, and an open store redirects it to the home page. |
|
|
431
458
|
| Discounts | Carts and orders have no discounts: discount arrays are empty and totals are undiscounted (the checkout of an app applies the discounts its Functions give, nowhere else). `Shopify.actions.updateCart` reports every discount code as not applicable. |
|
|
432
|
-
| Standard actions | A change the cart refuses (unknown variant
|
|
459
|
+
| Standard actions | A change the cart refuses (unknown variant) resolves with `userErrors` whose code is `INVALID`, and leaves that line untouched. More than the stock resolves with the same `userErrors`, the line holding what is in stock: the Storefront API would return a warning instead. |
|
|
433
460
|
| Selling plans | Selling plan groups, plans and allocations are there, on products, variants, the cart and `/products/<handle>.js`. A group applies to every variant of a product, not to some of them. Prepaid plans are not modelled: `per_delivery_price` is the price. The line items of orders have no selling plan. The widget of a subscription app is an app block: it renders with `--app` when it is that app's, else the theme's own purchase options do. |
|
|
434
461
|
| Store pickup | `variant.store_availabilities` and `/variants/<id>?section_id=` are there. Locations come in the order of the data: Shopify sorts them by distance from the visitor. Choosing pickup happens at checkout, which is not part of a theme, so `order.pickup_in_store?` is always `false`. |
|
|
435
462
|
| Swatches | A swatch is given to option values by name, or to one value of a product. On Shopify it comes from the category metafield the option is linked to. `filter_value.image` (the `image` presentation of a filter) is not generated. |
|
|
436
463
|
| B2B | Companies, company locations, their addresses and metafields are there: `customer.b2b?`, `current_company`, `current_location`, `company_available_locations` and the link that changes location. Catalogs are not: a B2B customer sees the same products, prices and quantity rules as everyone. No payment terms, no store credit. |
|
|
437
|
-
| Markets |
|
|
464
|
+
| Markets | A country pays in its currency (its own, its market's, else the shop's), at its market's `rate` (1 when there is none: lsf has no exchange rates) or at its fixed `prices`, on the storefront and at the checkout. Shopify's rounding rules of converted prices (`.99`, `.95`) and price adjustments in percent are not applied. A market is served at `/` or under subfolders of its own (`/en-ca`); a market with a domain of its own is not served. Orders the checkout records in the Admin API keep the amounts of the checkout, without converting them back to the shop's currency. |
|
|
438
465
|
| Taxes and shipping | Not computed. |
|
|
439
466
|
| Search | A simple search over titles, descriptions, vendors, types, tags and SKUs. Ranking is not Shopify's: `relevance` keeps the order of the data. `sort_by` takes `price-ascending` and `price-descending` (any other value is `relevance`); pages and articles come after the products. |
|
|
440
467
|
| Recommendations | Products of the same collections, or the product's explicit `recommendations`. |
|
|
441
468
|
| Collection filters | Availability, price, vendor, product type, tag and variant options, with the swatches of option values. Metafield filters are not generated. |
|
|
442
469
|
| Placeholder artwork | `image`, `collection-2` and `lifestyle-2` are Shopify's illustrations: the three that are published under a free license. For the other names, `placeholder_svg_tag` draws generic artwork of the right proportions. |
|
|
443
470
|
| Fonts | Shopify's font library is not bundled. `font_face` and `font_url` generate the same markup and URLs; the files are blank unless provided in `files/fonts/`. |
|
|
444
|
-
| Shopify's shared assets | Requests under `/cdn/shopifycloud/` (payment buttons, model viewer UI, ...) get an empty file of the right type. The files of `shopify_asset_url` are served from the binary: `vendor/qrcode.js` is the qrcode.js library (MIT), as on Shopify; `shopify_common.js` has our own `Shopify.formatMoney` and `Shopify.
|
|
471
|
+
| Shopify's shared assets | Requests under `/cdn/shopifycloud/` (payment buttons, model viewer UI, ...) get an empty file of the right type. The files of `shopify_asset_url` are served from the binary: `vendor/qrcode.js` is the qrcode.js library (MIT), as on Shopify; `shopify_common.js` has our own `Shopify.formatMoney`, `Shopify.resizeImage` and `Shopify.CountryProvinceSelector` (the province selector of an address form, filled from `data-provinces`); `option_selection.js`, `api.jquery.js`, `customer_area.js` and `currencies.js` only log a warning to the console, and `customer.css` is empty, because Shopify's code is proprietary; `gift-card/card.svg` is a drawing of our own, of the size of Shopify's, and `gift-card/add-to-apple-wallet.svg` a placeholder drawing. Any other name is a 404, logged by the server. |
|
|
445
472
|
| Theme editor | `request.design_mode` is always `false`, `Shopify.designMode` is not set, and `block.shopify_attributes` is empty. |
|
|
446
473
|
| Integers | 64-bit. Liquid on Shopify uses arbitrary-precision integers. |
|
|
447
474
|
| Byte strings | Strings are text (UTF-8). `base64_decode` of bytes that are not text replaces them with `�`, where Shopify carries the bytes on to the next filter. |
|
|
@@ -450,7 +477,7 @@ These are deliberate or not done yet. None of them raises a Liquid error.
|
|
|
450
477
|
| Caching | Shopify keeps rendered pages in a cache of its own. An answer from that cache has an `ETag` and no `Cache-Control`; local-storefront always answers like a page that was just rendered. Files have no `Last-Modified`, so nothing is answered with `304 Not Modified`. |
|
|
451
478
|
| Image formats | The format of an image is chosen by Shopify's rule, with other encoders: the files weigh about what Shopify's do, not the same, so an image can come as AVIF where Shopify sends WebP, or the reverse. A GIF is never converted. The color profile and the metadata of a file are not kept when it is encoded. |
|
|
452
479
|
| Preloading | Shopify sends the `Link` header ahead of the page, as `103 Early Hints`, and with the pages it answers from its own cache. local-storefront sends it with every page. Shopify's header also asks to connect to its CDN (`cdn.shopify.com`), which serves nothing locally, and has a limit on its number of entries, which local-storefront does not have. |
|
|
453
|
-
| Minification | Shopify minifies with esbuild, local-storefront with other libraries (oxc for scripts, Lightning CSS for stylesheets): a file weighs about what it does on Shopify, without being the same to the byte. In a stylesheet, declarations can come in another order within a rule. `inset` is not replaced with `top`, `right`, `bottom` and `left`. |
|
|
480
|
+
| Minification | Shopify minifies with esbuild, local-storefront with other libraries (oxc for scripts, Lightning CSS for stylesheets): a file weighs about what it does on Shopify, without being the same to the byte. In a stylesheet, declarations can come in another order within a rule. `inset` is not replaced with `top`, `right`, `bottom` and `left`, and the width, style and color of the four sides of a border, all set in one rule, become `border`, which also resets `border-image`. Shopify's link to the source map carries the `?v=` of the request, local-storefront's does not: a file that minifying saves only a few dozen bytes of can be minified locally and served as written on Shopify. |
|
|
454
481
|
| Compiled scripts | Shopify runs the `{% javascript %}` of a section only on the pages that have that section. local-storefront runs them all on every page. |
|
|
455
482
|
|
|
456
483
|
Found a difference that is not in this table? It is a bug: a Liquid snippet and the HTML
|
package/docs/control-api.md
CHANGED
|
@@ -52,6 +52,7 @@ backend's tests use, then the rest. How a test uses them:
|
|
|
52
52
|
| `GET /checkout?scenario=<name>` | Opens a checkout for a scenario, in a new session whose cookie it sets. With `--app`. | A redirect |
|
|
53
53
|
| `GET /__lsf/checkout/assets/<file>` | The files of the checkout page, which are in the binary. With `--app`. | The file |
|
|
54
54
|
| `GET /__lsf/extensions/<handle>/<file>` | A file of the build (`dist/`) of a checkout UI extension. With `--app`. | The file |
|
|
55
|
+
| `POST /__lsf/extensions/<handle>/session-token` | What `shopify.sessionToken.get()` gives a checkout or customer account UI extension of the app: a token for the shop (`dest`, its bare domain), the app (`aud`) and the customer logged in in the caller's session (`sub`, absent logged out), valid five minutes, signed with HS256 and the client secret ([session tokens](checkout.md#session-tokens)). The checkout page and the customer account ask it for their extensions; a test can call the backend with it. No CORS: another site cannot read it. `404` for an extension the app does not serve. With `--app`. | `{ "token", "authorization", "shop", "claims" }` |
|
|
55
56
|
| `GET /__lsf/admin` | The [Admin API](admin.md): its shops, their tokens, record counts and webhook subscriptions, the app, the throttle, the mutations, each shop's outbox. | JSON, [below](#admin-api) |
|
|
56
57
|
| `GET /__lsf/graphiql` | GraphiQL on the [Admin API](admin.md#graphiql-in-the-browser): the shops, their tokens and the served versions filled in. | HTML |
|
|
57
58
|
| `DELETE /__lsf/admin` | Puts every shop of the Admin API back to the store data (its clock and its outbox too), forgets the subscriptions the backend made and the tokens OAuth issued, and installs the app again (the printed token works). It revokes every token of the app, those of token exchange too, and sends no `app/uninstalled`: the backend's stored tokens are answered `401` ([reset between runs](admin.md#reset-between-runs)). | `{ "ok": true, "shops": [...] }` |
|
|
@@ -128,7 +129,7 @@ curl -H 'x-lsf-session: my-test' http://127.0.0.1:9292/cart.js
|
|
|
128
129
|
| `customer` | An email, `"default"` (the first customer of the data) or `"none"`. |
|
|
129
130
|
| `company_location` | For a B2B customer, the location they buy for. |
|
|
130
131
|
| `cart` | `items`, `note`, `attributes`. Each item has a `variant` (a SKU, an id, or a product handle for its first variant), and optionally a `quantity`, `properties` and a `selling_plan`. See [Cart](data-reference.md#cart). |
|
|
131
|
-
| `country` | The country the visitor shops from, by ISO code. |
|
|
132
|
+
| `country` | The country the visitor shops from, by ISO code. A country whose market is served under subfolders of its own (`subfolder_suffix`) is the visitor's on the pages under them, not at `/`, as on Shopify: see [countries and markets in the URL](storefront.md#countries-and-markets-in-the-url). |
|
|
132
133
|
| `data` | Store data for this session only, applied over the files and over what the [Admin API](admin.md#what-the-storefront-and-the-checkout-see) changed. See [testing](testing.md#data-for-one-test-only). |
|
|
133
134
|
| `throttle` | Delays for this session only. See [testing](testing.md#simulating-a-slow-network). |
|
|
134
135
|
|
package/docs/data-format.md
CHANGED
|
@@ -120,7 +120,8 @@ all, and a running server serves them at `/__lsf/schema/<kind>`.
|
|
|
120
120
|
renders are reproducible.
|
|
121
121
|
- **References** between entities use handles (products, collections, pages, blogs), emails
|
|
122
122
|
(customers) and SKUs or ids (variants). A reference to something that does not exist is an
|
|
123
|
-
error, with a suggestion when a close match exists.
|
|
123
|
+
error, with a suggestion when a close match exists. A metafield also takes Shopify's GIDs
|
|
124
|
+
([reference values](#reference-values)).
|
|
124
125
|
- **HTML** fields (`description`, `content`, `body`) are inserted as written.
|
|
125
126
|
|
|
126
127
|
## Products
|
|
@@ -163,7 +164,8 @@ product with options lists its variants; each gives one value per option:
|
|
|
163
164
|
`"inventory_policy": "continue"`. A variant without `inventory_quantity` is not tracked and
|
|
164
165
|
always available. `available` forces the answer either way. `"available": false` alone also
|
|
165
166
|
makes the variant tracked with a quantity of 0, as a sold-out variant is on Shopify, so themes
|
|
166
|
-
show "Sold out".
|
|
167
|
+
show "Sold out". Asking the cart for more than the stock (`/cart/add.js`, `/cart/change.js`)
|
|
168
|
+
puts what is in stock in it and answers `422`, as on Shopify.
|
|
167
169
|
`"available": false` with an `inventory_quantity` above 0 is a warning
|
|
168
170
|
(`unavailable_in_stock`): Shopify never has both. lsf keeps the variant unavailable with its
|
|
169
171
|
stock, which Horizon shows as "Unavailable".
|
|
@@ -396,6 +398,42 @@ resource with one of `url`, `collection`, `product`, `page`, `blog`, `article`
|
|
|
396
398
|
}
|
|
397
399
|
```
|
|
398
400
|
|
|
401
|
+
### lsf's default menus
|
|
402
|
+
|
|
403
|
+
Shopify's default menus, `main-menu` and `footer`, cannot be deleted from a store, and a theme's
|
|
404
|
+
`link_list` setting can only default to one of them. When the data does not have one of them,
|
|
405
|
+
lsf makes it up with links of its own:
|
|
406
|
+
|
|
407
|
+
| Handle | Title | Links |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| `main-menu` | Main menu | Home (`/`), Catalog (`/collections/all`) |
|
|
410
|
+
| `footer` | Footer menu | Search (`/search`) |
|
|
411
|
+
|
|
412
|
+
A header that shows these links does not prove that the data's menus work: they are lsf's, not
|
|
413
|
+
the store's. lsf says so everywhere it can:
|
|
414
|
+
|
|
415
|
+
- `lsf validate` warns (`default_menu`) for each of the two that the theme names, in a
|
|
416
|
+
`link_list` setting or in Liquid (`linklists['main-menu']`), and says where.
|
|
417
|
+
- `lsf render` prints a warning for each one the page renders. It is not an error, even with
|
|
418
|
+
`--strict`.
|
|
419
|
+
- With `serve --live-reload`, a page that renders one shows a note in its panel, and the
|
|
420
|
+
terminal says it once.
|
|
421
|
+
|
|
422
|
+
There is no option to turn them off, since a Shopify store always has them. Give the data the
|
|
423
|
+
store's menu instead. To render it without links, as a store whose menu is empty, write it with
|
|
424
|
+
none:
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{ "menus": { "footer": { "links": [] } } }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Any other handle the data does not have is not made up. In lsf, `linklists['customer-care']` is
|
|
431
|
+
then `nil`, and so is a `link_list` setting that names it: both equal `blank`, and a loop over
|
|
432
|
+
their `links` renders nothing. Shopify documents that a `link_list` setting whose menu does not
|
|
433
|
+
exist is `blank`. Its reference does not say what `linklists` gives for a handle the store does
|
|
434
|
+
not have. `lsf validate` warns about each menu the theme names that the data does not have
|
|
435
|
+
(`missing_menu`).
|
|
436
|
+
|
|
399
437
|
## The shop
|
|
400
438
|
|
|
401
439
|
Every field of `shop` is optional.
|
|
@@ -721,13 +759,26 @@ store has none: `npx local-storefront app init` writes a set.
|
|
|
721
759
|
|
|
722
760
|
The first language is the primary one, served at `/`. The others are served under
|
|
723
761
|
`/<iso_code>/` (`/fr/products/blue-shirt`) and use the theme's `locales/<iso_code>.json`.
|
|
724
|
-
Names
|
|
762
|
+
Names of countries default to built-in values. Without `countries`, the store
|
|
725
763
|
sells in one country: the shop's (`shop.address.country_code`), or `US`. The localization form
|
|
726
764
|
(`{% form 'localization' %}`) switches the session's country and language. A visitor who
|
|
727
765
|
selected no country is in the shop's (`shop.address.country_code`), the one of its primary
|
|
728
766
|
market, on the storefront as at the checkout; in the first country when the store does not sell
|
|
729
767
|
to the shop's.
|
|
730
768
|
|
|
769
|
+
A country has the provinces Shopify lists for it (the states of the United States, the provinces
|
|
770
|
+
of Canada, the prefectures of Japan, the regions of the United Kingdom: 37 countries, from
|
|
771
|
+
Shopify's address data). The country selectors of a theme list them as Shopify does: in
|
|
772
|
+
`data-provinces` of `country_option_tags` and `all_country_option_tags`, sorted by name, each a
|
|
773
|
+
pair of the name a form sends and the name to show (`["Tōkyō","Tokyo"]`,
|
|
774
|
+
`["District of Columbia","Washington DC"]`). The province selector of the checkout lists them
|
|
775
|
+
too, and an address gets the name of its province from its code, or the code from the name.
|
|
776
|
+
`provinces` restricts a country to those the store ships to, by code:
|
|
777
|
+
|
|
778
|
+
```json
|
|
779
|
+
{ "iso_code": "US", "provinces": ["CA", "OR", "WA"] }
|
|
780
|
+
```
|
|
781
|
+
|
|
731
782
|
`markets` groups countries the way Shopify's markets do. The checkout Functions read the
|
|
732
783
|
market of the buyer (`localization.market`) and of an address (`MailingAddress.market`):
|
|
733
784
|
|
|
@@ -735,8 +786,14 @@ market of the buyer (`localization.market`) and of an address (`MailingAddress.m
|
|
|
735
786
|
{
|
|
736
787
|
"localization": {
|
|
737
788
|
"countries": [{ "iso_code": "FR" }, { "iso_code": "DE" }, { "iso_code": "US" }],
|
|
789
|
+
"languages": [{ "iso_code": "en" }, { "iso_code": "fr" }, { "iso_code": "de" }],
|
|
738
790
|
"markets": [
|
|
739
|
-
{
|
|
791
|
+
{
|
|
792
|
+
"handle": "europe",
|
|
793
|
+
"countries": ["FR", "DE"],
|
|
794
|
+
"languages": ["fr", "de"],
|
|
795
|
+
"metafields": { "custom": { "tier": "gold" } }
|
|
796
|
+
}
|
|
740
797
|
]
|
|
741
798
|
}
|
|
742
799
|
}
|
|
@@ -745,6 +802,55 @@ market of the buyer (`localization.market`) and of an address (`MailingAddress.m
|
|
|
745
802
|
A country in no market is a market of its own, named after the country (`france`, `united-states`).
|
|
746
803
|
An address in a country the store does not sell to has no market.
|
|
747
804
|
|
|
805
|
+
A market's `languages` are the languages its countries are offered, as on Shopify:
|
|
806
|
+
`country.available_languages` lists them in that order, and so does
|
|
807
|
+
`localization.available_languages` for the visitor's country. The first is the market's default:
|
|
808
|
+
when the localization form switches to a country whose market does not offer the current
|
|
809
|
+
language, the visitor moves to that default; a language it offers stays. Here France and Germany
|
|
810
|
+
get French then German (a visitor on an English page who switches to Germany lands on `/fr/...`),
|
|
811
|
+
and the United States every language. A market without `languages`, and a country in no market,
|
|
812
|
+
offer every language of the store, the primary first. A language that is not in
|
|
813
|
+
`localization.languages` is an `unknown_language` error. Every language is served under
|
|
814
|
+
`/<iso_code>/` at `/`, whatever the visitor's country; a market with a `subfolder_suffix` (below)
|
|
815
|
+
has URLs of its own, in its languages only.
|
|
816
|
+
|
|
817
|
+
A market can sell in its own currency, as a Shopify market with a base currency: `currency`, the
|
|
818
|
+
`rate` that converts the store's prices to it (what one unit of the store currency is worth, as a
|
|
819
|
+
manual exchange rate; 1 when there is none, since lsf has no exchange rates), and `prices`, fixed
|
|
820
|
+
prices of a price list, which are not converted. A key of `prices` is a SKU, a variant id, or a
|
|
821
|
+
product handle for each of its variants; a value a price, or `{"price", "compare_at_price"}`.
|
|
822
|
+
|
|
823
|
+
```json
|
|
824
|
+
{
|
|
825
|
+
"handle": "europe",
|
|
826
|
+
"countries": ["FR", "DE"],
|
|
827
|
+
"currency": "EUR",
|
|
828
|
+
"rate": "0.92",
|
|
829
|
+
"prices": { "TEE-WHT-S": "25.00", "canvas-tote-bag": { "price": "30.00", "compare_at_price": "40.00" } }
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
A country pays in its own `currency`, else in its market's, else in the store's. A visitor in it
|
|
834
|
+
sees every price in that currency, rounded to its minor unit (to whole yen for JPY): products,
|
|
835
|
+
variants, compare-at and unit prices, selling plans, the cart, `/cart.js`, the checkout and its
|
|
836
|
+
shipping rates. `cart.currency`, `localization.country.currency` and the money filters follow;
|
|
837
|
+
`shop.currency` stays the store's, and `shop.enabled_currencies` lists every currency of the
|
|
838
|
+
countries. Gift cards keep their own currency, and orders of the data are priced as their
|
|
839
|
+
variants are.
|
|
840
|
+
|
|
841
|
+
`subfolder_suffix` serves a market under subfolders of its own, as a Shopify market whose web
|
|
842
|
+
presence is a subfolder of the primary domain: `/<language>-<suffix>/` for each of its
|
|
843
|
+
`languages` (`/en-be/`, `/fr-be/`), or each language of the store when it lists none. A subfolder
|
|
844
|
+
in a language the market does not offer is not found, as on Shopify (`/es-be/` when Belgium
|
|
845
|
+
offers English and French). A page there is in a country of the market, and `?country=` and the
|
|
846
|
+
localization form move the visitor between `/` and the subfolders as Shopify does, in a language
|
|
847
|
+
the market offers (see [the storefront guide](storefront.md#countries-and-markets-in-the-url)).
|
|
848
|
+
The market of the shop's country is served at `/`, so it cannot have one.
|
|
849
|
+
|
|
850
|
+
```json
|
|
851
|
+
{ "handle": "belgium", "countries": ["BE"], "subfolder_suffix": "be" }
|
|
852
|
+
```
|
|
853
|
+
|
|
748
854
|
## Images and files
|
|
749
855
|
|
|
750
856
|
An image is a path inside `files/`, optionally with metadata:
|
|
@@ -789,8 +895,8 @@ is served so that browsers fall back to the theme's fallback stack without netwo
|
|
|
789
895
|
## Metafields and metaobjects
|
|
790
896
|
|
|
791
897
|
Metafields are grouped by namespace, then key. A plain value gets its type inferred; spell out
|
|
792
|
-
`{ "type", "value" }` for anything else. References are written as handles
|
|
793
|
-
inside `files/`.
|
|
898
|
+
`{ "type", "value" }` for anything else. References are written as handles or GIDs
|
|
899
|
+
([reference values](#reference-values)), files as paths inside `files/`.
|
|
794
900
|
|
|
795
901
|
```json
|
|
796
902
|
{
|
|
@@ -812,16 +918,30 @@ inside `files/`.
|
|
|
812
918
|
`{{ product.metafields.custom.size_guide.value.title }}` then behave as on Shopify, including
|
|
813
919
|
`metafield_tag` and `metafield_text`.
|
|
814
920
|
|
|
921
|
+
A value that is an object or a list (a `rating`, a `money`, a `weight`, a `list.` type...) may
|
|
922
|
+
also be written as its JSON text, the Admin API's `value`, so that it can be pasted from the API
|
|
923
|
+
as is: `"{\"scale_min\":\"1.0\",\"scale_max\":\"5.0\",\"value\":\"4.5\"}"`. A `json`
|
|
924
|
+
metafield is the exception: a string is a value of it.
|
|
925
|
+
|
|
926
|
+
A `rating` keeps its `value`, `scale_min` and `scale_max` as strings, as Shopify does:
|
|
927
|
+
|
|
928
|
+
```json
|
|
929
|
+
"rating": { "type": "rating", "value": { "scale_min": "1.0", "scale_max": "5.0", "value": "4.5" } }
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
Its Liquid object is Shopify's, with strings too: `{{ m.value | json }}` gives
|
|
933
|
+
`{"rating":"4.5","scale_max":"5.0","scale_min":"1.0"}`, and `{% for i in (1..m.value.scale_max) %}`
|
|
934
|
+
runs 5 times, as Liquid turns the string `"5.0"` into 5 (it raises "invalid integer" on the
|
|
935
|
+
number `5.0`). A number in the data is kept as it is written, as a string: `"scale_max": 5` is
|
|
936
|
+
`"5"`.
|
|
937
|
+
|
|
815
938
|
A typed value is checked against its type, as Shopify checks it. `{"type": "number_integer",
|
|
816
939
|
"value": "two"}` is an `invalid_metafield` error, and so is a `weight` in `"kgs"` or a
|
|
817
940
|
`rating` outside its scale. A `weight`, `dimension` or `volume` takes the units Shopify lists
|
|
818
941
|
(`kilograms`, `centimeters`, `milliliters`...) and their short forms (`kg`, `cm`, `ml`). A
|
|
819
942
|
`language` is a locale code (`fr`, `pt-BR`). A type Shopify does not have is an
|
|
820
|
-
`unknown_metafield_type`. A
|
|
821
|
-
|
|
822
|
-
products, variants (by SKU or id), collections, pages, articles (`blog/article`) and
|
|
823
|
-
metaobjects (`type/handle`). A `file_reference` to a file that is not in `files/` is no error:
|
|
824
|
-
it shows as a placeholder.
|
|
943
|
+
`unknown_metafield_type`. A reference that names nothing of the data is an
|
|
944
|
+
`unknown_reference` ([reference values](#reference-values)).
|
|
825
945
|
|
|
826
946
|
A `file_reference` is an image when its extension is one (`.jpg`, `.png`, `.webp`...) and a
|
|
827
947
|
video for `.mp4`, `.mov`, `.webm` and `.m4v`: `sources`, `preview_image`, `video_tag` and
|
|
@@ -858,6 +978,98 @@ namespaces are (`$app:config`). Shopify writes them in full, `app--<app id>--aut
|
|
|
858
978
|
theme's Liquid uses: `metaobjects['app--1234--author']` and `metaobjects['$app:author']` both
|
|
859
979
|
find them, whatever the id, since the store data does not know it.
|
|
860
980
|
|
|
981
|
+
### Translations of metaobjects
|
|
982
|
+
|
|
983
|
+
An entry's `translations` give its fields in the store's other languages, by language, then
|
|
984
|
+
field key, as Shopify's translatable resources do (`translationsRegister`):
|
|
985
|
+
|
|
986
|
+
```json
|
|
987
|
+
{
|
|
988
|
+
"localization": { "languages": [{ "iso_code": "en" }, { "iso_code": "fr" }] },
|
|
989
|
+
"metaobjects": {
|
|
990
|
+
"faq": [{
|
|
991
|
+
"handle": "shipping",
|
|
992
|
+
"fields": {
|
|
993
|
+
"question": "How long does shipping take?",
|
|
994
|
+
"answer": { "type": "rich_text_field", "value": { "type": "root", "children": [{ "type": "paragraph", "children": [{ "type": "text", "value": "Two days." }] }] } },
|
|
995
|
+
"position": 1
|
|
996
|
+
},
|
|
997
|
+
"translations": {
|
|
998
|
+
"fr": {
|
|
999
|
+
"question": "Combien de temps prend la livraison ?",
|
|
1000
|
+
"answer": { "type": "root", "children": [{ "type": "paragraph", "children": [{ "type": "text", "value": "Deux jours." }] }] }
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}]
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
On a route of that language (`/fr/...`), `{{ metaobjects.faq.shipping.question }}`, the
|
|
1009
|
+
entry's page and its title, and the entries a metafield or a setting points to show the French
|
|
1010
|
+
values. As on Shopify, field by field, a field without a translation into the route's
|
|
1011
|
+
language shows its translation into the base language of a regional one (`fr` for `fr-CA`),
|
|
1012
|
+
else its own value.
|
|
1013
|
+
|
|
1014
|
+
- A translation is written as the field's value is (a string, a rich text object, a list), and
|
|
1015
|
+
checked against the field's type: a `url` that is not a URL is an `invalid_metafield`.
|
|
1016
|
+
- Only the types Shopify translates take one: `single_line_text_field`,
|
|
1017
|
+
`multi_line_text_field`, `rich_text_field`, `json`, `link`, `url`, `file_reference`, and the
|
|
1018
|
+
lists of single line texts, links, URLs and files. Numbers, booleans, dates, measurements,
|
|
1019
|
+
money and the other references are the same in every language: translating one is an
|
|
1020
|
+
`untranslatable_field`.
|
|
1021
|
+
- The language is one of `localization.languages` other than the first, the primary one, which
|
|
1022
|
+
the fields are written in (`unknown_language`). A key the entry has no field for is an
|
|
1023
|
+
`unknown_field`, with the closest one. A blank translation is an `invalid_value`: leave the key
|
|
1024
|
+
out instead.
|
|
1025
|
+
|
|
1026
|
+
The [Admin API](admin.md) reads these translations (`translatableResource`) and changes them
|
|
1027
|
+
(`translationsRegister`, `translationsRemove`), and the storefront shows what it changed.
|
|
1028
|
+
|
|
1029
|
+
### Reference values
|
|
1030
|
+
|
|
1031
|
+
Shopify writes a reference as a GID, and a list of references as a JSON array of GIDs
|
|
1032
|
+
(`["gid://shopify/Metaobject/123", "gid://shopify/Metaobject/456"]`, [list of data
|
|
1033
|
+
types](https://shopify.dev/docs/apps/build/custom-data/metafields/list-of-data-types)). The
|
|
1034
|
+
store data takes that form, and a shorter one: the key the data gives the entity. The shorter
|
|
1035
|
+
form is an lsf convention for fixtures; Shopify does not take it.
|
|
1036
|
+
|
|
1037
|
+
| Type | lsf's form | Shopify's form |
|
|
1038
|
+
|---|---|---|
|
|
1039
|
+
| `product_reference` | the handle: `"ceramic-mug"` | `"gid://shopify/Product/<id>"` |
|
|
1040
|
+
| `variant_reference` | the SKU (`"MUG-1"`), or the id as a number | `"gid://shopify/ProductVariant/<id>"` |
|
|
1041
|
+
| `collection_reference` | the handle | `"gid://shopify/Collection/<id>"` |
|
|
1042
|
+
| `page_reference` | the handle | `"gid://shopify/Page/<id>"` |
|
|
1043
|
+
| `article_reference` | `"blog/article"`, or the article's handle alone when one blog only has it | `"gid://shopify/Article/<id>"` |
|
|
1044
|
+
| `metaobject_reference`, `mixed_reference` | `"type/handle"`: `"faq/size"` | `"gid://shopify/Metaobject/<id>"` |
|
|
1045
|
+
| `file_reference` | the path inside `files/`: `"lookbook.jpg"` | not read: lsf gives files no GID |
|
|
1046
|
+
|
|
1047
|
+
A GID names the entity of that id in the store: the `id` the data gives it, or the one lsf gives
|
|
1048
|
+
it otherwise, which Liquid shows (`product.id`, `metaobject.system.id`) and the [Admin
|
|
1049
|
+
API](admin.md) answers. A metaobject entry has no `id` in the data: lsf makes one from its type
|
|
1050
|
+
and handle, so the GID of an entry copied from a Shopify store names nothing here. Write
|
|
1051
|
+
`"type/handle"` instead.
|
|
1052
|
+
|
|
1053
|
+
Any other form reads `nil` in Liquid and is an error. `"faq.size"`, `"faq:size"`, `"size"` and
|
|
1054
|
+
`{"type": "faq", "handle": "size"}` are neither Shopify's form nor lsf's, and `validate` says
|
|
1055
|
+
what to write:
|
|
1056
|
+
|
|
1057
|
+
```text
|
|
1058
|
+
error[unknown_reference]: "faq.size" names nothing: a metaobject_reference names a metaobject entry as "type/handle"
|
|
1059
|
+
--> store.json at /products/0/metafields/custom/faqs/value/1
|
|
1060
|
+
hint: use "faq/size"
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
A reference to something the data does not have is an `unknown_reference` too, with the closest
|
|
1064
|
+
name. These read `nil` without an error, as on Shopify:
|
|
1065
|
+
|
|
1066
|
+
- a product that is not on the storefront (a draft, an unpublished one);
|
|
1067
|
+
- a GID in the [Admin API's changes](admin.md#what-the-storefront-and-the-checkout-see) that
|
|
1068
|
+
names what the Admin API deleted.
|
|
1069
|
+
|
|
1070
|
+
A `file_reference` to a file that is not in `files/` is no error either: it shows as a
|
|
1071
|
+
placeholder.
|
|
1072
|
+
|
|
861
1073
|
|
|
862
1074
|
## Time and theme settings
|
|
863
1075
|
|
|
@@ -900,7 +1112,7 @@ extensions:
|
|
|
900
1112
|
|---|---|
|
|
901
1113
|
| `invalid_json` | The file is not valid JSON. The hint gives the line and column. |
|
|
902
1114
|
| `unreadable_file` | The file could not be read. |
|
|
903
|
-
| `unknown_field` | A field that does not exist. The hint suggests the closest one. The field is left out, the rest of the file is read. |
|
|
1115
|
+
| `unknown_field` | A field that does not exist, or a metaobject's translation of a field the entry does not have. The hint suggests the closest one. The field is left out, the rest of the file is read. |
|
|
904
1116
|
| `classic_customer_accounts` | A warning: `shop.customer_accounts` or a customer's `password`, which classic customer accounts had. The field is ignored. |
|
|
905
1117
|
| `missing_field` | A required field is absent. |
|
|
906
1118
|
| `wrong_type` | A value of the wrong JSON type. |
|
|
@@ -908,15 +1120,19 @@ extensions:
|
|
|
908
1120
|
| `invalid_money`, `invalid_date`, `invalid_email`, `invalid_handle`, `invalid_timezone`, `invalid_gift_card_code` | A value that does not parse as what it should be. `invalid_money` is a shipping rate below zero. |
|
|
909
1121
|
| `duplicate_handle`, `duplicate_id`, `duplicate_email`, `duplicate_menu`, `duplicate_variant`, `duplicate_gift_card` | Two entities with the same identity. |
|
|
910
1122
|
| `duplicate_section` | `shop`, `localization`, `session`, `checkout` or `now` defined in two files. |
|
|
911
|
-
| `unknown_language` | A checkout scenario names a language that is not in `localization.languages
|
|
912
|
-
| `unknown_product`, `unknown_collection`, `unknown_variant`, `unknown_customer`, `unknown_company`, `unknown_location`, `unknown_country`, `unknown_image`, `unknown_reference` | A reference to something that is not in the data, in a file of `checkouts/` as anywhere else. The hint suggests the closest match. A link of a menu is an `unknown_reference`, whatever it names (a product, a collection, a page, an article, a policy), and so is a reference of a metafield, and a shipping rate or a payment method a scenario's `entered` names. |
|
|
1123
|
+
| `unknown_language` | A checkout scenario or a market names a language that is not in `localization.languages`, or a metaobject's `translations` do, or name the primary language. |
|
|
1124
|
+
| `unknown_product`, `unknown_collection`, `unknown_variant`, `unknown_customer`, `unknown_company`, `unknown_location`, `unknown_country`, `unknown_image`, `unknown_reference` | A reference to something that is not in the data, in a file of `checkouts/` as anywhere else. The hint suggests the closest match. A link of a menu is an `unknown_reference`, whatever it names (a product, a collection, a page, an article, a policy), and so is a reference of a metafield (in a form lsf does not read, its hint is the form to write: `use "faq/size"`), and a shipping rate or a payment method a scenario's `entered` names. |
|
|
913
1125
|
| `duplicate_company`, `duplicate_location`, `missing_locations`, `missing_company` | A company without a location, two companies or two locations with the same name, or a customer that lists locations or a `company_contact` without a company. |
|
|
914
1126
|
| `duplicate_market_country` | A country in two `localization.markets`. |
|
|
1127
|
+
| `invalid_value`, `invalid_format` of a market's `currency` or `rate` | A currency that is not three letters, a rate that is not a decimal above 0. A key of its `prices` that names no variant is an `unknown_variant`. |
|
|
1128
|
+
| `invalid_value` of a `subfolder_suffix` | A suffix that is not two letters at least, that is a script code (`latn`), or that the market of the shop's country has. Two markets with the same suffix are a `duplicate_handle`. |
|
|
1129
|
+
| `unknown_province` | A code of a country's `provinces` that is not one of its provinces, or `provinces` for a country that has none. The hint lists its codes. |
|
|
915
1130
|
| `unknown_return_line` | A return of an order names a variant that is not a line of the order. |
|
|
916
1131
|
| `expect_checks_nothing` (warning) | An `excludes` of a scenario's `expect` names a payment method, express button or delivery option the checkout does not offer, so it checks nothing. The hint lists what it offers. |
|
|
917
1132
|
| `invalid_throttle` | The `throttle` of a session sent to `PUT /__lsf/session` is not one `--throttle` takes. |
|
|
918
1133
|
| `unknown_selling_plan`, `missing_selling_plan`, `duplicate_selling_plan` | A selling plan or a group that is not in the data, or that the product is not sold with; a group without a plan, or a product that requires a plan and has none; two groups or two plans with the same name. |
|
|
919
|
-
| `invalid_metafield`, `unknown_metafield_type` | A typed metafield (or field of a metaobject) whose value its type refuses, or whose type Shopify does not have. The hint shows how the type is written. |
|
|
1134
|
+
| `invalid_metafield`, `unknown_metafield_type` | A typed metafield (or field of a metaobject, or a translation of one) whose value its type refuses, or whose type Shopify does not have. The hint shows how the type is written. |
|
|
1135
|
+
| `untranslatable_field` | A metaobject's `translations` translate a field of a type Shopify does not translate (a number, a boolean, a product reference...). The hint lists the types it translates. |
|
|
920
1136
|
| `unknown_setting`, `invalid_setting` | A `theme_settings` id that `config/settings_schema.json` does not have, or a value its setting does not take. The hint suggests the closest id or option. |
|
|
921
1137
|
| `unchecked_theme_settings` (warning) | The data has `theme_settings` and there is no theme to check them against. |
|
|
922
1138
|
| `invalid_color`, `empty_swatch` | A swatch whose color is not a CSS color, or that has neither a color nor an image. |
|
|
@@ -924,6 +1140,8 @@ extensions:
|
|
|
924
1140
|
| `missing_variants`, `missing_options`, `option_mismatch`, `too_many_options` | The options of a product and the option values of its variants do not line up. |
|
|
925
1141
|
| `media_conflict`, `missing_sources`, `missing_external_video` | Incomplete or contradictory product media. |
|
|
926
1142
|
| `ambiguous_link` | A menu link that points at more than one thing. |
|
|
1143
|
+
| `default_menu` (warning) | The theme names `main-menu` or `footer`, and the data does not have it: lsf renders [its default](#lsfs-default-menus) there. `file` is `(data)` and `path` is `/menus/<handle>`. |
|
|
1144
|
+
| `missing_menu` (warning) | The theme names another menu that the data does not have: lsf renders no menu there. The message says where the theme names it, and the hint suggests the closest handle. |
|
|
927
1145
|
| `missing_locale_file` (warning) | A language the theme has no `locales/<code>.json` for. |
|
|
928
1146
|
| `gift_card_balance` (warning) | A gift card whose balance is above its initial value. |
|
|
929
1147
|
| `unavailable_in_stock` (warning) | A variant with `"available": false` and an `inventory_quantity` above 0, which Shopify never has. It stays unavailable, with its stock. The hint says which field to remove. |
|
package/docs/data-reference.md
CHANGED
|
@@ -170,7 +170,7 @@ A metafield with an explicit type.
|
|
|
170
170
|
| Field | Type | Description |
|
|
171
171
|
|---|---|---|
|
|
172
172
|
| `type` | string | **Required.** A Shopify metafield type, e.g. `single_line_text_field`, `multi_line_text_field`, `rich_text_field`, `number_integer`, `number_decimal`, `boolean`, `color`, `date`, `date_time`, `url`, `json`, `money`, `rating`, `weight`, `volume`, `dimension` and the other measurements (`distance`, `temperature`, `duration`...), `product_reference`, `collection_reference`, `page_reference`, `article_reference` (`blog/article`, or the article's handle alone), `file_reference`, `metaobject_reference`, or a `list.` variant of those. |
|
|
173
|
-
| `value` | any JSON value | **Required.** The value.
|
|
173
|
+
| `value` | any JSON value | **Required.** The value. Lists are JSON arrays. A reference is written as Shopify writes it, a GID (`"gid://shopify/Product/<id>"`, the id the entity has in the store), or by the key the store data gives the entity, an lsf convention: a handle (`"blue-shirt"`), `blog/article`, `type/handle` of a metaobject entry (`"faq/size"`), a SKU or an id of a variant. Files are paths inside `files/`. An object or a list may also be written as its JSON text, as the Admin API's `value` gives it. |
|
|
174
174
|
|
|
175
175
|
## Product
|
|
176
176
|
|
|
@@ -706,6 +706,7 @@ A metaobject entry (`metaobjects.<type>.<handle>`).
|
|
|
706
706
|
|---|---|---|
|
|
707
707
|
| `handle` | string | **Required.** |
|
|
708
708
|
| `fields` | map of [Metafield](#metafield) | Field values, with the same shorthand as metafields. |
|
|
709
|
+
| `translations` | map of map of any JSON value | The entry in the store's other languages, by language (an `iso_code` of `localization.languages`) then field key: `{"fr": {"question": "Combien ?"}}`. A value is written as the field's (a string, a rich text object, a list), and only the fields Shopify translates take one: text, rich text, JSON, links, URLs, files, and the lists of single line texts, links, URLs and files. On `/fr/...` a field shows its French value, else its own. |
|
|
709
710
|
| `template_suffix` | string | Selects `templates/metaobject/<type>.<suffix>.json` instead of `templates/metaobject/<type>.json` for the page of the entry, which is `/pages/<type>/<handle>`. |
|
|
710
711
|
|
|
711
712
|
## Localization
|
|
@@ -730,9 +731,10 @@ A country the store sells to.
|
|
|
730
731
|
|---|---|---|
|
|
731
732
|
| `iso_code` | string | **Required.** ISO 3166-1 alpha-2 code, e.g. `"FR"`. |
|
|
732
733
|
| `name` | string | Defaults to the English name of the country. |
|
|
733
|
-
| `currency` | string | ISO 4217 currency
|
|
734
|
+
| `currency` | string | ISO 4217 code of the currency its buyers pay in: prices, the cart and the checkout are in it. Defaults to the `currency` of its market, else the store currency. |
|
|
734
735
|
| `unit_system` | string | `metric` or `imperial`. Defaults to `metric`. |
|
|
735
736
|
| `popular` | boolean | Whether to list the country among the popular ones. |
|
|
737
|
+
| `provinces` | array of string | The provinces the store ships to, by code (`["CA", "OR", "WA"]`): those a country selector offers (`data-provinces` of `country_option_tags`), sorted by name as Shopify lists them. Defaults to every province Shopify lists for the country (states, regions, prefectures); a country Shopify lists none for has none. |
|
|
736
738
|
|
|
737
739
|
## Language
|
|
738
740
|
|
|
@@ -753,7 +755,30 @@ A market: the countries the store sells to as one, as checkout Functions see it
|
|
|
753
755
|
|---|---|---|
|
|
754
756
|
| `handle` | string | **Required.** What Functions call the market: `"europe"`. |
|
|
755
757
|
| `countries` | array of string | **Required.** The ISO 3166-1 alpha-2 codes of its countries, which are in `localization.countries`. |
|
|
758
|
+
| `languages` | array of string | The languages its countries are offered (`country.available_languages`, in this order), by ISO code, which are in `localization.languages`. The first is the market's default language: a visitor who switches to one of its countries in a language it does not offer gets it. Defaults to every language of the store. |
|
|
756
759
|
| `metafields` | map of map of [Metafield](#metafield) | |
|
|
760
|
+
| `subfolder_suffix` | string | Serves the market under subfolders of its own, as a Shopify market's web presence does: `"ca"` serves it at `/en-ca/`, `/fr-ca/`, one per language. Two letters at least. Without it, the market is served at `/`, with the primary market. |
|
|
761
|
+
| `currency` | string | The currency its countries pay in, by ISO 4217 code (`"EUR"`), as a Shopify market's base currency: prices, the cart and the checkout are in it. A country's own `currency` wins. Defaults to the store currency. |
|
|
762
|
+
| `rate` | string | What one unit of the store currency is worth in the market's currency, as a decimal string (`"0.92"`): prices are converted with it, as with a manual exchange rate on Shopify. Defaults to 1, since lsf has no exchange rates. |
|
|
763
|
+
| `prices` | map of [FixedPrice](#fixedprice) | Fixed prices in the market's currency, as a Shopify price list has them, which are not converted: by variant (a SKU, an id, or a product handle for each of its variants), a price (`"25.00"`) or `{"price": "25.00", "compare_at_price": "30.00"}`. |
|
|
764
|
+
|
|
765
|
+
## FixedPrice
|
|
766
|
+
|
|
767
|
+
A fixed price of a market: a price, or a price and a compare-at price.
|
|
768
|
+
|
|
769
|
+
One of:
|
|
770
|
+
|
|
771
|
+
- [Money](#money)
|
|
772
|
+
- [FixedPriceObject](#fixedpriceobject)
|
|
773
|
+
|
|
774
|
+
## FixedPriceObject
|
|
775
|
+
|
|
776
|
+
A fixed price with its compare-at price.
|
|
777
|
+
|
|
778
|
+
| Field | Type | Description |
|
|
779
|
+
|---|---|---|
|
|
780
|
+
| `price` | [Money](#money) | **Required.** |
|
|
781
|
+
| `compare_at_price` | [Money](#money) | None when omitted: a fixed price has the compare-at price it is given. |
|
|
757
782
|
|
|
758
783
|
## Location
|
|
759
784
|
|
package/docs/performance.md
CHANGED
|
@@ -263,4 +263,5 @@ Times are in milliseconds.
|
|
|
263
263
|
that a section writes itself, in a loop over `section.blocks`, are part of the section.
|
|
264
264
|
- `/__lsf/timings` counts pages by template, sections and blocks by id. Sections and blocks
|
|
265
265
|
count wherever they are rendered: in a page, alone, or in a cart response. The id of a
|
|
266
|
-
block is the one of `#shopify-block-<id
|
|
266
|
+
block is the one of `#shopify-block-<id>` the first time it renders, and ends with its key:
|
|
267
|
+
the renders after it (`<id>-1`, `<id>-2`, as on Shopify) count under it.
|
package/docs/storefront.md
CHANGED
|
@@ -16,10 +16,24 @@ on a WebSocket, and asks the server every 700 ms where a socket cannot be opened
|
|
|
16
16
|
script to the pages: leave it off for tests. Only HTML pages get it: a `{% layout none %}`
|
|
17
17
|
template that writes JSON is left as it is.
|
|
18
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
|
+
|
|
19
28
|
With `--live-reload`, a page with problems also shows a small panel: the errors of the store
|
|
20
29
|
data, the Liquid errors of the page, and why a page is the 404 page. Its button closes it. The
|
|
21
30
|
terminal has the same problems.
|
|
22
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
|
+
|
|
23
37
|
## Log in as a customer
|
|
24
38
|
|
|
25
39
|
Open <http://127.0.0.1:9292/account>. The page lists the customers of your data: click one to
|
|
@@ -74,8 +88,10 @@ in.
|
|
|
74
88
|
|
|
75
89
|
## What else is served as on Shopify
|
|
76
90
|
|
|
77
|
-
- **Styles and scripts are minified**, with a source map, as Shopify minifies them
|
|
78
|
-
|
|
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.
|
|
79
95
|
- **Responses are compressed** with Brotli or gzip, with Shopify's `Cache-Control`, so that an
|
|
80
96
|
audit of your theme sees what it would see on the store. `--no-compression` turns it off.
|
|
81
97
|
- **Pages have Shopify's scripts:** the `Shopify` object, `Shopify.actions`,
|
|
@@ -87,6 +103,31 @@ in.
|
|
|
87
103
|
Shopify signs it, and a Liquid answer is rendered in the theme's layout. See the
|
|
88
104
|
[Admin API guide](admin.md#app-proxy).
|
|
89
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
|
+
|
|
90
131
|
## Theme app extensions
|
|
91
132
|
|
|
92
133
|
With `--app`, the theme renders the app blocks and app embed blocks of the app's theme app
|
package/docs/testing.md
CHANGED
|
@@ -514,8 +514,8 @@ Everything a theme talks to on a storefront:
|
|
|
514
514
|
| Customer accounts | With new customer accounts, `/account` is a page of local-storefront where the visitor chooses who is logged in; `/account/login`, `/customer_authentication/login` and the other account URLs redirect to it. `/company_location/update?location_id=<id>&return_to=<path>` changes the location of a B2B customer. |
|
|
515
515
|
| Robots and sitemaps | `/robots.txt` (from `templates/robots.txt.liquid`, or Shopify's default rules), `/sitemap.xml` and `/sitemap_<products|pages|collections|blogs>_1.xml`. |
|
|
516
516
|
| Alternate templates | `?view=<suffix>` and the `template_suffix` of the resource. |
|
|
517
|
-
| Section Rendering API | `?section_id=<id>` and `?sections=<id>,<id>` on any page. An id the page does not have is a 404 for `section_id` and `null` in `sections`; either way `x-lsf-not-found` names it and lists the ids the page has. |
|
|
518
|
-
| Cart Ajax API | `GET /cart.js`, `POST /cart/add.js`, `/cart/change.js`, `/cart/update.js`, `/cart/clear.js` (with `sections` and `sections_url`), and the form posts `/cart/add`, `/cart`.
|
|
517
|
+
| Section Rendering API | `?section_id=<id>` and `?sections=<id>,<id>` on any page. An id the page does not have is a 404 for `section_id` and `null` in `sections`; either way `x-lsf-not-found` names it and lists the ids the page has. An id asked for twice in `sections` is rendered once, as on Shopify. |
|
|
518
|
+
| Cart Ajax API | `GET /cart.js`, `POST /cart/add.js`, `/cart/change.js`, `/cart/update.js`, `/cart/clear.js` (with `sections` and `sections_url`), and the form posts `/cart/add`, `/cart`. `add.js` and `change.js` keep to the stock as Shopify does: more than the stock puts what is in stock in the cart and answers `422`. `update.js` does not check it (Shopify does not either for the lines already in the cart). |
|
|
519
519
|
| Product and search JSON | `/products/<handle>.js`, `/products.json`, `/collections/<handle>/products.json`, `/search/suggest.json`, `/search/suggest?section_id=`, `/recommendations/products.json`, `/recommendations/products?section_id=&product_id=`. |
|
|
520
520
|
| Forms | Contact, newsletter (`customer`), blog comment, customer logout, localization (country and language), storefront password. The outcome shows in `form.posted_successfully?` and `form.errors` on the next page, once. A wrong storefront password comes back to `/password` with the error; the right one (`password`, or `shop.password`) goes to the home page. |
|
|
521
521
|
| CDN | `/cdn/shop/t/1/assets/<file>` (including `.liquid` assets), `/cdn/shop/files/<path>` with image transformations, the compiled `{% stylesheet %}` and `{% javascript %}` bundles, fonts. |
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-storefront",
|
|
3
|
-
"version": "0.3.0-rc.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0-rc.3",
|
|
4
|
+
"description": "Run Shopify themes and apps locally, without Shopify: the storefront, a local checkout for Functions and checkout UI extensions, and a local GraphQL Admin API with webhooks, from JSON store data. Made for tests in CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shopify",
|
|
7
7
|
"theme",
|
|
8
8
|
"liquid",
|
|
9
9
|
"storefront",
|
|
10
|
+
"shopify-app",
|
|
11
|
+
"shopify-functions",
|
|
12
|
+
"checkout",
|
|
13
|
+
"admin-api",
|
|
14
|
+
"graphql",
|
|
15
|
+
"webhooks",
|
|
10
16
|
"e2e",
|
|
11
17
|
"testing",
|
|
12
18
|
"playwright"
|
|
@@ -32,11 +38,11 @@
|
|
|
32
38
|
"node": ">=18"
|
|
33
39
|
},
|
|
34
40
|
"optionalDependencies": {
|
|
35
|
-
"local-storefront-darwin-arm64": "0.3.0-rc.
|
|
36
|
-
"local-storefront-darwin-x64": "0.3.0-rc.
|
|
37
|
-
"local-storefront-linux-arm64": "0.3.0-rc.
|
|
38
|
-
"local-storefront-linux-x64": "0.3.0-rc.
|
|
39
|
-
"local-storefront-win32-arm64": "0.3.0-rc.
|
|
40
|
-
"local-storefront-win32-x64": "0.3.0-rc.
|
|
41
|
+
"local-storefront-darwin-arm64": "0.3.0-rc.3",
|
|
42
|
+
"local-storefront-darwin-x64": "0.3.0-rc.3",
|
|
43
|
+
"local-storefront-linux-arm64": "0.3.0-rc.3",
|
|
44
|
+
"local-storefront-linux-x64": "0.3.0-rc.3",
|
|
45
|
+
"local-storefront-win32-arm64": "0.3.0-rc.3",
|
|
46
|
+
"local-storefront-win32-x64": "0.3.0-rc.3"
|
|
41
47
|
}
|
|
42
48
|
}
|