Skip to content

Commit cffd332

Browse files
Tunnel fix and cors defaults (#215)
1 parent 27adce3 commit cffd332

File tree

8 files changed

+430
-51
lines changed

8 files changed

+430
-51
lines changed

.changeset/ten-bars-drive.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"vite-plugin-shopify": minor
3+
---
4+
5+
This release addresses a critical compatibility issue with Vite 7 and enhances the development server experience.
6+
7+
**Vite 7 Tunnel Fix:** The plugin now automatically configures `server.allowedHosts` to work with the dynamic tunnel feature (`tunnel: true`), resolving the "Blocked request" error.
8+
9+
**Smart CORS Defaults:** To improve the out-of-the-box experience, the plugin now sets a default CORS policy that allows requests from `localhost` and `*.myshopify.com`, which is a common requirement for theme development. Your custom `server.cors` settings will always take precedence.

docs/guide/troubleshooting.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export default {
9494

9595
If you are experiencing Cloudflare tunnel errors with the Shopify Vite Plugin, you can use ngrok as a workaround.
9696
First, create an ngrok account and install the ngrok CLI, then follow their instructions to set up your access token.
97-
Next, run the command `ngrok http 3000` (or any other port number you prefer) and take note of the URL
97+
Next, run the command `ngrok http 5173` (or any other port number you prefer) and take note of the URL
9898
provided by ngrok, which ends with `ngrok-free.app`. Keep ngrok running. Finally, configure the plugin.
9999

100100
::: code-group
@@ -105,7 +105,7 @@ import shopify from 'vite-plugin-shopify'
105105
export default {
106106
plugins: [
107107
shopify({
108-
tunnel: 'https://123abc.ngrok-free.app:3000' // [!code ++]
108+
tunnel: 'https://123abc.ngrok-free.app' // [!code ++]
109109
})
110110
]
111111
}

examples/vite-shopify-example/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
"lint": "eslint ."
99
},
1010
"devDependencies": {
11-
"@vitejs/plugin-basic-ssl": "^1.0.2",
1211
"cross-env": "^7.0.3",
1312
"sass": "^1.52.2",
1413
"tsconfig": "workspace:*",
15-
"vite": "^6.0.11",
14+
"vite": "^7.2.4",
1615
"vite-plugin-page-reload": "workspace:*",
1716
"vite-plugin-shopify": "workspace:*"
1817
},

packages/vite-plugin-shopify/src/config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,26 @@ export default function shopifyConfig (options: Required<Options>): Plugin {
6767
? false
6868
: {
6969
...(config.server?.hmr === true ? {} : config.server?.hmr)
70-
}
70+
},
71+
allowedHosts: config.server?.allowedHosts ?? [
72+
...(typeof options.tunnel === 'string'
73+
? (() => {
74+
try {
75+
return [new URL(options.tunnel).hostname]
76+
} catch {
77+
throw new Error(`Invalid tunnel URL: ${options.tunnel}`)
78+
}
79+
})()
80+
: options.tunnel
81+
? ['.trycloudflare.com']
82+
: [])
83+
],
84+
cors: config.server?.cors ?? {
85+
origin: config.server?.origin ?? [
86+
/^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/, // allows localhost (default)
87+
/\.myshopify\.com$/ // allows myshopify.com URLs
88+
]
89+
}
7190
}
7291
}
7392

packages/vite-plugin-shopify/src/html.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ export default function shopifyHTML (options: Required<Options>): Plugin {
3737
}
3838
},
3939
configureServer ({ config, middlewares, httpServer }) {
40-
const tunnelConfig = resolveTunnelConfig(options)
41-
42-
if (tunnelConfig.frontendPort !== -1) {
43-
config.server.port = tunnelConfig.frontendPort
44-
config.server.allowedHosts = [new URL(tunnelConfig.frontendUrl).hostname]
45-
}
40+
const { frontendUrl, frontendPort, usingLocalhost } = generateFrontendURL(options)
4641

4742
httpServer?.once('listening', () => {
4843
const address = httpServer?.address()
@@ -55,16 +50,16 @@ export default function shopifyHTML (options: Required<Options>): Plugin {
5550
plugin.name === 'vite:react-babel' || plugin.name === 'vite:react-refresh'
5651
)
5752

58-
debug({ address, viteDevServerUrl, tunnelConfig })
53+
debug({ address, viteDevServerUrl, frontendUrl, frontendPort, usingLocalhost })
5954

6055
setTimeout(() => {
6156
void (async (): Promise<void> => {
6257
if (options.tunnel === false) {
6358
return
6459
}
6560

66-
if (tunnelConfig.frontendUrl !== '') {
67-
tunnelUrl = tunnelConfig.frontendUrl
61+
if (frontendUrl !== '') {
62+
tunnelUrl = frontendUrl
6863
isTTY() && renderInfo({ body: `${viteDevServerUrl} is tunneled to ${tunnelUrl}` })
6964
return
7065
}
@@ -76,7 +71,6 @@ export default function shopifyHTML (options: Required<Options>): Plugin {
7671
})
7772
tunnelClient = hook.valueOrAbort()
7873
tunnelUrl = await pollTunnelUrl(tunnelClient)
79-
config.server.allowedHosts = [new URL(tunnelUrl).hostname]
8074
isTTY() && renderInfo({ body: `${viteDevServerUrl} is tunneled to ${tunnelUrl}` })
8175
const viteTagSnippetContent = viteTagSnippetPrefix(config) + viteTagSnippetDev(
8276
tunnelUrl, options.entrypointsDir, reactPlugin, options.themeHotReload
@@ -88,8 +82,8 @@ export default function shopifyHTML (options: Required<Options>): Plugin {
8882
}, 100)
8983

9084
const viteTagSnippetContent = viteTagSnippetPrefix(config) + viteTagSnippetDev(
91-
tunnelConfig.frontendUrl !== ''
92-
? tunnelConfig.frontendUrl
85+
frontendUrl !== ''
86+
? frontendUrl
9387
: viteDevServerUrl, options.entrypointsDir, reactPlugin, options.themeHotReload
9488
)
9589

@@ -314,8 +308,14 @@ function isIpv6 (address: AddressInfo): boolean {
314308
address.family === 6
315309
}
316310

317-
function resolveTunnelConfig (options: Required<Options>): FrontendURLResult {
318-
let frontendPort = -1
311+
/**
312+
* The tunnel creation logic depends on the tunnel option:
313+
* - If tunnel is false, uses localhost
314+
* - If tunnel is a string (custom URL), uses that URL
315+
* - If tunnel is true, a tunnel is created (by default using cloudflare)
316+
*/
317+
function generateFrontendURL (options: Required<Options>): FrontendURLResult {
318+
const frontendPort = -1
319319
let frontendUrl = ''
320320
let usingLocalhost = false
321321

@@ -328,12 +328,7 @@ function resolveTunnelConfig (options: Required<Options>): FrontendURLResult {
328328
return { frontendUrl, frontendPort, usingLocalhost }
329329
}
330330

331-
const matches = options.tunnel.match(/(https:\/\/[^:]+):([0-9]+)/)
332-
if (matches === null) {
333-
throw new Error(`Invalid tunnel URL: ${options.tunnel}`)
334-
}
335-
frontendPort = Number(matches[2])
336-
frontendUrl = matches[1]
331+
frontendUrl = options.tunnel
337332
return { frontendUrl, frontendPort, usingLocalhost }
338333
}
339334

packages/vite-plugin-shopify/test/config.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,71 @@ describe('vite-plugin-shopify:config', () => {
4444
expect(config.publicDir).toEqual('public')
4545
expect(config.build.rollupOptions.input).toEqual(['frontend/entrypoints/theme.js', 'resources/js/foo.js'])
4646
})
47+
48+
it('sets allowedHosts for dynamic tunnel', () => {
49+
const options = resolveOptions({ tunnel: true })
50+
const userConfig = plugin(options)
51+
const config = userConfig.config({}, { command: 'serve', mode: 'development' })
52+
53+
expect(config.server.allowedHosts).toEqual(['.trycloudflare.com'])
54+
})
55+
56+
it('sets allowedHosts for static tunnel URL', () => {
57+
const options = resolveOptions({ tunnel: 'https://my-tunnel.ngrok-free.app' })
58+
const userConfig = plugin(options)
59+
const config = userConfig.config({}, { command: 'serve', mode: 'development' })
60+
61+
expect(config.server.allowedHosts).toEqual(['my-tunnel.ngrok-free.app'])
62+
})
63+
64+
it('does not override user-defined allowedHosts', () => {
65+
const options = resolveOptions({ tunnel: true })
66+
const userConfig = plugin(options)
67+
68+
const config = userConfig.config({
69+
server: {
70+
allowedHosts: ['my-custom-host.com']
71+
}
72+
}, { command: 'serve', mode: 'development' })
73+
74+
expect(config.server.allowedHosts).toEqual(['my-custom-host.com'])
75+
})
76+
77+
it('applies default CORS configuration when none is provided', () => {
78+
const options = resolveOptions({})
79+
const userConfig = plugin(options)
80+
const config = userConfig.config({}, { command: 'serve', mode: 'development' })
81+
const expectedCorsOrigin = [
82+
/^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/,
83+
/\.myshopify\.com$/
84+
]
85+
expect(config.server.cors.origin).toEqual(expectedCorsOrigin)
86+
})
87+
88+
it('does not override user-defined CORS configuration (boolean)', () => {
89+
const options = resolveOptions({})
90+
const userConfig = plugin(options)
91+
const config = userConfig.config({
92+
server: {
93+
cors: false
94+
}
95+
}, { command: 'serve', mode: 'development' })
96+
expect(config.server.cors).toBe(false)
97+
})
98+
99+
it('does not override user-defined CORS configuration (object)', () => {
100+
const options = resolveOptions({})
101+
const userConfig = plugin(options)
102+
const customCorsConfig = {
103+
origin: 'https://my-custom-origin.com'
104+
}
105+
const config = userConfig.config({
106+
server: {
107+
cors: customCorsConfig
108+
}
109+
}, { command: 'serve', mode: 'development' })
110+
expect(config.server.cors).toEqual(customCorsConfig)
111+
})
47112
})
48113

49114
describe('resolveOptions', () => {

packages/vite-plugin-shopify/test/html.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ describe('vite-plugin-shopify:html', () => {
167167

168168
const tagsHtml = await fs.readFile(path.join(__dirname, '__fixtures__', 'snippets', 'vite-tag.liquid'), { encoding: 'utf8' })
169169

170-
expect(mockViteDevServer.config.server.allowedHosts).toContain(new URL(mockResult.url).hostname)
171-
172170
expect(tagsHtml).toMatchSnapshot()
173171

174172
vi.useRealTimers()
@@ -178,7 +176,7 @@ describe('vite-plugin-shopify:html', () => {
178176
const options = resolveOptions({
179177
themeRoot: 'test/__fixtures__',
180178
sourceCodeDir: 'test/__fixtures__/frontend',
181-
tunnel: 'https://123abc.ngrok.io:3000'
179+
tunnel: 'https://123abc.ngrok.io'
182180
})
183181

184182
vi.useFakeTimers()
@@ -191,8 +189,6 @@ describe('vite-plugin-shopify:html', () => {
191189

192190
const tagsHtml = await fs.readFile(path.join(__dirname, '__fixtures__', 'snippets', 'vite-tag.liquid'), { encoding: 'utf8' })
193191

194-
expect(mockViteDevServer.config.server.allowedHosts).toContain(new URL(options.tunnel).hostname)
195-
196192
expect(tagsHtml).toMatchSnapshot()
197193

198194
vi.useRealTimers()

0 commit comments

Comments
 (0)