Skip to content

Commit

Permalink
feat(dev-server): add CSP support for vite's injectClientScript opt…
Browse files Browse the repository at this point in the history
…ion (#202)

* feat(dev-server): add nonce to client injected script when content-security-policy header is present on response

* test(dev-server): add test to cover injected script has attribute nonce

* add changeset
  • Loading branch information
meck93 authored Jan 5, 2025
1 parent 90e95be commit 3eae0ff
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/rich-apes-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/vite-dev-server': minor
---

feat: add CSP support to vite's `injectClientScript` option
16 changes: 16 additions & 0 deletions packages/dev-server/e2e-bun/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ test('Should contain an injected script tag', async ({ page }) => {
const lastScriptTag = await page.$('script:last-of-type')
expect(lastScriptTag).not.toBeNull()

const nonce = await lastScriptTag?.getAttribute('nonce')
expect(nonce).toBeNull()

const content = await lastScriptTag?.textContent()
expect(content).toBe('import("/@vite/client")')
})

test('Should contain an injected script tag with a nonce', async ({ page }) => {
await page.goto('/with-nonce')

const lastScriptTag = await page.$('script:last-of-type')
expect(lastScriptTag).not.toBeNull()

const nonce = await lastScriptTag?.getAttribute('nonce')
expect(nonce).not.toBeNull()

const content = await lastScriptTag?.textContent()
expect(content).toBe('import("/@vite/client")')
})
Expand Down
5 changes: 5 additions & 0 deletions packages/dev-server/e2e-bun/mock/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ app.get('/', (c) => {
return c.html('<h1>Hello Vite!</h1>')
})

app.get('/with-nonce', (c) => {
c.header('content-security-policy', 'script-src-elem \'self\' \'nonce-ZMuLoN/taD7JZTUXfl5yvQ==\';')
return c.html('<h1>Hello Vite!</h1>')
})

app.get('/file.ts', (c) => {
return c.text('console.log("exclude me!")')
})
Expand Down
16 changes: 16 additions & 0 deletions packages/dev-server/e2e/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ test('Should contain an injected script tag', async ({ page }) => {
const lastScriptTag = await page.$('script:last-of-type')
expect(lastScriptTag).not.toBeNull()

const nonce = await lastScriptTag?.getAttribute('nonce')
expect(nonce).toBeNull()

const content = await lastScriptTag?.textContent()
expect(content).toBe('import("/@vite/client")')
})

test('Should contain an injected script tag with a nonce', async ({ page }) => {
await page.goto('/with-nonce')

const lastScriptTag = await page.$('script:last-of-type')
expect(lastScriptTag).not.toBeNull()

const nonce = await lastScriptTag?.getAttribute('nonce')
expect(nonce).not.toBeNull()

const content = await lastScriptTag?.textContent()
expect(content).toBe('import("/@vite/client")')
})
Expand Down
5 changes: 5 additions & 0 deletions packages/dev-server/e2e/mock/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ app.get('/', (c) => {
return c.html('<h1>Hello Vite!</h1>')
})

app.get('/with-nonce', (c) => {
c.header('content-security-policy', 'script-src-elem \'self\' \'nonce-ZMuLoN/taD7JZTUXfl5yvQ==\';')
return c.html('<h1>Hello Vite!</h1>')
})

app.get('/name', (c) => c.html(`<h1>My name is ${c.env.NAME}</h1>`))

app.get('/wait-until', (c) => {
Expand Down
5 changes: 4 additions & 1 deletion packages/dev-server/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ export function devServer(options?: DevServerOptions): VitePlugin {
options?.injectClientScript !== false &&
response.headers.get('content-type')?.match(/^text\/html/)
) {
const script = '<script>import("/@vite/client")</script>'
const nonce = response.headers
.get('content-security-policy')
?.match(/'nonce-([^']+)'/)?.[1]
const script = `<script${nonce ? ` nonce="${nonce}"` : ''}>import("/@vite/client")</script>`
return injectStringToResponse(response, script)
}
return response
Expand Down

0 comments on commit 3eae0ff

Please sign in to comment.