mirror of https://github.com/vuejs/vitepress
parent
515933bc5f
commit
fe6c1ff2e9
@ -0,0 +1,117 @@
|
||||
import { access, readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
test('batched SSR writes complete shared and per-page artifacts', async () => {
|
||||
if (!process.env.VITE_TEST_SSR_BATCH) return
|
||||
|
||||
const outDir = path.resolve('.vitepress/dist')
|
||||
const [
|
||||
indexHtml,
|
||||
dynamicHtml,
|
||||
staticHtml,
|
||||
scopedHtml,
|
||||
lastBatchHtml,
|
||||
notFoundHtml,
|
||||
iconsCss,
|
||||
hashmap
|
||||
] = await Promise.all([
|
||||
readFile(path.join(outDir, 'index.html'), 'utf8'),
|
||||
readFile(path.join(outDir, 'dynamic-routes/foo.html'), 'utf8'),
|
||||
readFile(path.join(outDir, 'ssr-static.html'), 'utf8'),
|
||||
readFile(path.join(outDir, 'ssr-scoped.html'), 'utf8'),
|
||||
readFile(path.join(outDir, 'text-literals/index.html'), 'utf8'),
|
||||
readFile(path.join(outDir, '404.html'), 'utf8'),
|
||||
readFile(path.join(outDir, 'vp-icons.css'), 'utf8'),
|
||||
readFile(path.join(outDir, 'hashmap.json'), 'utf8')
|
||||
])
|
||||
|
||||
expect(indexHtml).toContain('<div id="app">')
|
||||
expect(dynamicHtml).toContain('<title>Foo - transformed | Example</title>')
|
||||
expect(dynamicHtml).toContain('name="ssr-batch-hook-state"')
|
||||
expect(dynamicHtml).toContain(
|
||||
'data-ssr-batch-transform="dynamic-routes/foo.md"'
|
||||
)
|
||||
expect(staticHtml).toContain('<title>Static batching page | Example</title>')
|
||||
expect(staticHtml).toContain('<h1 id="static-batching-page"')
|
||||
expect(staticHtml).toContain(
|
||||
'<p data-static-batch-marker="preserved">Static HTML marker</p>'
|
||||
)
|
||||
expect(staticHtml).toContain(
|
||||
'<span class="VPBadge warning">static badge</span>'
|
||||
)
|
||||
expect(staticHtml).toContain('<!-- static comment preserved -->')
|
||||
expect(staticHtml).toContain(
|
||||
'<img data-static-public-asset src="/batch-public.txt" alt="Static public asset">'
|
||||
)
|
||||
expect(scopedHtml).toContain('Scoped module identity')
|
||||
expect(scopedHtml).toMatch(/class="scoped-batch-marker" data-v-[\da-f]+/)
|
||||
expect(staticHtml).toMatch(
|
||||
/<meta name="ssr-batch-hook-state" content="\d+:ssr-static\.md">/
|
||||
)
|
||||
expect(staticHtml).toContain(
|
||||
'<meta name="ssr-batch-after-config-resolve" content="coordinator mutation retained">'
|
||||
)
|
||||
expect(staticHtml).toContain('data-ssr-batch-transform="ssr-static.md"')
|
||||
expect(lastBatchHtml).toContain('<h1 id="text-literals"')
|
||||
expect(dynamicHtml).toContain('<pre class="params">')
|
||||
expect(dynamicHtml).toContain('"id": "foo"')
|
||||
expect(dynamicHtml).not.toContain('{{ $params }}')
|
||||
expect(notFoundHtml).toContain('<title>404 | Example</title>')
|
||||
expect(iconsCss).toContain('.vpi-social-github')
|
||||
expect(hashmap).not.toContain('undefined')
|
||||
await expect(
|
||||
access(path.join(outDir, 'batch-public.txt'))
|
||||
).resolves.toBeUndefined()
|
||||
if (!process.env.DEBUG) {
|
||||
await expect(access(path.resolve('.vitepress/.temp'))).rejects.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
test('resolved config-file hooks preserve legacy physical Markdown SSR semantics', async () => {
|
||||
if (!process.env.VITE_TEST_BUILD) return
|
||||
|
||||
const html = await readFile(
|
||||
path.resolve('.vitepress/dist/ssr-plugin-safety.html'),
|
||||
'utf8'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<p data-resolved-load-environment="ssr">physical Markdown load hook</p>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<p data-resolved-transform-mode="server">environment-sensitive Markdown transform</p>'
|
||||
)
|
||||
expect(html).not.toContain('data-resolved-transform-mode="client"')
|
||||
})
|
||||
|
||||
test('the static SSR fast path hydrates with normal client-page semantics', async () => {
|
||||
if (!process.env.VITE_TEST_SSR_BATCH) return
|
||||
|
||||
await goto('/ssr-static.html')
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByRole('heading', { level: 1, name: 'Static batching page' })
|
||||
.isVisible()
|
||||
).toBe(true)
|
||||
expect(
|
||||
await page.locator('[data-static-batch-marker="preserved"]').textContent()
|
||||
).toBe('Static HTML marker')
|
||||
expect(await page.locator('.VPBadge.warning').textContent()).toBe(
|
||||
'static badge'
|
||||
)
|
||||
expect(
|
||||
await page.locator('[data-static-public-asset]').getAttribute('src')
|
||||
).toBe('/batch-public.txt')
|
||||
})
|
||||
|
||||
test('scoped pages preserve client and SSR module identity', async () => {
|
||||
if (!process.env.VITE_TEST_SSR_BATCH) return
|
||||
|
||||
await goto('/ssr-scoped.html')
|
||||
|
||||
const marker = page.locator('.scoped-batch-marker')
|
||||
expect(await marker.textContent()).toBe('Scoped module identity')
|
||||
expect(
|
||||
await marker.evaluate((element) => getComputedStyle(element).color)
|
||||
).toBe('rgb(1, 2, 3)')
|
||||
})
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Resolved plugin artifact safety
|
||||
---
|
||||
|
||||
# Resolved plugin artifact safety
|
||||
|
||||
This page is transformed by a plugin loaded from `vite.config.ts`.
|
||||
@ -0,0 +1,48 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
const artifactSafetyPageRE = /(?:^|\/)ssr-plugin-safety[.]md$/
|
||||
|
||||
export default defineConfig({
|
||||
publicDir: process.env.VITE_TEST_SSR_PLUGIN_PARITY
|
||||
? 'batch-public'
|
||||
: undefined,
|
||||
resolve: process.env.VITE_TEST_SSR_PLUGIN_PARITY
|
||||
? {
|
||||
alias: {
|
||||
'/vitepress.png': path.resolve(
|
||||
import.meta.dirname,
|
||||
'public/vitepress.png'
|
||||
)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
plugins: [
|
||||
{
|
||||
name: 'test:config-file-artifact-safety',
|
||||
apply: 'build',
|
||||
applyToEnvironment(environment) {
|
||||
const environmentName = environment.name
|
||||
return {
|
||||
name: `test:resolved-artifact-safety:${environmentName}`,
|
||||
enforce: 'pre',
|
||||
load: {
|
||||
filter: { id: artifactSafetyPageRE },
|
||||
async handler(id) {
|
||||
const source = await readFile(id, 'utf8')
|
||||
return `${source}\n<p data-resolved-load-environment="${environmentName}">physical Markdown load hook</p>`
|
||||
}
|
||||
},
|
||||
transform: {
|
||||
filter: { id: artifactSafetyPageRE },
|
||||
handler(code, _id, options) {
|
||||
const mode = options?.ssr ? 'server' : 'client'
|
||||
return `${code}\n<p data-resolved-transform-mode="${mode}">environment-sensitive Markdown transform</p>`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -0,0 +1,79 @@
|
||||
import { resolveConfig } from 'node/config'
|
||||
import {
|
||||
canCompileSsrPageArtifact,
|
||||
canReuseSsrPageArtifactWithPlugins
|
||||
} from 'node/markdownToVue'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
describe('SSR artifact plugin safety', () => {
|
||||
let root: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (root) {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
root = undefined
|
||||
}
|
||||
})
|
||||
|
||||
test('checks plugins discovered in resolved environments and honors the explicit safety contract', async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-resolved-hooks-'))
|
||||
const file = path.join(root, 'index.md')
|
||||
await writeFile(file, '# Resolved plugin\n')
|
||||
|
||||
const siteConfig = await resolveConfig(root, 'build', 'production')
|
||||
siteConfig.vite = { plugins: [] }
|
||||
expect(canCompileSsrPageArtifact(siteConfig, file)).toBe(true)
|
||||
|
||||
const resolvedConfigFilePlugin = {
|
||||
name: 'config-file-markdown-load',
|
||||
load: {
|
||||
filter: { id: /[.]md$/ },
|
||||
handler() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(
|
||||
canReuseSsrPageArtifactWithPlugins([resolvedConfigFilePlugin], file)
|
||||
).toBe(false)
|
||||
|
||||
const explicitlySafeResolvedPlugin = {
|
||||
...resolvedConfigFilePlugin,
|
||||
api: { vitepress: { ssrArtifactSafe: true } }
|
||||
}
|
||||
expect(
|
||||
canReuseSsrPageArtifactWithPlugins([explicitlySafeResolvedPlugin], file)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('checks the plugin produced by applyToEnvironment after resolution', async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-environment-hooks-'))
|
||||
const file = path.join(root, 'index.md')
|
||||
await writeFile(file, '# Per-environment plugin\n')
|
||||
|
||||
const environmentPlugin = {
|
||||
name: 'config-file-per-environment',
|
||||
applyToEnvironment(environment: { name: string }) {
|
||||
return {
|
||||
name: `config-file-per-environment:${environment.name}`,
|
||||
enforce: 'pre' as const,
|
||||
transform: {
|
||||
filter: { id: /[.]md$/ },
|
||||
handler(code: string, _id: string, options?: { ssr?: boolean }) {
|
||||
return `${code}\n${options?.ssr ? 'server' : 'client'}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const resolvedSsrPlugin = environmentPlugin.applyToEnvironment({
|
||||
name: 'ssr'
|
||||
})
|
||||
|
||||
expect(canReuseSsrPageArtifactWithPlugins([resolvedSsrPlugin], file)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,104 @@
|
||||
import { resolveConfig } from 'node/config'
|
||||
import { PageArtifactStore } from 'node/pageArtifacts'
|
||||
import { createVitePressPlugin } from 'node/plugin'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
describe('resolved plugin artifact safety', () => {
|
||||
let root: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (root) {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
root = undefined
|
||||
}
|
||||
})
|
||||
|
||||
test('uses resolved-config plugins when deciding whether an artifact is reusable', async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'vitepress-resolved-plugins-'))
|
||||
const file = path.join(root, 'index.md')
|
||||
const source = '# Resolved plugin safety\n'
|
||||
await writeFile(file, source)
|
||||
|
||||
const siteConfig = await resolveConfig(root, 'build', 'production')
|
||||
siteConfig.markdown = { cache: false }
|
||||
siteConfig.vite = { plugins: [] }
|
||||
|
||||
const compileWithResolvedPlugin = async (
|
||||
resolvedPlugin: Plugin,
|
||||
namespace: string
|
||||
) => {
|
||||
const store = new PageArtifactStore(siteConfig.cacheDir, { namespace })
|
||||
const plugins = await createVitePressPlugin(
|
||||
siteConfig,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
coordinatorClient: true,
|
||||
pageArtifactStore: store,
|
||||
skipGitScan: true
|
||||
}
|
||||
)
|
||||
const vitePressPlugin = plugins[0] as Plugin
|
||||
const configResolved = getHookHandler(vitePressPlugin.configResolved)
|
||||
await configResolved.call(undefined, {
|
||||
base: '/',
|
||||
build: { rolldownOptions: { plugins: [] } },
|
||||
command: 'build',
|
||||
plugins: [vitePressPlugin, resolvedPlugin],
|
||||
publicDir: siteConfig.publicDir
|
||||
} as any)
|
||||
|
||||
const transform = getHookHandler(vitePressPlugin.transform as any)
|
||||
await transform.call(
|
||||
{
|
||||
addWatchFile() {},
|
||||
environment: { mode: 'build', name: 'client' }
|
||||
},
|
||||
source,
|
||||
file
|
||||
)
|
||||
return store.getCurrentMetadata('index.md')
|
||||
}
|
||||
|
||||
const resolvedConfigFilePlugin: Plugin = {
|
||||
name: 'test:resolved-config-file-load',
|
||||
load: {
|
||||
filter: { id: /[.]md$/ },
|
||||
handler() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(siteConfig.vite.plugins).not.toContain(resolvedConfigFilePlugin)
|
||||
await expect(
|
||||
compileWithResolvedPlugin(resolvedConfigFilePlugin, 'resolved-unsafe')
|
||||
).resolves.toEqual({
|
||||
staticPage: false,
|
||||
requiresSourceModuleIdentity: true
|
||||
})
|
||||
|
||||
const explicitlySafePlugin = {
|
||||
...resolvedConfigFilePlugin,
|
||||
api: { vitepress: { ssrArtifactSafe: true } }
|
||||
} as Plugin
|
||||
await expect(
|
||||
compileWithResolvedPlugin(explicitlySafePlugin, 'resolved-safe')
|
||||
).resolves.toEqual({
|
||||
staticPage: true,
|
||||
requiresSourceModuleIdentity: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getHookHandler<T extends (...args: any[]) => any>(
|
||||
hook: T | { handler: T } | undefined
|
||||
): T {
|
||||
if (!hook) throw new Error('Expected plugin hook.')
|
||||
return typeof hook === 'function' ? hook : hook.handler
|
||||
}
|
||||
Loading…
Reference in new issue