|
1 | 1 | import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; |
| 2 | +import { stripTypeScriptTypes } from 'node:module'; |
2 | 3 | import { join } from 'node:path'; |
3 | 4 | import { afterEach, describe, expect, it, vi } from 'vite-plus/test'; |
| 5 | +import * as Vue from 'vue'; |
| 6 | +import { compileScript, parse } from 'vue/compiler-sfc'; |
| 7 | +import { renderToString } from 'vue/server-renderer'; |
4 | 8 | import { pagesPlugin } from '../../src/plugin-pages.ts'; |
5 | 9 |
|
6 | 10 | const TMP = join(import.meta.dirname, '__tmp_pages_plugin__'); |
7 | 11 | const PAGES_DIR = join(TMP, 'pages'); |
8 | 12 |
|
9 | 13 | type HookWithHandler<T> = { handler: T }; |
10 | 14 | type PagesPluginHooks = { |
| 15 | + transform?: HookWithHandler<(code: string, id: string) => { code: string } | undefined>; |
11 | 16 | buildStart?: (() => Promise<void>) | HookWithHandler<() => Promise<void>>; |
12 | 17 | configureServer?: (server: unknown) => void; |
13 | 18 | config?: |
@@ -38,6 +43,22 @@ function writePage(relativePath: string, source: string): void { |
38 | 43 |
|
39 | 44 | type PatchProps = (comp: Record<string, unknown>, props: Record<string, unknown>) => void; |
40 | 45 |
|
| 46 | +function compilePage(source: string): Vue.Component & Record<string, unknown> { |
| 47 | + const transform = pagesPlugin(PAGES_DIR).find((p) => p.name === 'void-vue:pages-transform') as |
| 48 | + | PagesPluginHooks |
| 49 | + | undefined; |
| 50 | + const transformed = getHookHandler(transform?.transform)(source, join(PAGES_DIR, 'index.vue')); |
| 51 | + const { descriptor } = parse(transformed?.code ?? source); |
| 52 | + const compiled = compileScript(descriptor, { id: 'test-page', inlineTemplate: true }); |
| 53 | + // Evaluate the real SFC compiler output using the same Vue instance as the renderer. |
| 54 | + const code = stripTypeScriptTypes(compiled.content) |
| 55 | + .replace(/import \{([^}]+)\} from ['"]vue['"]/g, (_, names: string) => { |
| 56 | + return `const {${names.replace(/\s+as\s+/g, ':')}} = Vue;`; |
| 57 | + }) |
| 58 | + .replace('export default', 'return'); |
| 59 | + return new Function('Vue', code)(Vue) as Vue.Component & Record<string, unknown>; |
| 60 | +} |
| 61 | + |
41 | 62 | function extractPatchProps(moduleSource: string): PatchProps { |
42 | 63 | const marker = 'function hasRuntimePropsDeclaration'; |
43 | 64 | const start = moduleSource.indexOf(marker); |
@@ -138,6 +159,55 @@ describe('pagesPlugin generated patchProps', () => { |
138 | 159 | expect(comp.props).toEqual(['count', 'title']); |
139 | 160 | expect(comp.__voidPatched).toBe(true); |
140 | 161 | }); |
| 162 | + |
| 163 | + for (const virtualId of [ |
| 164 | + '\0virtual:void-pages-renderer', |
| 165 | + '\0virtual:void-pages-client', |
| 166 | + ] as const) { |
| 167 | + it(`renders typed loader props alongside model defaults in ${virtualId.slice(1)}`, async () => { |
| 168 | + const source = `<script setup lang="ts"> |
| 169 | +const props = defineProps<{ title: string }>(); |
| 170 | +const draft = defineModel('draft', { type: String, default: 'Draft' }); |
| 171 | +</script> |
| 172 | +<template><h1>{{ props.title }}</h1><p>{{ draft }}</p></template>`; |
| 173 | + writePage('index.vue', source); |
| 174 | + const plugin = pagesPlugin(PAGES_DIR).find((p) => p.name === 'void-vue:pages') as |
| 175 | + | PagesPluginHooks |
| 176 | + | undefined; |
| 177 | + if (!plugin) { |
| 178 | + throw new Error('Expected void-vue:pages plugin.'); |
| 179 | + } |
| 180 | + const patchProps = await loadGeneratedPatchProps(plugin, virtualId); |
| 181 | + const comp = compilePage(source); |
| 182 | + const declared = comp.props; |
| 183 | + |
| 184 | + patchProps(comp, { title: 'Hello' }); |
| 185 | + |
| 186 | + expect(await renderToString(Vue.createSSRApp(comp, { title: 'Hello' }))).toBe( |
| 187 | + '<!--[--><h1>Hello</h1><p>Draft</p><!--]-->', |
| 188 | + ); |
| 189 | + expect(declared).toEqual({ draft: { type: String, default: 'Draft' }, draftModifiers: {} }); |
| 190 | + expect(comp.props).toMatchObject(declared as object); |
| 191 | + }); |
| 192 | + |
| 193 | + it(`keeps array declarations when registering additional loader props in ${virtualId.slice(1)}`, async () => { |
| 194 | + writePage('index.vue', '<template><h1>Home</h1></template>'); |
| 195 | + const plugin = pagesPlugin(PAGES_DIR).find((p) => p.name === 'void-vue:pages') as |
| 196 | + | PagesPluginHooks |
| 197 | + | undefined; |
| 198 | + if (!plugin) { |
| 199 | + throw new Error('Expected void-vue:pages plugin.'); |
| 200 | + } |
| 201 | + const patchProps = await loadGeneratedPatchProps(plugin, virtualId); |
| 202 | + const declared = ['title', 'optional']; |
| 203 | + const comp = { props: declared }; |
| 204 | + |
| 205 | + patchProps(comp, { title: 'Hello', count: 1 }); |
| 206 | + |
| 207 | + expect(comp.props).toEqual(['title', 'optional', 'count']); |
| 208 | + expect(declared).toEqual(['title', 'optional']); |
| 209 | + }); |
| 210 | + } |
141 | 211 | }); |
142 | 212 |
|
143 | 213 | describe('pagesPlugin dev HMR', () => { |
|
0 commit comments