mirror of https://github.com/vuejs/vitepress
feat: add support for asset sharding (#5432)
parent
b0452c5d8f
commit
5f69cc9d90
@ -0,0 +1,119 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'node:fs'
|
||||||
|
import { join, resolve, sep } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import { newPage, realErrors, waitForHydration, type TestPage } from './helpers'
|
||||||
|
|
||||||
|
const dir = resolve(fileURLToPath(import.meta.url), '..')
|
||||||
|
const dist = (...p: string[]) =>
|
||||||
|
join(dir, 'fixture/.vitepress/dist-sharded', ...p)
|
||||||
|
const origin = () => `http://localhost:${process.env['SHARDED_PORT']}`
|
||||||
|
|
||||||
|
// output-relative paths of everything under assets/
|
||||||
|
const files = () =>
|
||||||
|
readdirSync(dist('assets'), { recursive: true, withFileTypes: true })
|
||||||
|
.filter((e) => e.isFile())
|
||||||
|
.map((e) =>
|
||||||
|
join(e.parentPath, e.name)
|
||||||
|
.slice(dist('assets').length + 1)
|
||||||
|
.split(sep)
|
||||||
|
.join('/')
|
||||||
|
)
|
||||||
|
|
||||||
|
const isShardedPath = (f: string) => /^[0-2]\/[^/]+$/.test(f)
|
||||||
|
|
||||||
|
describe('assetsShards emit', () => {
|
||||||
|
test('page chunks land in numbered subdirectories', () => {
|
||||||
|
const pages = files().filter((f) => /\.md\.[\w-]+(\.lean)?\.js$/.test(f))
|
||||||
|
expect(pages.length).toBeGreaterThan(0)
|
||||||
|
expect(pages.every(isShardedPath)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('imported assets are sharded too, shared chunks and the app are not', () => {
|
||||||
|
const all = files()
|
||||||
|
const assets = all.filter(
|
||||||
|
(f) => /\.(png|woff2|css)$/.test(f) && !/^vp-icons\./.test(f)
|
||||||
|
)
|
||||||
|
expect(assets.length).toBeGreaterThan(0)
|
||||||
|
expect(assets.every(isShardedPath)).toBe(true)
|
||||||
|
expect(all.some((f) => /^chunks\/framework\.[\w-]+\.js$/.test(f))).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(all.some((f) => /^app\.[\w-]+\.js$/.test(f))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hash map entries carry the shard and resolve to real files', () => {
|
||||||
|
const map: Record<string, string> = JSON.parse(
|
||||||
|
readFileSync(dist('hashmap.json'), 'utf-8')
|
||||||
|
)
|
||||||
|
// hash map keys are lowercased page names, file names keep their case
|
||||||
|
const lower = new Set(files().map((f) => f.toLowerCase()))
|
||||||
|
expect(Object.keys(map).length).toBeGreaterThan(0)
|
||||||
|
for (const [page, entry] of Object.entries(map)) {
|
||||||
|
expect(entry).toMatch(/^[0-2]\/[\w-]+$/)
|
||||||
|
const [shard, hash] = entry.split('/')
|
||||||
|
const chunk = `${shard}/${page}.${hash}`.toLowerCase()
|
||||||
|
expect(lower.has(`${chunk}.js`)).toBe(true)
|
||||||
|
expect(lower.has(`${chunk}.lean.js`)).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preload links point at files that exist', () => {
|
||||||
|
const existing = new Set(files().map((f) => `assets/${f}`))
|
||||||
|
for (const page of [
|
||||||
|
'index',
|
||||||
|
'sub/page',
|
||||||
|
'sub/deep/page2',
|
||||||
|
'moved/target'
|
||||||
|
]) {
|
||||||
|
const links = [
|
||||||
|
...readFileSync(dist(`${page}.html`), 'utf-8').matchAll(
|
||||||
|
/<link rel="modulepreload" href="\/([^"]+)">/g
|
||||||
|
)
|
||||||
|
].map((m) => m[1]!)
|
||||||
|
expect(links.some((l) => l.includes('.md.'))).toBe(true)
|
||||||
|
for (const link of links) expect(existing.has(link)).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('assetsShards in the browser', () => {
|
||||||
|
let t: TestPage
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
t = await newPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await t.page.close()
|
||||||
|
await t.browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pages hydrate with sharded chunks', async () => {
|
||||||
|
await t.page.goto(`${origin()}/`)
|
||||||
|
await waitForHydration(t.page)
|
||||||
|
expect(await t.page.textContent('h1')).toContain('Home')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('client-side navigation loads page chunks from their shard', async () => {
|
||||||
|
await t.page.evaluate(() => ((window as any).__spa_marker = 1))
|
||||||
|
await t.page.click('.vp-doc a[href="/sub/page.html"]')
|
||||||
|
await t.page.waitForFunction(() =>
|
||||||
|
document.querySelector('h1')?.textContent?.includes('Sub page')
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
await t.page.evaluate(() => (window as any).__spa_marker === 1)
|
||||||
|
).toBe(true)
|
||||||
|
const chunk = await t.page.evaluate(() =>
|
||||||
|
performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((r) => r.name)
|
||||||
|
.find((n) => /\/assets\/\d+\/sub_page\.md\.[\w-]+\.js$/.test(n))
|
||||||
|
)
|
||||||
|
expect(chunk).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no console or page errors across the whole flow', () => {
|
||||||
|
expect(realErrors(t.errors)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in new issue