From 0a02503e146d39566604df37a41365e730364be9 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 1 Oct 2026 23:56:48 +0900 Subject: [PATCH 1/4] fix(vrowzer): give both Workers a default index.html when ready() gets none The Web Worker wrote a default /index.html during setup, but the Service Worker, which serves the preview HTML, never got one. A ready() without /index.html, as in the README usage example, therefore left the preview without HTML: since #69 it reported one 404 load error and stayed blank. initialize() now adds a default /index.html to the files it copies from ready() when they have none, and sends it to both Workers. The default is minimal: an empty #app element and a module script that loads /main.js. An /index.html given to ready() is used as before. --- packages/vrowzer/src/index-file-sync.test.ts | 14 +++++++++++++ packages/vrowzer/src/index.ts | 22 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/vrowzer/src/index-file-sync.test.ts b/packages/vrowzer/src/index-file-sync.test.ts index f0bb1f3..66c1594 100644 --- a/packages/vrowzer/src/index-file-sync.test.ts +++ b/packages/vrowzer/src/index-file-sync.test.ts @@ -188,6 +188,19 @@ describe('Vrowzer file synchronization', () => { ]) }) + test('gives both Workers the same default /index.html when ready() gets none', async () => { + const vrowzer = Vrowzer() + + await expect(vrowzer.ready({ files: { '/main.js': 'export {}' } })).resolves.toBe(true) + + const webWorkerIndex = workers[0]!.messages[0]!.files!['/index.html'] + const init = controllerMocks.postMessage.mock.calls[0]![0] as TestMessage + expect(webWorkerIndex).toBeTypeOf('string') + expect(init.files!['/index.html']).toBe(webWorkerIndex) + expect(webWorkerIndex).toContain('
') + expect(webWorkerIndex).toContain('') + }) + test('sends binary files to both Workers as copies, without transferring them', async () => { const bytes = [0x89, 0x50, 0x4e, 0x47, 0x00, 0xff] const buffer = new Uint8Array(bytes).buffer @@ -211,6 +224,7 @@ describe('Vrowzer file synchronization', () => { type: 'V_FS_INIT', files: { '/main.js': 'export {}', + '/index.html': expect.stringContaining('
'), '/dist/client/client.mjs': 'client code', '/dist/client/env.mjs': 'env code' }, diff --git a/packages/vrowzer/src/index.ts b/packages/vrowzer/src/index.ts index 98cb03a..f026a8a 100644 --- a/packages/vrowzer/src/index.ts +++ b/packages/vrowzer/src/index.ts @@ -70,6 +70,24 @@ const DEFAULT_SERVICE_WORKER_READY_TIMEOUT = 60_000 const DEFAULT_WEB_WORKER_SETUP_TIMEOUT = 90_000 const DEFAULT_FILE_SYNC_TIMEOUT = 10_000 +/** + * The `/index.html` used when the files given to `ready()` have none. It loads `/main.js` as a + * module script, with an empty `#app` element to render into. + */ +const DEFAULT_INDEX_HTML = ` + + + + + Preview + + +
+ + + +` + /** * `postMessage()` type used by the preview bootstrap to report load failures to the host. */ @@ -1091,6 +1109,10 @@ export function Vrowzer(options: VrowzerOptions = {}): Readonly { try { // The files are sent later, so copy them as they are when ready() is called const initialFiles = copyInitialFiles(config.files) + // A preview always loads /index.html, so give both Workers the same default + if (!Object.hasOwn(initialFiles, '/index.html')) { + initialFiles['/index.html'] = DEFAULT_INDEX_HTML + } // 1. Create Web Worker + add as publisher target webWorker = new Worker(new URL('./web-worker.ts', import.meta.url), { type: 'module' }) From 425a41d46008f5473803ba4df98bf3a8e385e066 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 1 Oct 2026 23:57:35 +0900 Subject: [PATCH 2/4] refactor(vite-dev-server): leave the default index.html to the runtime setupVirtualFiles() wrote a default /index.html in the Web Worker. The Service Worker serves the preview HTML and never had this default, so it was never served. The vrowzer runtime now sends the same default /index.html to both Workers when ready() gets none, so the Web Worker no longer writes its own. /public/.gitkeep is still written. --- .../src/node/transformer.test.ts | 2 ++ .../vite-dev-server/src/node/transformer.ts | 23 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/vite-dev-server/src/node/transformer.test.ts b/packages/vite-dev-server/src/node/transformer.test.ts index d847fe5..521a2fe 100644 --- a/packages/vite-dev-server/src/node/transformer.test.ts +++ b/packages/vite-dev-server/src/node/transformer.test.ts @@ -235,6 +235,8 @@ describe('virtual files', () => { expect(fs.writeFileSync).toHaveBeenCalledWith('/public/logo.png', new Uint8Array(bytes)) expect(fs.writeFileSync).toHaveBeenCalledWith('/main.js', 'export {}', { encoding: 'utf8' }) + // The runtime provides /index.html, so the Web Worker does not write a default one + expect(vi.mocked(fs.writeFileSync).mock.calls.map(([path]) => path)).not.toContain('/index.html') }) }) diff --git a/packages/vite-dev-server/src/node/transformer.ts b/packages/vite-dev-server/src/node/transformer.ts index d29e546..61f1b70 100644 --- a/packages/vite-dev-server/src/node/transformer.ts +++ b/packages/vite-dev-server/src/node/transformer.ts @@ -414,29 +414,6 @@ export async function setupHMR(server: ViteDevServer): Promise): void { fs.mkdirSync('/public', { recursive: true }) fs.writeFileSync('/public/.gitkeep', '', { encoding: 'utf8' }) - fs.writeFileSync( - '/index.html', - ` - - - - - Preview - - - -

Loading...

- - -`, - { encoding: 'utf8' } - ) if (files) { updateFiles(files) From 6e78e61bd8c821ac6a30936fbbc4e108d9d7ca4d Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 2 Oct 2026 00:00:54 +0900 Subject: [PATCH 3/4] test(vrowzer): cover the README usage example in the browser Opened with ?files=readme-example, the playground gives ready() only the /main.js of the README usage example. A new test opens it in a new browser context, which has a Service Worker of its own, and checks that the preview shows "Hello!", as the example expects. Without the default /index.html, the preview stays blank and the test times out. --- .../vrowzer/integration/playground/main.ts | 24 ++++++++++++----- .../integration/vrowzer.integration-test.ts | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/vrowzer/integration/playground/main.ts b/packages/vrowzer/integration/playground/main.ts index a0a5a3c..cae089b 100644 --- a/packages/vrowzer/integration/playground/main.ts +++ b/packages/vrowzer/integration/playground/main.ts @@ -24,15 +24,27 @@ const initialBinaryFiles = { } ;(window as any).__initialBinaryFiles__ = initialBinaryFiles +// `?files=readme-example` gives ready() only the files of the README usage example, which has no +// /index.html. A test opens it in a new browser context, with a Service Worker of its own. +const readmeExample = new URLSearchParams(location.search).get('files') === 'readme-example' +const readmeExampleFiles = { + '/main.js': ` + document.getElementById('app').innerHTML = '

Hello!

' + if (import.meta.hot) { import.meta.hot.accept() } + ` +} + async function init() { try { status.textContent = 'Initializing vrowzer...' const ready = await vrowzer.ready({ - files: { - ...initialBinaryFiles, - '/public/initial-public.txt': 'initial public file', - '/index.html': ` + files: readmeExample + ? readmeExampleFiles + : { + ...initialBinaryFiles, + '/public/initial-public.txt': 'initial public file', + '/index.html': ` @@ -44,7 +56,7 @@ async function init() { `, - '/main.js': ` + '/main.js': ` document.getElementById('app').innerHTML = \`

Hello from Vrowzer!

count: 0

@@ -55,7 +67,7 @@ if (import.meta.hot) { import.meta.hot.accept() } ` - } + } }) if (!ready) { diff --git a/packages/vrowzer/integration/vrowzer.integration-test.ts b/packages/vrowzer/integration/vrowzer.integration-test.ts index bdce522..39fef10 100644 --- a/packages/vrowzer/integration/vrowzer.integration-test.ts +++ b/packages/vrowzer/integration/vrowzer.integration-test.ts @@ -590,6 +590,32 @@ describe('Vrowzer E2E', () => { dataset: 'preview' }) }) + + test('shows the README usage example without /index.html', async () => { + // The playground gives ready() only the /main.js of the README usage example. A new browser + // context has a Service Worker of its own, so the shared playground is not affected. + const context = await browser.newContext() + const readmePage = await context.newPage() + + try { + await readmePage.goto(`${serverUrl}?files=readme-example`) + await readmePage.waitForFunction( + () => document.getElementById('status')?.textContent === 'Ready', + undefined, + { timeout: 60_000 } + ) + await readmePage.waitForFunction( + () => + ( + document.querySelector('#preview-container iframe') as HTMLIFrameElement | null + )?.contentDocument?.querySelector('#app h1')?.textContent === 'Hello!', + undefined, + { timeout: 30_000 } + ) + } finally { + await context.close() + } + }, 120_000) }) describe('file operations', () => { From a91403c312314315e171b5d4833fc8f2320e2f62 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 2 Oct 2026 00:01:14 +0900 Subject: [PATCH 4/4] docs(vrowzer): describe the default index.html The README section of ready() and the JSDoc of VrowzerConfig.files say that, without /index.html, Vrowzer uses a default one: an empty #app element and a module script that loads /main.js. The API references are regenerated. --- packages/vrowzer/README.md | 2 ++ packages/vrowzer/docs/default/interfaces/VrowzerConfig.md | 2 +- packages/vrowzer/src/index.ts | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vrowzer/README.md b/packages/vrowzer/README.md index c8838de..471ad7b 100644 --- a/packages/vrowzer/README.md +++ b/packages/vrowzer/README.md @@ -178,6 +178,8 @@ const ready = await vrowzer.ready({ File contents can be strings or `ArrayBuffer`s. Each `ArrayBuffer` is copied for the Workers when `ready()` is called, so the caller's buffer stays usable. +If `files` has no `/index.html`, Vrowzer uses a default one: an empty `
` and a module script that loads `/main.js`. + #### `mount(container, options): PreviewSession` Mounts a preview iframe into the given DOM element. `options.id` is a host-defined, non-empty pane identity. Mounting the same ID again returns the existing session without moving or reloading its iframe; the first container and params remain in effect. diff --git a/packages/vrowzer/docs/default/interfaces/VrowzerConfig.md b/packages/vrowzer/docs/default/interfaces/VrowzerConfig.md index a1c0ce6..c7ba0ed 100644 --- a/packages/vrowzer/docs/default/interfaces/VrowzerConfig.md +++ b/packages/vrowzer/docs/default/interfaces/VrowzerConfig.md @@ -12,4 +12,4 @@ export interface VrowzerConfig | Name | Type | Description | | --- | --- | --- | -| `files` | `Record` | A record of file paths and their corresponding content, which can be either a string or an ArrayBuffer. An ArrayBuffer is copied for the Workers when [`Vrowzer.ready`](/packages/vrowzer/docs/default/interfaces/Vrowzer.md#method-ready) is called, and stays usable. | +| `files` | `Record` | A record of file paths and their corresponding content, which can be either a string or an ArrayBuffer. An ArrayBuffer is copied for the Workers when [`Vrowzer.ready`](/packages/vrowzer/docs/default/interfaces/Vrowzer.md#method-ready) is called, and stays usable. Without `/index.html`, a default one is used: an empty `#app` element and a module script that loads `/main.js`. | diff --git a/packages/vrowzer/src/index.ts b/packages/vrowzer/src/index.ts index f026a8a..1bf58e0 100644 --- a/packages/vrowzer/src/index.ts +++ b/packages/vrowzer/src/index.ts @@ -169,6 +169,8 @@ export interface VrowzerConfig { /** * A record of file paths and their corresponding content, which can be either a string or an ArrayBuffer. * An ArrayBuffer is copied for the Workers when {@linkcode Vrowzer.ready} is called, and stays usable. + * Without `/index.html`, a default one is used: an empty `#app` element and a module script that + * loads `/main.js`. */ files: Record }