diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 60d6cb7..35085f8 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -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", @@ -57,4 +58,4 @@ "@hono/node-server": "^1.2.0", "miniflare": "^3.20231016.0" } -} \ No newline at end of file +} diff --git a/packages/dev-server/src/dev-server.ts b/packages/dev-server/src/dev-server.ts index 6381fd7..aa494a2 100644 --- a/packages/dev-server/src/dev-server.ts +++ b/packages/dev-server/src/dev-server.ts @@ -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()) + '' - const headers = new Headers(response.headers) - headers.delete('content-length') - return new Response(body, { - status: response.status, - headers, - }) + const script = '' + return injectStringToResponse(response, script) } return response })(req, res) @@ -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, + }) +} diff --git a/packages/dev-server/test/e2e.test.ts b/packages/dev-server/test/e2e.test.ts index 7c48c6c..d6a03f4 100644 --- a/packages/dev-server/test/e2e.test.ts +++ b/packages/dev-server/test/e2e.test.ts @@ -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!') }) @@ -14,18 +17,15 @@ 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') }) @@ -33,7 +33,7 @@ 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!') }) @@ -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!') +}) diff --git a/packages/dev-server/test/mock/worker.ts b/packages/dev-server/test/mock/worker.ts index df36ef8..f414725 100644 --- a/packages/dev-server/test/mock/worker.ts +++ b/packages/dev-server/test/mock/worker.ts @@ -6,27 +6,50 @@ const app = new Hono<{ } }>() -app.get('/', (c) => c.html('