From 870216f7098a7431050cc5df63116e89841f347f Mon Sep 17 00:00:00 2001 From: Miguel Montalvo Date: Sun, 15 Feb 2026 20:49:26 -0600 Subject: [PATCH 1/4] Output the URL for a given entry --- examples/vite-shopify-example/.gitignore | 1 + .../vite-shopify-example/layout/theme.liquid | 3 +- examples/vite-shopify-example/vite.config.ts | 1 + packages/vite-plugin-shopify/src/constants.ts | 2 + packages/vite-plugin-shopify/src/html.ts | 50 +++++++++++++++++-- packages/vite-plugin-shopify/src/options.ts | 2 + packages/vite-plugin-shopify/src/types.ts | 7 +++ 7 files changed, 62 insertions(+), 4 deletions(-) diff --git a/examples/vite-shopify-example/.gitignore b/examples/vite-shopify-example/.gitignore index b25ba532..696daa29 100644 --- a/examples/vite-shopify-example/.gitignore +++ b/examples/vite-shopify-example/.gitignore @@ -1,2 +1,3 @@ /assets /snippets/vite.liquid +/snippets/vite-asset.liquid diff --git a/examples/vite-shopify-example/layout/theme.liquid b/examples/vite-shopify-example/layout/theme.liquid index 2c32d035..dfb79eab 100755 --- a/examples/vite-shopify-example/layout/theme.liquid +++ b/examples/vite-shopify-example/layout/theme.liquid @@ -11,8 +11,9 @@ + {%- liquid - render 'vite' with 'theme.scss', preload_stylesheet: true + render 'vite' with 'theme.scss' render 'vite' with 'theme.ts' render 'vite' with '@/foo.ts' render 'vite' with '~/bar.ts' diff --git a/examples/vite-shopify-example/vite.config.ts b/examples/vite-shopify-example/vite.config.ts index 92d5bd52..0e984808 100644 --- a/examples/vite-shopify-example/vite.config.ts +++ b/examples/vite-shopify-example/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ shopify({ tunnel: true, snippetFile: 'vite.liquid', + snippetAssetFile: 'vite-asset.liquid', additionalEntrypoints: [ 'frontend/foo.ts', // relative to sourceCodeDir 'frontend/bar.ts', diff --git a/packages/vite-plugin-shopify/src/constants.ts b/packages/vite-plugin-shopify/src/constants.ts index 19f58d58..288d0e0d 100644 --- a/packages/vite-plugin-shopify/src/constants.ts +++ b/packages/vite-plugin-shopify/src/constants.ts @@ -17,3 +17,5 @@ export const CSS_EXTENSIONS_REGEX = new RegExp( export const hotReloadScriptId = 'hot-reload-client' export const hotReloadScriptUrl = 'https://cdn.jsdelivr.net/npm/@shopify/theme-hot-reload/dist/theme-hot-reload.min.js' + +export const snippetAssetFile = 'vite-asset.liquid' diff --git a/packages/vite-plugin-shopify/src/html.ts b/packages/vite-plugin-shopify/src/html.ts index 6d798ea9..9a76b237 100644 --- a/packages/vite-plugin-shopify/src/html.ts +++ b/packages/vite-plugin-shopify/src/html.ts @@ -6,7 +6,7 @@ import createDebugger from 'debug' import startTunnel from '@shopify/plugin-cloudflare/hooks/tunnel' import { renderInfo, isTTY } from '@shopify/cli-kit/node/ui' -import { CSS_EXTENSIONS_REGEX, KNOWN_CSS_EXTENSIONS, hotReloadScriptId, hotReloadScriptUrl } from './constants' +import { CSS_EXTENSIONS_REGEX, KNOWN_CSS_EXTENSIONS, hotReloadScriptId, hotReloadScriptUrl, snippetAssetFile } from './constants' import type { Options, DevServerUrl, FrontendURLResult } from './types' import type { TunnelClient } from '@shopify/cli-kit/node/plugins/tunnel' @@ -21,8 +21,20 @@ export default function shopifyHTML (options: Required): Plugin { const viteTagSnippetPath = path.resolve(options.themeRoot, `snippets/${options.snippetFile}`) const viteTagSnippetName = options.snippetFile.replace(/\.[^.]+$/, '') - const viteTagSnippetPrefix = (config: ResolvedConfig): string => - viteTagDisclaimer + viteTagEntryPath(config.resolve.alias, options.entrypointsDir, viteTagSnippetName) + const viteTagSnippetPrefix = (config: ResolvedConfig, snippetName = viteTagSnippetName): string => + viteTagDisclaimer + viteTagEntryPath(config.resolve.alias, options.entrypointsDir, snippetName) + + const resolvedSnippetAssetFile = typeof options.snippetAssetFile === 'string' + ? options.snippetAssetFile + : options.snippetAssetFile + ? snippetAssetFile + : '' + const viteAssetSnippetPath = resolvedSnippetAssetFile + ? path.resolve(options.themeRoot, `snippets/${resolvedSnippetAssetFile}`) + : '' + const viteAssetSnippetName = resolvedSnippetAssetFile + ? path.parse(resolvedSnippetAssetFile).name + : '' return { name: 'vite-plugin-shopify-html', @@ -76,8 +88,14 @@ export default function shopifyHTML (options: Required): Plugin { tunnelUrl, options.entrypointsDir, reactPlugin, options.themeHotReload ) + const viteAssetSnippetContent = viteAssetSnippetName && (viteTagSnippetPrefix(config, viteAssetSnippetName) + viteAssetSnippetDev( + tunnelUrl, options.entrypointsDir + )) + // Write vite-tag with a Cloudflare Tunnel URL fs.writeFileSync(viteTagSnippetPath, viteTagSnippetContent) + + viteAssetSnippetContent && fs.writeFileSync(viteAssetSnippetPath, viteAssetSnippetContent) })() }, 100) @@ -87,8 +105,14 @@ export default function shopifyHTML (options: Required): Plugin { : viteDevServerUrl, options.entrypointsDir, reactPlugin, options.themeHotReload ) + const viteAssetSnippetContent = viteAssetSnippetName && (viteTagSnippetPrefix(config, viteAssetSnippetName) + viteAssetSnippetDev( + frontendUrl !== '' ? frontendUrl : viteDevServerUrl, options.entrypointsDir + )) + // Write vite-tag snippet for development server fs.writeFileSync(viteTagSnippetPath, viteTagSnippetContent) + + viteAssetSnippetContent && fs.writeFileSync(viteAssetSnippetPath, viteAssetSnippetContent) } }) @@ -125,6 +149,7 @@ export default function shopifyHTML (options: Required): Plugin { } const assetTags: string[] = [] + const assetUrls: string[] = [] const manifest = JSON.parse( fs.readFileSync(manifestFilePath, 'utf8') ) as Manifest @@ -174,18 +199,24 @@ export default function shopifyHTML (options: Required): Plugin { } assetTags.push(viteEntryTag(entryPaths, tagsForEntry.join('\n '), assetTags.length === 0)) + assetUrls.push(viteEntryTag(entryPaths, `{{ ${assetUrl(file, options.versionNumbers)} }}`, assetUrls.length === 0)) } // Generate entry tag for bundled "style.css" file when cssCodeSplit is false if (src === 'style.css' && !config.build.cssCodeSplit) { assetTags.push(viteEntryTag([src], stylesheetTag(file, options.versionNumbers), false)) + assetUrls.push(viteEntryTag([src], `{{ ${assetUrl(file, options.versionNumbers)} }}`, false)) } }) const viteTagSnippetContent = viteTagSnippetPrefix(config) + assetTags.join('\n') + '\n{% endif %}\n' + const viteAssetSnippetContent = viteAssetSnippetName && viteTagSnippetPrefix(config, viteAssetSnippetName) + assetUrls.join('\n') + '\n{% endif %}\n' + // Write vite-tag snippet for production build fs.writeFileSync(viteTagSnippetPath, viteTagSnippetContent) + + viteAssetSnippetContent && fs.writeFileSync(viteAssetSnippetPath, viteAssetSnippetContent) } } } @@ -245,6 +276,19 @@ const scriptTag = (fileName: string, versionNumbers: boolean): string => const stylesheetTag = (fileName: string, versionNumbers: boolean): string => `{{ ${assetUrl(fileName, versionNumbers)} | stylesheet_tag: preload: preload_stylesheet }}` +// Generate vite-asset snippet for development +const viteAssetSnippetDev = (assetHost: string, entrypointsDir: string): string => + `{% liquid + assign path_prefix = path | slice: 0 + if path_prefix == '/' + assign file_url_prefix = '${assetHost}' + else + assign file_url_prefix = '${assetHost}/${entrypointsDir}/' + endif + assign file_url = path | prepend: file_url_prefix + echo file_url +%}` + // Generate vite-tag snippet for development const viteTagSnippetDev = (assetHost: string, entrypointsDir: string, reactPlugin: Plugin | undefined, themeHotReload: boolean): string => `{% liquid diff --git a/packages/vite-plugin-shopify/src/options.ts b/packages/vite-plugin-shopify/src/options.ts index 7786caab..911fe5b5 100644 --- a/packages/vite-plugin-shopify/src/options.ts +++ b/packages/vite-plugin-shopify/src/options.ts @@ -10,6 +10,7 @@ export const resolveOptions = ( const entrypointsDir = options.entrypointsDir ?? normalizePath(path.join(sourceCodeDir, 'entrypoints')) const additionalEntrypoints = options.additionalEntrypoints ?? [] const snippetFile = options.snippetFile ?? 'vite-tag.liquid' + const snippetAssetFile = options.snippetAssetFile ?? false const versionNumbers = options.versionNumbers ?? false const tunnel = options.tunnel ?? false const themeHotReload = options.themeHotReload ?? true @@ -20,6 +21,7 @@ export const resolveOptions = ( entrypointsDir, additionalEntrypoints, snippetFile, + snippetAssetFile, versionNumbers, tunnel, themeHotReload diff --git a/packages/vite-plugin-shopify/src/types.ts b/packages/vite-plugin-shopify/src/types.ts index 181c23a9..3b8d1640 100644 --- a/packages/vite-plugin-shopify/src/types.ts +++ b/packages/vite-plugin-shopify/src/types.ts @@ -34,6 +34,13 @@ export interface Options { */ snippetFile?: string + /** + * This snippet outputs the URL for the given entrypoint. + * + * @default false + */ + snippetAssetFile?: boolean | string + /** * Specifies whether to append version numbers to your production-ready asset URLs in {@link snippetFile}. * From 71f80122604a3aec5f854144ffc41eea50bd2097 Mon Sep 17 00:00:00 2001 From: Miguel Montalvo Date: Sun, 15 Feb 2026 21:08:22 -0600 Subject: [PATCH 2/4] Tests for the viteAssetFile feature --- .../__fixtures__/snippets/vite-asset.liquid | 15 ++++++ .../test/__snapshots__/html.test.ts.snap | 21 +++++++++ .../test/__snapshots__/index.test.ts.snap | 38 +++++++++++++++ .../vite-plugin-shopify/test/config.test.ts | 5 +- .../vite-plugin-shopify/test/html.test.ts | 27 +++++++++++ .../vite-plugin-shopify/test/index.test.ts | 47 +++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 packages/vite-plugin-shopify/test/__fixtures__/snippets/vite-asset.liquid diff --git a/packages/vite-plugin-shopify/test/__fixtures__/snippets/vite-asset.liquid b/packages/vite-plugin-shopify/test/__fixtures__/snippets/vite-asset.liquid new file mode 100644 index 00000000..90cf9bb6 --- /dev/null +++ b/packages/vite-plugin-shopify/test/__fixtures__/snippets/vite-asset.liquid @@ -0,0 +1,15 @@ +{% comment %} + IMPORTANT: This snippet is automatically generated by vite-plugin-shopify. + Do not attempt to modify this file directly, as any changes will be overwritten by the next build. +{% endcomment %} +{% liquid + assign entry = entry | default: vite-asset + assign path = entry | replace: '@@/', '../../resources/js/' | replace: '~/', '../' | replace: '@/', '../' +%} +{% if path == "/test/__fixtures__/frontend/entrypoints/customers.js" or path == "customers.js" %} + {{ 'customers-MM9Bv3NP.js' | asset_url }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.css" or path == "theme.css" %} + {{ 'theme-y6Yj_vm2.css' | asset_url }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.js" or path == "theme.js" %} + {{ 'theme-Df-cUSaP.js' | asset_url }} +{% endif %} diff --git a/packages/vite-plugin-shopify/test/__snapshots__/html.test.ts.snap b/packages/vite-plugin-shopify/test/__snapshots__/html.test.ts.snap index e187c17e..50dc89a4 100644 --- a/packages/vite-plugin-shopify/test/__snapshots__/html.test.ts.snap +++ b/packages/vite-plugin-shopify/test/__snapshots__/html.test.ts.snap @@ -184,3 +184,24 @@ exports[`vite-plugin-shopify:html > builds out .liquid files for development wit {% endif %} " `; + +exports[`vite-plugin-shopify:html > builds out vite-asset snippet for development 1`] = ` +"{% comment %} + IMPORTANT: This snippet is automatically generated by vite-plugin-shopify. + Do not attempt to modify this file directly, as any changes will be overwritten by the next build. +{% endcomment %} +{% liquid + assign entry = entry | default: vite-asset + assign path = entry | replace: '~/', '../' | replace: '@/', '../' | replace: '@@/', '../../resources/js/' +%} +{% liquid + assign path_prefix = path | slice: 0 + if path_prefix == '/' + assign file_url_prefix = 'http://localhost:5173' + else + assign file_url_prefix = 'http://localhost:5173/test/__fixtures__/frontend/entrypoints/' + endif + assign file_url = path | prepend: file_url_prefix + echo file_url +%}" +`; diff --git a/packages/vite-plugin-shopify/test/__snapshots__/index.test.ts.snap b/packages/vite-plugin-shopify/test/__snapshots__/index.test.ts.snap index 617268cc..38f2e219 100644 --- a/packages/vite-plugin-shopify/test/__snapshots__/index.test.ts.snap +++ b/packages/vite-plugin-shopify/test/__snapshots__/index.test.ts.snap @@ -68,3 +68,41 @@ exports[`vite-plugin-shopify > builds out .liquid files for production without m {% endif %} " `; + +exports[`vite-plugin-shopify > builds out vite-asset snippet for production 1`] = ` +"{% comment %} + IMPORTANT: This snippet is automatically generated by vite-plugin-shopify. + Do not attempt to modify this file directly, as any changes will be overwritten by the next build. +{% endcomment %} +{% liquid + assign entry = entry | default: vite-asset + assign path = entry | replace: '@@/', '../../resources/js/' | replace: '~/', '../' | replace: '@/', '../' +%} +{% if path == "/test/__fixtures__/frontend/entrypoints/customers.js" or path == "customers.js" %} + {{ 'customers-MM9Bv3NP.js' | asset_url | split: '?' | first }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.css" or path == "theme.css" %} + {{ 'theme-y6Yj_vm2.css' | asset_url | split: '?' | first }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.js" or path == "theme.js" %} + {{ 'theme-Df-cUSaP.js' | asset_url | split: '?' | first }} +{% endif %} +" +`; + +exports[`vite-plugin-shopify > builds out vite-asset snippet for production with version numbers 1`] = ` +"{% comment %} + IMPORTANT: This snippet is automatically generated by vite-plugin-shopify. + Do not attempt to modify this file directly, as any changes will be overwritten by the next build. +{% endcomment %} +{% liquid + assign entry = entry | default: vite-asset + assign path = entry | replace: '@@/', '../../resources/js/' | replace: '~/', '../' | replace: '@/', '../' +%} +{% if path == "/test/__fixtures__/frontend/entrypoints/customers.js" or path == "customers.js" %} + {{ 'customers-MM9Bv3NP.js' | asset_url }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.css" or path == "theme.css" %} + {{ 'theme-y6Yj_vm2.css' | asset_url }} +{% elsif path == "/test/__fixtures__/frontend/entrypoints/theme.js" or path == "theme.js" %} + {{ 'theme-Df-cUSaP.js' | asset_url }} +{% endif %} +" +`; diff --git a/packages/vite-plugin-shopify/test/config.test.ts b/packages/vite-plugin-shopify/test/config.test.ts index 850a8f77..b95c7d15 100644 --- a/packages/vite-plugin-shopify/test/config.test.ts +++ b/packages/vite-plugin-shopify/test/config.test.ts @@ -120,17 +120,20 @@ describe('resolveOptions', () => { expect(options.entrypointsDir).toBe('frontend/entrypoints') expect(options.additionalEntrypoints).toEqual([]) expect(options.snippetFile).toEqual('vite-tag.liquid') + expect(options.snippetAssetFile).toBe(false) }) it('accepts a partial configuration', () => { const options = resolveOptions({ themeRoot: 'shopify', - sourceCodeDir: 'src' + sourceCodeDir: 'src', + snippetAssetFile: true }) expect(options.themeRoot).toBe('shopify') expect(options.sourceCodeDir).toBe('src') expect(options.entrypointsDir).toBe('src/entrypoints') + expect(options.snippetAssetFile).toBe(true) }) }) diff --git a/packages/vite-plugin-shopify/test/html.test.ts b/packages/vite-plugin-shopify/test/html.test.ts index 19e4df78..1ea57a29 100644 --- a/packages/vite-plugin-shopify/test/html.test.ts +++ b/packages/vite-plugin-shopify/test/html.test.ts @@ -193,4 +193,31 @@ describe('vite-plugin-shopify:html', () => { vi.useRealTimers() }) + + it('builds out vite-asset snippet for development', async () => { + const options = resolveOptions({ + themeRoot: 'test/__fixtures__', + sourceCodeDir: 'test/__fixtures__/frontend', + snippetAssetFile: 'vite-asset.liquid' + }) + + const { configureServer } = html(options) + + const viteServer = await ( + await createServer({ + logLevel: 'silent', + configFile: path.join(__dirname, '__fixtures__', 'vite.config.js') + }) + ).listen() + + configureServer(viteServer) + + viteServer.httpServer?.emit('listening') + + const assetSnippet = await fs.readFile(path.join(__dirname, '__fixtures__', 'snippets', 'vite-asset.liquid'), { encoding: 'utf8' }) + + await viteServer.close() + + expect(assetSnippet).toMatchSnapshot() + }) }) diff --git a/packages/vite-plugin-shopify/test/index.test.ts b/packages/vite-plugin-shopify/test/index.test.ts index f1c00634..281ba6bf 100644 --- a/packages/vite-plugin-shopify/test/index.test.ts +++ b/packages/vite-plugin-shopify/test/index.test.ts @@ -69,4 +69,51 @@ describe('vite-plugin-shopify', () => { expect(tagsHtml).toMatchSnapshot() }) + + it('builds out vite-asset snippet for production', async () => { + await build({ + logLevel: 'silent', + plugins: [ + shopify({ + themeRoot: path.join(__dirname, '__fixtures__'), + sourceCodeDir: path.join(__dirname, '__fixtures__', 'frontend'), + snippetFile: 'vite-tag.liquid', + snippetAssetFile: 'vite-asset.liquid' + }) + ], + resolve: { + alias: { + '@@': normalizePath(path.resolve(path.join(__dirname, '__fixtures__', 'resources', 'js'))) + } + } + }) + + const assetSnippet = await fs.readFile(path.join(__dirname, '__fixtures__', 'snippets', 'vite-asset.liquid'), { encoding: 'utf8' }) + + expect(assetSnippet).toMatchSnapshot() + }) + + it('builds out vite-asset snippet for production with version numbers', async () => { + await build({ + logLevel: 'silent', + plugins: [ + shopify({ + themeRoot: path.join(__dirname, '__fixtures__'), + sourceCodeDir: path.join(__dirname, '__fixtures__', 'frontend'), + snippetFile: 'vite-tag.liquid', + snippetAssetFile: 'vite-asset.liquid', + versionNumbers: true + }) + ], + resolve: { + alias: { + '@@': normalizePath(path.resolve(path.join(__dirname, '__fixtures__', 'resources', 'js'))) + } + } + }) + + const assetSnippet = await fs.readFile(path.join(__dirname, '__fixtures__', 'snippets', 'vite-asset.liquid'), { encoding: 'utf8' }) + + expect(assetSnippet).toMatchSnapshot() + }) }) From 0b802b72c805fa6af6f9072bf31df8ded58b59ee Mon Sep 17 00:00:00 2001 From: Miguel Montalvo Date: Sun, 15 Feb 2026 21:22:36 -0600 Subject: [PATCH 3/4] Docs update --- docs/guide/configuration.md | 7 +++++++ packages/vite-plugin-shopify/README.md | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index a8946996..56eee13c 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -51,6 +51,13 @@ Additional files to use as entry points (accepts an array of file paths or glob Specifies the file name of the snippet that loads your assets. +## snippetAssetFile + +- **Type:** `boolean | string` +- **Default:** `false` + +This snippet outputs the URL for the given entrypoint. + ## versionNumbers - **Type:** `boolean` diff --git a/packages/vite-plugin-shopify/README.md b/packages/vite-plugin-shopify/README.md index df834067..52460b86 100644 --- a/packages/vite-plugin-shopify/README.md +++ b/packages/vite-plugin-shopify/README.md @@ -44,6 +44,8 @@ export default { additionalEntrypoints: [], // Specifies the file name of the snippet that loads your assets snippetFile: 'vite-tag.liquid', + // This snippet outputs the URL for the given entrypoint + snippetAssetFile: false, // Specifies whether to append version numbers to your production-ready asset URLs in `snippetFile` versionNumbers: false, // Enables the creation of Cloudflare tunnels during dev, allowing previews from any device @@ -127,6 +129,8 @@ You can pass the `preload_stylesheet` variable to the `vite-tag` snippet to enab {% render 'vite-tag' with 'theme.scss', preload_stylesheet: true %} ``` +Alternatively, use `snippetAssetFile: 'vite-asset.liquid'` to get just the URL: `{% render 'vite-asset', entry: 'theme.scss' %}` + ### Import aliases For convenience, `~/` and `@/` are aliased to your `frontend` folder, which simplifies imports: From 8eb17e70de57abbc4b834c0e724d3958b4cec876 Mon Sep 17 00:00:00 2001 From: Miguel Montalvo Date: Sun, 15 Feb 2026 22:02:06 -0600 Subject: [PATCH 4/4] Release note --- .changeset/silly-crews-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-crews-smile.md diff --git a/.changeset/silly-crews-smile.md b/.changeset/silly-crews-smile.md new file mode 100644 index 00000000..ba54a755 --- /dev/null +++ b/.changeset/silly-crews-smile.md @@ -0,0 +1,5 @@ +--- +"vite-plugin-shopify": minor +--- + +Add snippetAssetFile option for URL-only output