pull/5454/merge
lllleolin-max 3 days ago committed by GitHub
commit 88563e0ee5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,4 +1,4 @@
import { writeFile, unlink } from 'node:fs/promises' import { readFile, writeFile, unlink } from 'node:fs/promises'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
describe('static data file support in vite 3', () => { describe('static data file support in vite 3', () => {
@ -86,6 +86,48 @@ describe('static data file support in vite 3', () => {
} }
}) })
test.runIf(!process.env.VITE_TEST_BUILD)(
'hmr updates content rendered from included files',
async () => {
const foo = fileURLToPath(new URL('./content/foo.md', import.meta.url))
const bar = fileURLToPath(new URL('./content/bar.md', import.meta.url))
const originalFoo = await readFile(foo, 'utf8')
const originalBar = await readFile(bar, 'utf8')
try {
await writeFile(
foo,
originalFoo.replace('Hello', '<!-- @include: ./bar.md -->')
)
await page.waitForFunction(() => {
const data = JSON.parse(
document.querySelector('pre#content')!.textContent!
)
return data.some(
(item: { url: string; src: string }) =>
item.url.endsWith('/foo.html') && item.src.includes('@include:')
)
})
await writeFile(bar, originalBar.replace('Hello', 'Updated include'))
await page.waitForFunction(() => {
const data = JSON.parse(
document.querySelector('pre#content')!.textContent!
)
return data.some(
(item: { url: string; html: string; excerpt: string }) =>
item.url.endsWith('/foo.html') &&
item.html.includes('Updated include') &&
item.excerpt.includes('Updated include')
)
})
} finally {
await writeFile(bar, originalBar)
await writeFile(foo, originalFoo)
}
}
)
/* /*
MODIFY a.json with { a: false } MODIFY a.json with { a: false }
this should trigger a hmr update and the content should be updated to [{ a: false }, { b: true }] this should trigger a hmr update and the content should be updated to [{ a: false }, { b: true }]

@ -1,4 +1,4 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { mkdir, mkdtemp, rm, stat, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import path from 'node:path' import path from 'node:path'
@ -82,6 +82,105 @@ describe('node/contentLoader', () => {
expect(data[0].excerpt).toContain('Intro says My Post.') expect(data[0].excerpt).toContain('Intro says My Post.')
}) })
test.each([
['nested include', '<!-- @include: ./partial.md -->'],
['code snippet', '<<< ./included.txt']
])(
'refreshes cached HTML and excerpts after a %s changes',
async (_, src) => {
await setup(false)
const included = path.join(root!, 'included.txt')
await writeFile(included, 'Before the edit')
await writeFile(
path.join(root!, 'partial.md'),
'<!-- @include: ./included.txt -->'
)
await writeFile(path.join(root!, 'post.md'), `${src}\n\n---\n\nBody`)
const loader = createContentLoader(['post.md', 'other.md'], {
render: true,
excerpt: true
})
const initial = await loader.load()
const initialPost = initial.find((page) => page.url === '/post.html')!
const initialOther = initial.find((page) => page.url === '/other.html')!
expect(initialPost.html).toContain('Before the edit')
expect(initialPost.excerpt).toContain('Before the edit')
const unchanged = await loader.load()
expect(unchanged.find((page) => page.url === '/post.html')).toBe(
initialPost
)
// Change only the dependency, keeping the page's source and mtime intact.
const timestamp = (await stat(included)).mtimeMs + 1000
await writeFile(included, 'After the edit')
await utimes(included, timestamp / 1000, timestamp / 1000)
const updated = await loader.load()
const updatedPost = updated.find((page) => page.url === '/post.html')!
expect(updatedPost.html).toContain('After the edit')
expect(updatedPost.excerpt).toContain('After the edit')
expect(updatedPost.html).not.toContain('Before the edit')
expect(updated.find((page) => page.url === '/other.html')).toBe(
initialOther
)
}
)
test('tracks dependencies rendered only by a custom excerpt', async () => {
await setup(false)
const included = path.join(root!, 'included.txt')
await writeFile(included, 'Before the edit')
const loader = createContentLoader('index.md', {
excerpt(file) {
file.excerpt = '<!-- @include: ./included.txt -->'
}
})
const initial = await loader.load()
expect(initial[0].html).toBeUndefined()
expect(initial[0].excerpt).toContain('Before the edit')
const timestamp = (await stat(included)).mtimeMs + 1000
await writeFile(included, 'After the edit')
await utimes(included, timestamp / 1000, timestamp / 1000)
expect((await loader.load())[0].excerpt).toContain('After the edit')
})
test('reports deleted dependencies instead of returning cached HTML', async () => {
await setup(false)
await writeFile(
path.join(root!, 'post.md'),
'<!-- @include: ./other.md -->'
)
const loader = createContentLoader('post.md', { render: true })
expect((await loader.load())[0].html).toContain('Other')
await rm(path.join(root!, 'other.md'))
await expect(loader.load()).rejects.toThrow('Include file not found:')
})
test('refreshes silent includes when a missing file is created or deleted', async () => {
await setup(false)
const config = (global as any).VITEPRESS_CONFIG
config.markdown = { include: { silent: true } }
config.logger.warn = vi.fn()
await writeFile(
path.join(root!, 'post.md'),
'<!-- @include: ./missing.md -->'
)
const loader = createContentLoader('post.md', { render: true })
expect((await loader.load())[0].html).not.toContain('Created content')
const included = path.join(root!, 'missing.md')
await writeFile(included, 'Created content')
expect((await loader.load())[0].html).toContain('Created content')
await rm(included)
expect((await loader.load())[0].html).not.toContain('Created content')
})
test.each([false, true])( test.each([false, true])(
'uses the rewritten locale for rendered content and excerpts (render: %s)', 'uses the rewritten locale for rendered content and excerpts (render: %s)',
async (render) => { async (render) => {

@ -91,7 +91,14 @@ export function createContentLoader<T = ContentData[]>(
) )
} }
const cache = new Map<string, { data: any; timestamp: number }>() const cache = new Map<
string,
{
data: ContentData
timestamp: number
dependencies: Map<string, number | undefined>
}
>()
watch = normalizeGlob(watch, config.srcDir) watch = normalizeGlob(watch, config.srcDir)
@ -122,7 +129,20 @@ export function createContentLoader<T = ContentData[]>(
const timestamp = (await stat(file)).mtimeMs const timestamp = (await stat(file)).mtimeMs
const cached = cache.get(file) const cached = cache.get(file)
if (cached && timestamp === cached.timestamp) return cached.data if (
cached &&
timestamp === cached.timestamp &&
(
await Promise.all(
[...cached.dependencies].map(
async ([file, timestamp]) =>
(await getDependencyTimestamp(file)) === timestamp
)
)
).every(Boolean)
) {
return cached.data
}
const src = await readTextFile(file) const src = await readTextFile(file)
@ -172,7 +192,15 @@ export function createContentLoader<T = ContentData[]>(
url url
} }
cache.set(file, { data, timestamp }) // Both the full render and the excerpt can import other files.
const dependencies = new Map(
await Promise.all(
[...new Set(env.includes)].map(async (file) => {
return [file, await getDependencyTimestamp(file)] as const
})
)
)
cache.set(file, { data, timestamp, dependencies })
return data return data
}, },
{ concurrency: config.buildConcurrency } { concurrency: config.buildConcurrency }
@ -183,3 +211,14 @@ export function createContentLoader<T = ContentData[]>(
} }
} }
} }
async function getDependencyTimestamp(file: string) {
try {
return (await stat(file)).mtimeMs
} catch (error) {
// Silent includes can refer to missing files. Their creation must also
// invalidate the cache, while deleted files need to be rendered again.
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
throw error
}
}

Loading…
Cancel
Save