diff --git a/__tests__/unit/node/plugins/localSearchPlugin.test.ts b/__tests__/unit/node/plugins/localSearchPlugin.test.ts index 0ecab64e7..91f96ab48 100644 --- a/__tests__/unit/node/plugins/localSearchPlugin.test.ts +++ b/__tests__/unit/node/plugins/localSearchPlugin.test.ts @@ -226,7 +226,97 @@ describe('node/plugins/localSearchPlugin', () => { expect(warn).toHaveBeenCalledWith( expect.stringContaining('Failed to index broken.md for search') ) + + const file = path.join(root, 'index.md') + await writeFile(file, '# Home\n\n\n') + await (plugin.hotUpdate as any).call( + { environment: { name: 'client' } }, + { file } + ) + const previousIndex = loadIndex( + await (plugin.load as any)?.handler.call({}, '/@localSearchIndexroot') + ) + expect(previousIndex.search('healthytoken')).toHaveLength(1) }) + + test.each([ + [ + 'renames a heading', + '# Main\n\nstabletoken\n\n## Updated\n\ncurrenttoken\n' + ], + ['removes a section', '# Main\n\nstabletoken\n'], + ['empties a page', ''], + [ + 'disables search', + '---\nsearch: false\n---\n\n# Original\n\nobsoletetoken\n' + ], + ['produces empty custom search content', 'hiddenbycustomrender', true] + ] as [string, string, boolean?][])( + 'removes stale search results when an edit %s', + async (_, content, customRender) => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + await mkdir(path.join(root, '.vitepress')) + const file = path.join(root, 'changed.md') + await writeFile( + file, + '# Main\n\nstabletoken\n\n## Original\n\nobsoletetoken\n' + ) + await writeFile( + path.join(root, 'other.md'), + '# Other\n\nunrelatedtoken\n' + ) + await writeFile( + path.join(root, '.vitepress/config.mjs'), + `export default { + rewrites: { 'changed.md': 'zh/changed.md', 'other.md': 'zh/changed-extra.md' }, + locales: { zh: { lang: 'zh' } }, + themeConfig: { search: { provider: 'local' } } + }` + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + if (customRender) { + siteConfig.site.themeConfig.search.options = { + _render: (src: string, env: any, md: any) => + src === content ? '' : md.renderAsync(src, env) + } + } + const plugin = await localSearchPlugin(siteConfig) + await (plugin.configResolved as any)?.call( + {}, + { publicDir: siteConfig.publicDir } + ) + await (plugin.load as any)?.handler.call({}, '/@localSearchIndex') + const readIndex = async () => + loadIndex( + await (plugin.load as any)?.handler.call({}, '/@localSearchIndexzh') + ) + + expect((await readIndex()).search('obsoletetoken')).toMatchObject([ + { id: '/zh/changed.html#original' } + ]) + + await writeFile(file, content) + await (plugin.hotUpdate as any).call( + { environment: { name: 'client' } }, + { file } + ) + + const index = await readIndex() + expect(index.search('obsoletetoken')).toEqual([]) + expect(index.search('unrelatedtoken')).toMatchObject([ + { id: '/zh/changed-extra.html#other' } + ]) + if (content.includes('currenttoken')) { + expect(index.search('currenttoken')).toMatchObject([ + { id: '/zh/changed.html#updated' } + ]) + } + if (content.includes('stabletoken')) { + expect(index.search('stabletoken')).toHaveLength(1) + } + } + ) }) function loadIndex(serializedModule: string) { diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index b91ad0a10..1bb795194 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -81,6 +81,7 @@ export async function localSearchPlugin( } const indexByLocales = new Map>() + const indexedIdsByPage = new Map>() function getIndexByLocale(locale: string) { let index = indexByLocales.get(locale) @@ -157,6 +158,13 @@ export async function localSearchPlugin( ) return } + // Replace all sections from the previous render, including anchors that + // disappeared or pages that now opt out of search. + for (const id of indexedIdsByPage.get(page) ?? []) { + index.has(id) && index.discard(id) + } + const indexedIds = new Set() + indexedIdsByPage.set(page, indexedIds) if (!html) return const sections = // user provided generator @@ -175,6 +183,7 @@ export async function localSearchPlugin( title: titles.at(-1)!, titles: titles.slice(0, -1) }) + indexedIds.add(id) } }