Skip to content

Commit

Permalink
feat(dev-server): support streaming (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe authored Nov 1, 2023
1 parent 31d62c8 commit fb626fc
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 21 deletions.
3 changes: 2 additions & 1 deletion packages/dev-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"bumpp": "^9.2.0",
"glob": "^10.3.4",
"hono": "^3.5.8",
"playwright": "^1.39.0",
"publint": "^0.1.12",
"rimraf": "^5.0.1",
"tsup": "^7.2.0",
Expand All @@ -57,4 +58,4 @@
"@hono/node-server": "^1.2.0",
"miniflare": "^3.20231016.0"
}
}
}
58 changes: 48 additions & 10 deletions packages/dev-server/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,10 @@ export function devServer(options?: DevServerOptions): Plugin {
})
if (
options?.injectClientScript !== false &&
// If the response is a streaming, it does not inject the script:
!response.headers.get('transfer-encoding')?.match('chunked') &&
response.headers.get('content-type')?.match(/^text\/html/)
) {
const body =
(await response.text()) + '<script type="module" src="/@vite/client"></script>'
const headers = new Headers(response.headers)
headers.delete('content-length')
return new Response(body, {
status: response.status,
headers,
})
const script = '<script>import("/@vite/client")</script>'
return injectStringToResponse(response, script)
}
return response
})(req, res)
Expand All @@ -118,3 +110,49 @@ export function devServer(options?: DevServerOptions): Plugin {
}
return plugin
}

function injectStringToResponse(response: Response, content: string) {
const stream = response.body
const newContent = new TextEncoder().encode(content)

if (!stream) return null

const reader = stream.getReader()
const newContentReader = new ReadableStream({
start(controller) {
controller.enqueue(newContent)
controller.close()
},
}).getReader()

const combinedStream = new ReadableStream({
async start(controller) {
for (;;) {
const [existingResult, newContentResult] = await Promise.all([
reader.read(),
newContentReader.read(),
])

if (existingResult.done && newContentResult.done) {
controller.close()
break
}

if (!existingResult.done) {
controller.enqueue(existingResult.value)
}
if (!newContentResult.done) {
controller.enqueue(newContentResult.value)
}
}
},
})

const headers = new Headers(response.headers)
headers.delete('content-length')

return new Response(combinedStream, {
headers,
status: response.status,
})
}
27 changes: 19 additions & 8 deletions packages/dev-server/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ test('Should return 200 response', async ({ page }) => {
const response = await page.goto('/')
expect(response?.status()).toBe(200)

const content = await page.textContent('body')
const headers = response?.headers() ?? {}
expect(headers['x-via']).toBe('vite')

const content = await page.textContent('h1')
expect(content).toBe('Hello Vite!')
})

Expand All @@ -14,26 +17,23 @@ test('Should contain an injected script tag', async ({ page }) => {
const lastScriptTag = await page.$('script:last-of-type')
expect(lastScriptTag).not.toBeNull()

const typeValue = await lastScriptTag?.getAttribute('type')
expect(typeValue).toBe('module')

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

test('Should have Cloudflare bindings', async ({ page }) => {
const response = await page.goto('/name')
expect(response?.status()).toBe(200)

const content = await page.textContent('body')
const content = await page.textContent('h1')
expect(content).toBe('My name is Hono')
})

test('Should not throw an error if using `waitUntil`', async ({ page }) => {
const response = await page.goto('/wait-until')
expect(response?.status()).toBe(200)

const content = await page.textContent('body')
const content = await page.textContent('h1')
expect(content).toBe('Hello Vite!')
})

Expand All @@ -53,3 +53,14 @@ test('Should exclude the file specified in the config file', async ({ page }) =>
response = await page.goto('/static/foo.png')
expect(response?.status()).toBe(404)
})

test('Should return 200 response - /stream', async ({ page }) => {
const response = await page.goto('/stream')
expect(response?.status()).toBe(200)

const headers = response?.headers() ?? {}
expect(headers['x-via']).toBe('vite')

const content = await page.textContent('h1')
expect(content).toBe('Hello Vite!')
})
25 changes: 24 additions & 1 deletion packages/dev-server/test/mock/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,50 @@ const app = new Hono<{
}
}>()

app.get('/', (c) => c.html('<h1>Hello Vite!</h1>'))
app.get('/', (c) => {
c.header('x-via', 'vite')
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) => {
const fn = async () => {}
c.executionCtx.waitUntil(fn())
return c.html('<h1>Hello Vite!</h1>')
})

app.get('/file.ts', (c) => {
return c.text('console.log("exclude me!")')
})

app.get('/app/foo', (c) => {
return c.html('<h1>exclude me!</h1>')
})

app.get('/ends-in-ts', (c) => {
return c.text('this should not be excluded')
})

app.get('/favicon.ico', (c) => {
return c.text('a good favicon')
})

app.get('/static/foo.png', (c) => {
return c.text('a good image')
})

app.get('/stream', () => {
const html = new TextEncoder().encode('<h1>Hello Vite!</h1>')
const stream = new ReadableStream({
start(controller) {
controller.enqueue(html)
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/html', 'x-via': 'vite' },
})
})

export default app
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ __metadata:
glob: ^10.3.4
hono: ^3.5.8
miniflare: ^3.20231016.0
playwright: ^1.39.0
publint: ^0.1.12
rimraf: ^5.0.1
tsup: ^7.2.0
Expand Down Expand Up @@ -3546,7 +3547,7 @@ __metadata:
languageName: node
linkType: hard

"playwright@npm:1.39.0":
"playwright@npm:1.39.0, playwright@npm:^1.39.0":
version: 1.39.0
resolution: "playwright@npm:1.39.0"
dependencies:
Expand Down

0 comments on commit fb626fc

Please sign in to comment.