Repository navigation
Releases: 0xtlt/local-storefront
Release list
v0.2.0
lsf now runs the checkout and the customer account of a Shopify app: its Functions and its UI extensions, on your machine. Classic customer accounts are no longer simulated: see Changed.
Added
- A local checkout for Shopify apps.
lsf app dev --app <dir>serves the checkout of an app without a theme, andlsf serve --app <dir>leads the/checkoutof a theme to it. It runs the Shopify Functions of the app (cart transform, discounts, delivery and payment customization, validation) from their built Wasm modules, and shows its checkout UI extensions in the page, in the standard one-page look or in the Shop app's (--variant shop).--extension <handle>(repeatable) picks the extensions to use,--buildruns the build command of each Function first. A badge in the corner of the page opens a dialog with the input and output of each Function run side by side. API versions 2026-10 and 2027-01 are supported. Guide: checkout. - Customer account UI extensions, and every checkout target. With an app,
/accountis the customer account of Shopify's new customer accounts: the orders of the customer logged in (from the store data, and the last order placed in the local checkout), the status of an order (its shipments, what is not shipped, its returns), the profile (addresses, and the company and locations of a B2B customer), and the full pages of extensions. The 24 targets of customer account UI extensions render there, with theshopifyglobal they read (the account logged in, the order, navigation, toasts) and the components only customer accounts have (s-page,s-customer-account-action,s-menu, …); an order action opens its dialog. The checkout gains the 14 targets it lacked: pickup locations and points, the targets of payment apps (with the form dialog), the gift card form, bundle lines, the chat, and the address autocomplete, which suggests addresses as the buyer types. An order of the store data can list itsreturns. See customer account. - The commands of a Shopify app are under
lsf app, named as Shopify's CLI names them:app devserves the app alone (its checkout and its customer account),app initwrites checkout scenarios,app buildbuilds its extensions,app checkruns the scenarios for CI,app function runandapp function buildits Functions. CLI reference. - Testing an app's checkout in CI.
lsf app checkruns the Functions of every scenario and exits with 1 when one fails or does not validate, or when there is no scenario.lsf validatechecks the app of the folder too (with the demo store when the app has no store data), and its JSON counts the app's errors with the data's. The checkout and the customer account setdata-lsf-readyon the document once nothing is on its way and every extension rendered (aria-busymeanwhile).DELETE /__lsf/sessionsresets every session. The orders of a session are numbered after the last one of the store data, one after the other whatever the other sessions do, and placed at itsnow: a test reads the same number and date every time. Testing guide. - Commands of an app need no
--appin its folder (lsf app dev,lsf app init,lsf app function run,lsf app check), and read itsshopify-local/from anywhere.lsf app initgives every Function the settings its input query reads ([extensions.input.variables]): those of the app's own store data, or empty values of the right type inapp-settings.json, so that the scenarios it writes passapp check; a variable without a value says where to give it. The frame of an extension is in the language it renders in (lang).lsf app function runsays why a Function asked for did not run (no discount names it, an empty cart, an error of the app) and exits with 1; a handle or a scenario close to an existing one is suggested. Its--sessiontakesdataasPUT /__lsf/sessiondoes: a discount or settings for one run. - Watch mode for the checkout of an app.
lsf app dev --app <dir> --watch(andserve --app <dir> --watch) builds a Function again when its files (or the files the Functions share, such as a Cargo workspace'sCargo.toml) change, and reloads the open checkout, thank-you and home pages once the builds are over, asshopify app devdoes. At start it builds only the Functions that have no module or changed since the last watch (all with--build). The checkout runs a copy of the last module that built, never what a build is writing: a build that fails keeps it and shows its first error on the page and on the home page. A checkout UI extension's bundle,shopify.app.tomland the scenarios reload the pages too. See watch mode. - Log in as a customer at the checkout. "Log in" in the Contact section of the local checkout opens a menu of the customers of the store data. Picking one logs the checkout (and the session) in as them: the Contact section shows the account, Delivery offers their saved addresses with the default one chosen, a B2B buyer picks the location they buy for, and the Functions and extensions read the new buyer.
/checkout?customer=<email>opens the checkout logged in, and the home page lists the customers. The demo store has a profile of each kind (new, returning, B2B, abroad). See log in as a customer. - The checkout looks like Shopify's. The standard look of the local checkout now follows Shopify's one-page checkout: its header, the two columns with the grey order summary, floating labels, grouped payment and shipping choices, the order summary with its quantity badges, and a mobile layout with a folded summary. The checkout UI extensions use the same design tokens (
tokens.css), so they look native in it. See the look of the page. lsf app function run [handle]... --app <dir>runs the Functions once, without a server, and prints each input, output and log with the checkout that results.--sessionand--checkoutset the cart and what the buyer entered,--format jsonis for tools. It exits with1when a Function fails to run.- A
checkoutsection in the store data sets the shipping rates, the payment methods, the discounts that use the Functions of the app, the settings of its other Functions and a tax rate. Without it: one "Standard" rate at 5.00, three payment methods and no tax. - The checkout in the control API.
/checkouts/cn/<token>serves the page,.jsonits state,/updatetakes operations and/completeplaces the order, which/thank-youshows.GET /__lsf/checkout?session=<id>reads the state of a session for tests. Without--app,/checkoutis the summary of the cart, as before. - The Functions of the checkout can read the fields they could not. An input query that asks for
Cart.deliverableLines, the discount applications and allocations of the cart, its lines and its delivery groups (each Function sees the discounts the steps before it applied),PurchasingCompany.contact, the dates, locale and totals of companies and their locations,MailingAddress.market,PaymentCustomizationPaymentMethod.placementsorShop.metaobjectnow runs instead of failing with the field's path. The store data sets them:created_at,updated_at,locale,orders_countandtotal_spenton companies and locations, acompany_contacton a customer,localization.markets,placementson a payment method; everything has a default. Metaobjects are the ones of the data. - Network access for Functions. The
.fetchtargets of the lines discounts, delivery discounts and cart validation Functions run: the checkout sends the HTTP request they ask for and gives the response to the.runtarget asfetchResult, with Shopify's limits (GET and POST, HTTPS, a timeout of 100 to 2000 ms, 100 KB, no redirect;httpis accepted forlocalhost). The request and the response show in the Functions dialog and inlsf app function run. - A deferred payment in the checkout. A line sold with a selling plan that takes a deposit (a pre-order, try before you buy:
checkout_chargein the store data) is no longer charged in full. The order summary shows the total, what is due today and what is due later; the state (.json) hascost.dueNow,cost.dueLaterandcost.checkoutChargeAmount(thecart.checkout_charge_amountof the storefront cart) anddueLateron the lines; the order recordstotalReceivedandtotalOutstanding, and the thank-you page shows the remaining balance.lsf app function runprints it. Shipping is due today and tax follows the amount it is on. Theshopifyglobal and the Function input do not change: Shopify exposes no deferred amount there. - A home page for the checkout.
GET /oflsf app dev(and/__lsf/checkouts, whichlsf serve --appprints when it starts, as the theme owns/) lists every Function of the app (API, targets, build status, network access with a.fetchtarget, what is wrong with it) and every checkout UI extension (API version, capabilities, targets, and the place each target has on the page, or that it has none), and a card for every checkout scenario, with a link that opens a checkout for it.?format=jsonanswers what it says. - Checkout scenarios. A file of
checkouts/in the store data (checkouts/<name>.json) is a named checkout setup, on top of the store data: the cart (lines, quantities, selling plans, note, attributes), the buyer (a guest, a customer, a company location), the country and language, what the buyer entered (email, address, discount codes, delivery, payment), overrides of thecheckoutsection, the look and the extensions that run./checkout?scenario=<name>opens a fresh checkout for it, in a new session. Scenarios are validated (validate, with the codes of the...
v0.1.18
Changed
- Security update of the dependencies. 33 crates are updated, among them
brotli9,jsonschema0.59 andbase640.23. Nothing changes in whatlsfdoes. - The release binaries are built with Rust 1.99.1, up from 1.98.1.
- The npm packages are built and published with Node 24.22.1. The release and npm workflows pin it, and the development toolchain pins Node 26.12.1.
What's Changed
- chore(deps): security update – Rust 1.99.1, Node 26.12.1/24.22.1, 33 crates by @0xtlt in #9
- chore(release): v0.1.18 by @0xtlt in #10
Full Changelog: v0.1.17...v0.1.18
v0.1.17
Added
- The pages of metaobjects are served. With
templates/metaobject/<type>.json(or.liquid) in the theme,/pages/<type>/<handle>renders the entry, with the globalmetaobject,?view=and a per-entrytemplate_suffixin the data.shopify://metaobjects/links lead there, andmetaobject.system.urlis nil for a type without a template, as on a store whose definition has no web pages. - A 404 page says why. The server answers it with an
x-lsf-not-foundheader (no route, unknown resource, no template) and logs the reason, so a test can fail on it. shopify_asset_urlfiles are served.vendor/qrcode.jsis the MIT qrcode.js library that Shopify serves, so a gift card draws its QR code. The other documented files are small local stand-ins:shopify_common.jsformats money, the others only warn in the console.
Fixed
metaobjectandmetaobject_listsettings are metaobjects, not the string of the template: a handle,type.handle,type/handleor agid://shopify/Metaobject/id.- The search follows
sort_by:relevance,price-ascendingandprice-descending, with the other results after the products under a price order, as Shopify does.search.sort_bysays which order applies andsearch.sort_optionslists those three. 'now' | date: '%N'differs between calls. Each'now'reads the clock again, as on Shopify, so ids built from it no longer repeat. With a fixednowin the data, each read is one nanosecond after the previous one: renders stay reproducible.
What's Changed
Full Changelog: v0.1.16...v0.1.17
v0.1.16
Added
-
npx local-storefront checkfinds what Shopify's strict Liquid parser rejects, andnpx local-storefront check --fixrewrites it. Shopify is moving every store to a strict parser, and a file it rejects can no longer be uploaded (Expected end_of_string but found pipe).npx local-storefront checkreports each tag and output it rejects, with Shopify's message and the fix, in.liquidfiles and in theliquidsettings of JSON templates, section groups andconfig/settings_data.json.- Where what the code meant is clear,
--fixdoes it, and the report says how the output changes: a filter in arenderargument is computed into a variable on the line above, anandin an argument is evaluated,('X' | downcase)is the string and not the variableX,reversedafterlimitreverses,=in a condition compares, and the body of{% when 'x' and y %}goes in{% if y %}(acasewith anelsebecomesif/elsif/else). - Elsewhere it keeps today's output, as Shopify's own migration does, without the comment it leaves in the file.
- A fix keeps the layout of the markup and is checked before it is written. Markup without a fix is reported with the reason.
--format jsonprints one JSON document, for tools and AI agents: each problem with its code, file, line, markup and fix.
See strict Liquid.
- Where what the code meant is clear,
Changed
- The documentation is organised by task. The README is short: what local-storefront does, how to start, and a link per task. Each task has its guide in
docs/(data, testing, strict Liquid, formatting, performance, storefront), next to two references: the CLI, with every option, exit code and output format, and the control API. docs/agents.md tells an AI agent how to drive local-storefront, andllms.txtlists the documentation for language models. - Messages name the subcommand to run (
the init command,run again with --fix) rather thanlsf, which is only the command of the installs that are not from npm.
What's Changed
Full Changelog: v0.1.15...v0.1.16
v0.1.15
Added
-
lsf fmtformats the Liquid files of a theme the way Prettier does with Shopify's Liquid plugin, without Node: Liquid, HTML, the JSON of{% schema %}, the CSS of<style>,{% style %}and{% stylesheet %}, and the JavaScript of<script>and{% javascript %}.lsf fmt --checklists the files that are not formatted and fails if there are any, for CI. The 283 files of Shopify's Horizon theme are checked in 0.08 s, where Prettier takes 1.6 s.- It reads the configuration Prettier reads:
.prettierrc,.editorconfigand.prettierignore.--option printWidth=100sets an option from the command line, and--stdin(with--stdin-filepath) formats what an editor sends. Prettier's options for JavaScript (semi,trailingComma,arrowParens, ...) apply too. - Files named one by one, as a commit hook names them, are formatted when they are
.liquidfiles that.prettierignoredoes not name. A file is replaced in one step, with its permissions. A file that cannot be formatted is reported, and the others are formatted all the same.
- It reads the configuration Prettier reads:
-
Feature previews.
--preview <name>(orLSF_PREVIEW) turns on a feature preview of Shopify, as a development store can. Without it, a page renders as on a live store: a theme that uses a preview gets the error a live store gives, andlsfnames the preview to turn on. The first one isliquid-july-26, Shopify's "Liquid July '26 changes":{% block 'name', parameter: value %}content{% endblock %}rendersblocks/name.liquidfrom a template or a layout. The block sees its parameters and its body, ascontentorblock.content; a parameter named after a setting of its schema setsblock.settings; arrays can be written in place. With it, Shopify's Skeleton theme renders.{% partial 'name' %}and@shopify/partial-rendering: a theme that uses partials gets an import map for the module, andpartials.refresh('name')swaps in the region from the server, keeping focus, form values and scroll, in loops and table rows too. Shopify has not published its own markup yet: lsf marks a partial with HTML comments.
Fixed
- A
file_referencemetafield or metaobject field that points to a video (.mp4,.mov,.webm,.m4v) is a video, as on Shopify:sources,preview_imageandvideo_tagwork, andmetafield_tagprints a<video>as Shopify does. It was a plain file with only aurl, so themes rendered it empty. The poster is the.jpgof the same name. A.movis announced asvideo/mp4, as Shopify announces the MP4 it makes of it, so that Chrome and Firefox play it; thevideosetting does the same. - Files of
files/are served in parts when the browser asks with aRangeheader. Safari played no video without it, and no browser could seek in one. Only the part asked for is read, and a part is never compressed..m4vfiles are served asvideo/mp4. - Metafield values are the objects Shopify gives for four more types. A
coloris a color (.red,.rgb...), not text. Anarticle_referenceis the article, namedblog/articleor by the handle of an article that only one blog has. Every measurement type (distance,temperature,duration...) has atype, avalueand aunit, andmetafield_tagprints it with two decimals at most, where onlyweight,volumeanddimensionwere printed. Amoneyhas thecurrencyit is written in, and is formatted in it:€5,00 EURin a store that sells in dollars. request.locale.direction(and that of every locale) isrtlfor Arabic, Hebrew, Persian and the other languages written right to left, and follows the script when the locale names one (pa-Arab). Horizon setsdiron<html>from it.- The
applicable_disclosuresfilter keeps every disclosure, as Shopify does when it cannot tell the buyer's country. It was an unknown filter that printed a warning.
What's Changed
New Contributors
Full Changelog: v0.1.14...v0.1.15
v0.1.14
Fixed
lsf servegives back the memory it used to encode images. A server left running for three hours on a large theme held 1 GB. After every image of that theme is asked for, 277 MB are left where 664 MB were, and pages render as fast as before.
Full Changelog: v0.1.13...v0.1.14
v0.1.13
Added
Server-Timingon every response, as on a Shopify storefront:processingfor the whole, thenrender,template,layout,compressandthrottle.lsf serve --timingsalso names every section and every theme block.lsf profile <path>renders one page and prints what rendered what: the template, the layout, each section, its blocks and their snippets, with the time of each.--linesgoes down to the tag.lsf profile --pointscounts what the page asks of a store instead of the time it takes here: a product, a metafield, a search. A local server reads a metafield as fast as a title, Shopify does not.--costchanges what each kind of thing costs.- Flame graphs.
/__lsf/profile?path=<path>&html=1opens the profile in speedscope, the viewer thatshopify theme profileopens. It is embedded: nothing is fetched. /__lsf/timingsadds up the renders so far by template, section and block.
Full Changelog: v0.1.12...v0.1.13
v0.1.12
The responses of lsf serve now weigh what a Shopify storefront sends, so that the sizes a browser or an audit reports are those of the real store.
Added
- Minified stylesheets and scripts, by the rules Shopify follows, with source maps: the tools of a browser still show the files as written.
--no-minifyserves them as they are. - Images in the lightest format the browser reads. AVIF, WebP or the format of the file, chosen from the
Acceptheader as Shopify's image CDN does. Cache-Controlas Shopify sends it for each kind of response: pages, assets, images, fonts, the cart.- The resource hints Shopify adds. The
Linkheader preloads the stylesheets and the scripts that block rendering, and connects to the origins they come from.
Fixed
- The
Linkheader ofpreload: trueandpreload_tagis written as Shopify writes it, one header with quoted values. A browser could not read the previous form for an image with asrcset. - A font preloaded with
preload_taggetscrossorigin, without which a browser does not use it. - The
?v=of a.liquidasset changes with the theme settings it is rendered with, not only with its source. - A placeholder image served from the cache says that it is one (
x-lsf-placeholder).
Full Changelog: v0.1.11...v0.1.12
v0.1.11
Added
- Responses are compressed with Brotli or gzip for the clients that accept it, as on a storefront. A 420 KB collection page of Horizon becomes 53 KB.
--no-compressionsends responses as they are.
Fixed
{{-and{%-trim whitespace the way storefronts do, which is not quite what the language says: when the text before one is nothing but whitespace, its first character stays. Line breaks in a<title>or between two outputs now match Shopify's.- The rules of
robots.txtare on their own lines, those a theme adds with{{ 'Disallow: ...' }}included.
Full Changelog: v0.1.10...v0.1.11
v0.1.10
Changed
--live-reloadgoes through a WebSocket. A page reloads about 100 ms after a save, where it asked the server every 700 ms. It still does when the socket cannot open, behind a proxy for example.- A page survives a restart of the server, and reloads if the files changed meanwhile.
- A save or a build that writes several files reloads the pages once.
- Only the directories a theme is made of and the data directory are watched, which is much faster for a theme that keeps
node_modulesnext to its sources. A data directory outside the theme (--data) now reloads the pages. A file outside those directories no longer does.
Full Changelog: v0.1.9...v0.1.10