From f3a700f3083ed49fa93b8a4ba7649627d89c187c Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Tue, 3 Nov 2026 11:24:51 +0900 Subject: [PATCH] fix(md): run client scripts in builds without an .island. file The Markdown client entry joined the client build only when a file with `.island.` in its name existed under `pages/`. Without one, a page's client script ran in dev but not after a production build, even when the page imported an island. Add the entry when a page the build keeps has client code, using the same file list and script extraction as the Markdown page scan. A script with only island imports, a ` + +# Script + +unset +`, + 'pages/mixed.md': ` + +# Mixed + +unset + + +`, + }, + }); + let server: PreviewServer | undefined; + let browser: Browser | undefined; + try { + const manifest = JSON.parse( + readFileSync(join(build.root, 'dist/client/.vite/manifest.json'), 'utf8'), + ); + expect(manifest).toHaveProperty(['virtual:void-md-client']); + // A page without client code still ships no JavaScript. + expect(build.page('about.html').html).not.toContain(' = []; + page.on('pageerror', (error) => errors.push(error.message)); + + await page.goto(new URL('script', base).href); + await expect.poll(() => page.textContent('#out'), { timeout: 15_000 }).toBe('set-by-script'); + + await page.goto(new URL('mixed', base).href); + await expect.poll(() => page.textContent('#out'), { timeout: 15_000 }).toBe('set-by-script'); + await waitForIslandsHydrated(page, 1, 15_000); + await page.getByRole('button', { name: 'Count: 0', exact: true }).click(); + await expect.poll(() => page.getByRole('button').textContent()).toBe('Count: 1'); + + expect(errors).toEqual([]); + } finally { + await browser?.close(); + await server?.close(); + build.cleanup(); + } +}, 120_000); diff --git a/packages/md/test/unit/plugin-source-dir.test.ts b/packages/md/test/unit/plugin-source-dir.test.ts index 0ca6ebbce..5f1e413fc 100644 --- a/packages/md/test/unit/plugin-source-dir.test.ts +++ b/packages/md/test/unit/plugin-source-dir.test.ts @@ -77,7 +77,7 @@ describe('voidMarkdown sourceDir support', () => { const buildStart = getHookHandler(plugin.buildStart) as () => Promise; const load = getHookHandler(plugin.load) as (id: string) => string | undefined; - config({ root }, { command }); + await config({ root }, { command }); configResolved({ root, plugins: [{ name: 'void-react:pages' }], @@ -121,21 +121,32 @@ describe('voidMarkdown sourceDir support', () => { expect((await scanForCommand(root, 'build')).paths).toEqual(['/posts/stats']); }); - it('adds the markdown client entry for sourceDir pages with scripts and islands', () => { + function buildConfigFor(pages: Record): Promise { const root = createTempRoot(); - writeFileSync( - join(root, 'src', 'pages', 'posts', 'hello.md'), - '\n\n# Hello\n', - ); - writeFileSync(join(root, 'src', 'pages', 'posts', 'Counter.island.tsx'), 'export default {};'); - - const plugin = createPlugin(); - const config = getHookHandler(plugin.config) as ( + for (const [file, content] of Object.entries(pages)) { + writeFileSync(join(root, 'src', 'pages', file), content); + } + const config = getHookHandler(createPlugin().config) as ( config: { root: string }, env: { command: 'build' }, - ) => unknown; + ) => Promise; + return config({ root }, { command: 'build' }); + } - expect(config({ root }, { command: 'build' })).toEqual({ + const CLIENT_CODE = '\n\n# Hello\n'; + const ISLAND_IMPORT = 'import Counter from "./Counter.tsx" with { island: "load" };'; + + it.each([ + ['a page', { 'posts/hello.md': CLIENT_CODE }], + ['a build-only page', { 'stats.prod.md': CLIENT_CODE }], + [ + 'a page that also imports an island', + { + 'mixed.md': `\n\n\n`, + }, + ], + ])('adds the markdown client entry for client code on %s', async (_name, pages) => { + expect(await buildConfigFor(pages)).toEqual({ environments: { client: { build: { @@ -147,4 +158,27 @@ describe('voidMarkdown sourceDir support', () => { }, }); }); + + it.each([ + ['pages without a script', { 'hello.md': '# Hello\n' }], + [ + 'a script with only island imports', + { 'hello.md': `\n\n\n` }, + ], + [ + 'a script tag in a code block', + { 'hello.md': '# Hello\n\n```html\n\n```\n' }, + ], + ['client code on a dev-only page', { 'debug.dev.md': CLIENT_CODE, 'hello.md': '# Hello\n' }], + [ + 'island files next to pages without client code', + { + 'hello.md': `\n\n\n`, + 'Counter.island.tsx': 'export default {};', + '_x.island.txt': '', + }, + ], + ])('does not add the markdown client entry for %s', async (_name, pages) => { + expect(await buildConfigFor(pages)).toBeUndefined(); + }); });