Skip to content

Releases: 0xtlt/local-storefront

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 30 Oct 13:15
7969758

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, and lsf serve --app <dir> leads the /checkout of 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, --build runs 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, /account is 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 the shopify global 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 its returns. See customer account.
  • The commands of a Shopify app are under lsf app, named as Shopify's CLI names them: app dev serves the app alone (its checkout and its customer account), app init writes checkout scenarios, app build builds its extensions, app check runs the scenarios for CI, app function run and app function build its Functions. CLI reference.
  • Testing an app's checkout in CI. lsf app check runs the Functions of every scenario and exits with 1 when one fails or does not validate, or when there is no scenario. lsf validate checks 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 set data-lsf-ready on the document once nothing is on its way and every extension rendered (aria-busy meanwhile). DELETE /__lsf/sessions resets 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 its now: a test reads the same number and date every time. Testing guide.
  • Commands of an app need no --app in its folder (lsf app dev, lsf app init, lsf app function run, lsf app check), and read its shopify-local/ from anywhere. lsf app init gives 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 in app-settings.json, so that the scenarios it writes pass app 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 run says 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 --session takes data as PUT /__lsf/session does: a discount or settings for one run.
  • Watch mode for the checkout of an app. lsf app dev --app <dir> --watch (and serve --app <dir> --watch) builds a Function again when its files (or the files the Functions share, such as a Cargo workspace's Cargo.toml) change, and reloads the open checkout, thank-you and home pages once the builds are over, as shopify app dev does. 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.toml and 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. --session and --checkout set the cart and what the buyer entered, --format json is for tools. It exits with 1 when a Function fails to run.
  • A checkout section 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, .json its state, /update takes operations and /complete places the order, which /thank-you shows. GET /__lsf/checkout?session=<id> reads the state of a session for tests. Without --app, /checkout is 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.placements or Shop.metaobject now runs instead of failing with the field's path. The store data sets them: created_at, updated_at, locale, orders_count and total_spent on companies and locations, a company_contact on a customer, localization.markets, placements on a payment method; everything has a default. Metaobjects are the ones of the data.
  • Network access for Functions. The .fetch targets 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 .run target as fetchResult, with Shopify's limits (GET and POST, HTTPS, a timeout of 100 to 2000 ms, 100 KB, no redirect; http is accepted for localhost). The request and the response show in the Functions dialog and in lsf 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_charge in 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) has cost.dueNow, cost.dueLater and cost.checkoutChargeAmount (the cart.checkout_charge_amount of the storefront cart) and dueLater on the lines; the order records totalReceived and totalOutstanding, and the thank-you page shows the remaining balance. lsf app function run prints it. Shipping is due today and tax follows the amount it is on. The shopify global and the Function input do not change: Shopify exposes no deferred amount there.
  • A home page for the checkout. GET / of lsf app dev (and /__lsf/checkouts, which lsf serve --app prints when it starts, as the theme owns /) lists every Function of the app (API, targets, build status, network access with a .fetch target, 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=json answers 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 the checkout section, 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...
Read more

v0.1.18

Choose a tag to compare

@github-actions github-actions released this 21 Oct 16:04
6657294

Changed

  • Security update of the dependencies. 33 crates are updated, among them brotli 9, jsonschema 0.59 and base64 0.23. Nothing changes in what lsf does.
  • 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

Choose a tag to compare

@github-actions github-actions released this 20 Oct 16:04

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 global metaobject, ?view= and a per-entry template_suffix in the data. shopify://metaobjects/ links lead there, and metaobject.system.url is 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-found header (no route, unknown resource, no template) and logs the reason, so a test can fail on it.
  • shopify_asset_url files are served. vendor/qrcode.js is 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.js formats money, the others only warn in the console.

Fixed

  • metaobject and metaobject_list settings are metaobjects, not the string of the template: a handle, type.handle, type/handle or a gid://shopify/Metaobject/ id.
  • The search follows sort_by: relevance, price-ascending and price-descending, with the other results after the products under a price order, as Shopify does. search.sort_by says which order applies and search.sort_options lists 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 fixed now in the data, each read is one nanosecond after the previous one: renders stay reproducible.

What's Changed

  • fix: lift the limits of 0.1.16 found on a real theme by @0xtlt in #8

Full Changelog: v0.1.16...v0.1.17

v0.1.16

Choose a tag to compare

@github-actions github-actions released this 14 Oct 11:00

Added

  • npx local-storefront check finds what Shopify's strict Liquid parser rejects, and npx local-storefront check --fix rewrites 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 check reports each tag and output it rejects, with Shopify's message and the fix, in .liquid files and in the liquid settings of JSON templates, section groups and config/settings_data.json.

    • Where what the code meant is clear, --fix does it, and the report says how the output changes: a filter in a render argument is computed into a variable on the line above, an and in an argument is evaluated, ('X' | downcase) is the string and not the variable X, reversed after limit reverses, = in a condition compares, and the body of {% when 'x' and y %} goes in {% if y %} (a case with an else becomes if/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 json prints one JSON document, for tools and AI agents: each problem with its code, file, line, markup and fix.

    See strict Liquid.

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, and llms.txt lists the documentation for language models.
  • Messages name the subcommand to run (the init command, run again with --fix) rather than lsf, which is only the command of the installs that are not from npm.

What's Changed

  • feat(check): find and fix what Shopify's strict Liquid parser rejects by @0xtlt in #7

Full Changelog: v0.1.15...v0.1.16

v0.1.15

Choose a tag to compare

@github-actions github-actions released this 14 Oct 07:57

Added

  • lsf fmt formats 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 --check lists 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, .editorconfig and .prettierignore. --option printWidth=100 sets 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 .liquid files that .prettierignore does 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.

    See what differs from Prettier.

  • Feature previews. --preview <name> (or LSF_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, and lsf names the preview to turn on. The first one is liquid-july-26, Shopify's "Liquid July '26 changes":

    • {% block 'name', parameter: value %}content{% endblock %} renders blocks/name.liquid from a template or a layout. The block sees its parameters and its body, as content or block.content; a parameter named after a setting of its schema sets block.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, and partials.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_reference metafield or metaobject field that points to a video (.mp4, .mov, .webm, .m4v) is a video, as on Shopify: sources, preview_image and video_tag work, and metafield_tag prints a <video> as Shopify does. It was a plain file with only a url, so themes rendered it empty. The poster is the .jpg of the same name. A .mov is announced as video/mp4, as Shopify announces the MP4 it makes of it, so that Chrome and Firefox play it; the video setting does the same.
  • Files of files/ are served in parts when the browser asks with a Range header. 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. .m4v files are served as video/mp4.
  • Metafield values are the objects Shopify gives for four more types. A color is a color (.red, .rgb...), not text. An article_reference is the article, named blog/article or by the handle of an article that only one blog has. Every measurement type (distance, temperature, duration...) has a type, a value and a unit, and metafield_tag prints it with two decimals at most, where only weight, volume and dimension were printed. A money has the currency it is written in, and is formatted in it: €5,00 EUR in a store that sells in dollars.
  • request.locale.direction (and that of every locale) is rtl for Arabic, Hebrew, Persian and the other languages written right to left, and follows the script when the locale names one (pa-Arab). Horizon sets dir on <html> from it.
  • The applicable_disclosures filter 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

  • lsf fmt, feature previews (block, partial), media and metafield fixes by @0xtlt in #5

New Contributors

  • @0xtlt made their first contribution in #5

Full Changelog: v0.1.14...v0.1.15

v0.1.14

Choose a tag to compare

@github-actions github-actions released this 06 Oct 13:31

Fixed

  • lsf serve gives 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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 08:27

Added

  • Server-Timing on every response, as on a Shopify storefront: processing for the whole, then render, template, layout, compress and throttle. lsf serve --timings also 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. --lines goes down to the tag.
  • lsf profile --points counts 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. --cost changes what each kind of thing costs.
  • Flame graphs. /__lsf/profile?path=<path>&html=1 opens the profile in speedscope, the viewer that shopify theme profile opens. It is embedded: nothing is fetched.
  • /__lsf/timings adds up the renders so far by template, section and block.

Full Changelog: v0.1.12...v0.1.13

v0.1.12

Choose a tag to compare

@github-actions github-actions released this 04 Oct 07:51

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-minify serves them as they are.
  • Images in the lightest format the browser reads. AVIF, WebP or the format of the file, chosen from the Accept header as Shopify's image CDN does.
  • Cache-Control as Shopify sends it for each kind of response: pages, assets, images, fonts, the cart.
  • The resource hints Shopify adds. The Link header preloads the stylesheets and the scripts that block rendering, and connects to the origins they come from.

Fixed

  • The Link header of preload: true and preload_tag is written as Shopify writes it, one header with quoted values. A browser could not read the previous form for an image with a srcset.
  • A font preloaded with preload_tag gets crossorigin, without which a browser does not use it.
  • The ?v= of a .liquid asset 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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 17:53

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-compression sends 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.txt are on their own lines, those a theme adds with {{ 'Disallow: ...' }} included.

Full Changelog: v0.1.10...v0.1.11

v0.1.10

Choose a tag to compare

@github-actions github-actions released this 03 Oct 16:55

Changed

  • --live-reload goes 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_modules next 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