vite-plugin-shopify-liquid-imports
v0.2.6
Published
Post-build companion to vite-plugin-shopify that enables native-ESM code splitting on Shopify themes via a classic bootstrap and Blob module graph — no import maps or .js.liquid chunk rewriting.
Maintainers
Readme
vite-plugin-shopify-liquid-imports
Post-build companion to vite-plugin-shopify that enables native-ESM code splitting on Shopify themes without import maps or .js.liquid chunk rewriting.
It emits a Liquid JSON manifest of Shopify asset_urls, a tiny classic bootstrap (*loader.min.js), and rewrites chunk graphs so modules load through Blob URLs with stable marker substitution.
Why
Shopify themes can't ship a normal hashed-filename ESM chunk graph — chunk URLs must go through asset_url. Import maps are the usual workaround, but they have weak browser support and conflict when multiple maps exist in one theme.
This plugin takes a different path:
- Liquid emits a JSON manifest of CDN URLs (Shopify's own
?v=), then a classic bootstrap script. - The bootstrap fetches module sources, rewrites controlled static-import markers to Blob URLs, and
import()s the entry Blob. - Dynamic
import()calls become namespaced loader imports, so lazy chunks stay lazy.
Browser floor: dynamic import() + Blob/createObjectURL (~Chrome 63 / Safari 11.1 / Firefox 67). Storefront CSP must allow script-src blob: and CDN connect-src.
Install
npm i -D vite-plugin-shopify-liquid-importsPeers: vite >= 8, vite-plugin-shopify >= 4.
Usage
Register after vite-plugin-shopify:
import { defineConfig } from 'vite'
import shopify from 'vite-plugin-shopify'
import liquidImports from 'vite-plugin-shopify-liquid-imports'
export default defineConfig({
plugins: [
shopify({
themeRoot: '../',
snippetFile: 'vite-tag.liquid',
versionNumbers: true,
}),
liquidImports({
namespace: 'my-app',
assetPrefix: 'MA_',
snippetFile: 'vite-tag.liquid', // must match shopify()
versionNumbers: true, // must match shopify()
entrypointsDir: './entries', // must match shopify() when non-default
prefetchSnippetFile: 'vite-tag-preload.liquid', // optional
}),
],
})The plugin forces production modulePreload: false, sourcemap: false, and stable [name].min.js / [name].min.css entry filenames under assetPrefix (non-CSS assets like fonts/images keep a content hash).
In production it regenerates the shared snippetFile from the Vite manifest: JS entries become Blob-loader branches (JSON manifest + classic bootstrap); CSS and other non-JS entries keep stylesheet_tag / shopify-style tags. No consumer-side CSS patch plugin is required.
Options
| Option | Required | Description |
|--------|----------|-------------|
| namespace | yes | Isolates runtime Symbol.for key, markers, and reload storage. Must match /^[a-z][a-z0-9-]*$/. |
| assetPrefix | yes | Filename prefix for all emitted JS/CSS assets (e.g. HC_). Must match /^[A-Za-z][A-Za-z0-9_-]*$/. |
| snippetFile | yes | Shared Liquid snippet with vite-plugin-shopify. Must match shopify({ snippetFile }). |
| versionNumbers | no | Match shopify({ versionNumbers }). Default true. |
| entrypointsDir | no | Match shopify({ entrypointsDir }) for CSS path aliases. Default entries. |
| prefetchSnippetFile | no | Section-gated prefetch helper snippet. |
Bootstrap loading (JS entries)
Per-render params control how the classic *loader.min.js bootstrap is requested. They apply only to JS entry branches (CSS branches ignore them).
| Param | Values | Default |
|-------|--------|---------|
| script_loading | defer | async | blocking | defer |
| fetchpriority | high | low | auto | omit attribute |
{% comment %} Secondary UI (cart drawers, widgets) — default {% endcomment %}
{% render 'vite-tag' with 'ns-hybrid-cart.tsx' %}
{% comment %} Main-page / LCP content — start the bootstrap earlier {% endcomment %}
{% render 'vite-tag',
entry: 'naked-quiz-v2.tsx',
script_loading: 'async',
fetchpriority: 'high'
%}Legacy {% render 'vite-tag' with 'entry.tsx' %} still works and defaults to defer with no fetchpriority. Unknown script_loading values are coerced to defer (storefront-safe).
Guidance
- Secondary widgets / cart:
defer(default). - LCP / main content apps: prefer
async+fetchpriority: 'high'. Useblockingonly when the tag is intentionally early in the parse and you want the bootstrap file itself parser-blocking. - These params only affect the tiny classic bootstrap. The module graph still loads asynchronously via
fetch+import(blob).blockingdoes not make the full app render-blocking.
Section-gated prefetch
When prefetchSnippetFile is set, sections can warm a lazy chunk's source fetch without evaluating it:
{% render 'vite-tag-preload', chunk: 'PreCartUpsells' %}chunk is the Vite [name] (e.g. PreCartUpsells). Prefetch shares the loader's source-fetch map so a later import does not download twice.
What it does
| Hook | Action |
|------|--------|
| config | Forces modulePreload: false, sourcemap: false, and prefixed stable chunk names. |
| generateBundle | Rewrites static/dynamic imports to loader markers/calls, stamps modules, emits *loader.min.js. |
| closeBundle | Regenerates the shared vite-tag from the Vite manifest (JS → Blob loader, CSS → stylesheet_tag) and optional prefetch helper. |
Stable module identity
Shopify assigns a volatile ?v= to assets. The Liquid manifest embeds each module's asset_url once per page render, and every module source is stamped with /* shopify-module-loader:<namespace>:<buildId> */. A mismatch triggers one guarded reload to heal mixed deployments.
Caveats
- Place after
shopify()— productioncloseBundleregenerates the shared vite-tag; register this plugin after so it wins the final write. - Shared vite-tag — only the JS entry loading path is replaced (no
type=module/modulepreloadfor managed chunks). CSS entries and other non-JS tags from the manifest are preserved. Do not append CSS with a local post-plugin. - No production source maps — Blob marker substitution invalidates them.
- Chunk-scoped CSS is unsupported — use a CSS entry,
cssCodeSplit: false, or?inline. The build fails clearly if a JS chunk imports CSS. - CSP: allow
script-src blob:and CDNconnect-src. - Stable asset names are not atomic across Shopify uploads; build stamps + one guarded reload mitigate mixed deployments.
Breaking change (0.2)
0.1.x rewrote chunks to .js.liquid with inline asset_url imports. 0.2.0 replaces that approach entirely with the bootstrap + Blob loader. Options and output format are incompatible — migrate call sites to the options above.
License
MIT
