Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/vite-dev-server/src/node/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

Expand Down
23 changes: 0 additions & 23 deletions packages/vite-dev-server/src/node/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,29 +414,6 @@ export async function setupHMR(server: ViteDevServer): Promise<FileChangeTracker
function setupVirtualFiles(files?: Record<string, string | ArrayBuffer>): void {
fs.mkdirSync('/public', { recursive: true })
fs.writeFileSync('/public/.gitkeep', '', { encoding: 'utf8' })
fs.writeFileSync(
'/index.html',
`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
}
</style>
</head>
<body>
<div id="app"><p>Loading...</p></div>
<script type="module" src="/main.js"></script>
</body>
</html>`,
{ encoding: 'utf8' }
)

if (files) {
updateFiles(files)
Expand Down
2 changes: 2 additions & 0 deletions packages/vrowzer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<div id="app">` 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/vrowzer/docs/default/interfaces/VrowzerConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export interface VrowzerConfig

| Name | Type | Description |
| --- | --- | --- |
| `files` | `Record<string, string \| ArrayBuffer>` | 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<string, string \| ArrayBuffer>` | 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`. |
24 changes: 18 additions & 6 deletions packages/vrowzer/integration/playground/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<h1>Hello!</h1>'
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': `<!doctype html>
files: readmeExample
? readmeExampleFiles
: {
...initialBinaryFiles,
'/public/initial-public.txt': 'initial public file',
'/index.html': `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -44,7 +56,7 @@ async function init() {
<script type="module" src="/main.js"></script>
</body>
</html>`,
'/main.js': `
'/main.js': `
document.getElementById('app').innerHTML = \`
<h1>Hello from Vrowzer!</h1>
<p id="counter">count: 0</p>
Expand All @@ -55,7 +67,7 @@ if (import.meta.hot) {
import.meta.hot.accept()
}
`
}
}
})

if (!ready) {
Expand Down
26 changes: 26 additions & 0 deletions packages/vrowzer/integration/vrowzer.integration-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/vrowzer/src/index-file-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div id="app"></div>')
expect(webWorkerIndex).toContain('<script type="module" src="/main.js"></script>')
})

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
Expand All @@ -211,6 +224,7 @@ describe('Vrowzer file synchronization', () => {
type: 'V_FS_INIT',
files: {
'/main.js': 'export {}',
'/index.html': expect.stringContaining('<div id="app"></div>'),
'/dist/client/client.mjs': 'client code',
'/dist/client/env.mjs': 'env code'
},
Expand Down
24 changes: 24 additions & 0 deletions packages/vrowzer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
`

/**
* `postMessage()` type used by the preview bootstrap to report load failures to the host.
*/
Expand Down Expand Up @@ -151,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<string, string | ArrayBuffer>
}
Expand Down Expand Up @@ -1091,6 +1111,10 @@ export function Vrowzer(options: VrowzerOptions = {}): Readonly<Vrowzer> {
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' })
Expand Down
Loading