vocs 2.1.12 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/internal/config.d.ts +246 -9
  2. package/dist/internal/config.d.ts.map +1 -1
  3. package/dist/internal/config.js +38 -9
  4. package/dist/internal/config.js.map +1 -1
  5. package/dist/internal/llms.d.ts.map +1 -1
  6. package/dist/internal/llms.js +5 -1
  7. package/dist/internal/llms.js.map +1 -1
  8. package/dist/internal/markdown-imports.d.ts +2 -0
  9. package/dist/internal/markdown-imports.d.ts.map +1 -0
  10. package/dist/internal/markdown-imports.js +103 -0
  11. package/dist/internal/markdown-imports.js.map +1 -0
  12. package/dist/internal/search.client.d.ts +6 -0
  13. package/dist/internal/search.client.d.ts.map +1 -1
  14. package/dist/internal/search.client.js +23 -0
  15. package/dist/internal/search.client.js.map +1 -1
  16. package/dist/internal/search.d.ts +2 -1
  17. package/dist/internal/search.d.ts.map +1 -1
  18. package/dist/internal/search.js +10 -14
  19. package/dist/internal/search.js.map +1 -1
  20. package/dist/internal/vite-plugins.js +1 -1
  21. package/dist/internal/vite-plugins.js.map +1 -1
  22. package/dist/react/Link.d.ts.map +1 -1
  23. package/dist/react/Link.js +11 -6
  24. package/dist/react/Link.js.map +1 -1
  25. package/dist/react/internal/Search.d.ts.map +1 -1
  26. package/dist/react/internal/Search.js +5 -7
  27. package/dist/react/internal/Search.js.map +1 -1
  28. package/dist/server/openapi/assets.generated.js +4 -4
  29. package/dist/server/openapi/assets.generated.js.map +1 -1
  30. package/dist/styles/twoslash.css +1 -1
  31. package/package.json +1 -1
  32. package/src/internal/config.ts +294 -21
  33. package/src/internal/llms.test.ts +35 -0
  34. package/src/internal/llms.ts +6 -1
  35. package/src/internal/markdown-imports.ts +154 -0
  36. package/src/internal/search.client.ts +29 -0
  37. package/src/internal/search.test.ts +195 -0
  38. package/src/internal/search.ts +14 -14
  39. package/src/internal/vite-plugins.ts +1 -1
  40. package/src/react/Link.tsx +12 -7
  41. package/src/react/internal/Search.tsx +7 -10
  42. package/src/server/openapi/assets.generated.ts +4 -4
  43. package/src/styles/twoslash.css +1 -1
@@ -0,0 +1,154 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+ import type * as Estree from 'estree'
4
+ import type * as MdAst from 'mdast'
5
+ import remarkMdx from 'remark-mdx'
6
+ import remarkParse from 'remark-parse'
7
+ import { unified } from 'unified'
8
+
9
+ type MdxEsm = MdAst.RootContent & {
10
+ type: 'mdxjsEsm'
11
+ data?: {
12
+ estree?: Estree.Program
13
+ }
14
+ }
15
+
16
+ type MdxJsxElement = MdAst.RootContent & {
17
+ type: 'mdxJsxFlowElement' | 'mdxJsxTextElement'
18
+ attributes?: unknown[]
19
+ children?: MdAst.RootContent[]
20
+ name?: string | null
21
+ }
22
+
23
+ type MarkdownImport = {
24
+ source: string
25
+ }
26
+
27
+ type Replacement = {
28
+ end: number
29
+ start: number
30
+ value: string
31
+ }
32
+
33
+ export function inlineMarkdownImports(source: string, filePath: string): string {
34
+ return inline(source, path.resolve(filePath), new Set())
35
+ }
36
+
37
+ function inline(source: string, filePath: string, seen: Set<string>): string {
38
+ if (seen.has(filePath)) return ''
39
+ seen.add(filePath)
40
+
41
+ let tree: MdAst.Root
42
+ try {
43
+ tree = unified().use(remarkParse).use(remarkMdx).parse(source)
44
+ } catch {
45
+ return source
46
+ }
47
+
48
+ const imports = new Map<string, MarkdownImport>()
49
+ const replacements: Replacement[] = []
50
+
51
+ collectImports(tree.children, imports, replacements)
52
+ collectComponentReplacements(tree, imports, filePath, seen, replacements)
53
+
54
+ if (replacements.length === 0) return source
55
+ return applyReplacements(source, replacements)
56
+ }
57
+
58
+ function collectImports(
59
+ children: MdAst.RootContent[],
60
+ imports: Map<string, MarkdownImport>,
61
+ replacements: Replacement[],
62
+ ) {
63
+ for (const node of children) {
64
+ if (node.type !== 'mdxjsEsm') continue
65
+
66
+ const mdxEsm = node as MdxEsm
67
+ const declarations = mdxEsm.data?.estree?.body.filter(
68
+ (statement): statement is Estree.ImportDeclaration => statement.type === 'ImportDeclaration',
69
+ )
70
+ if (!declarations || declarations.length === 0) continue
71
+
72
+ let markdownDeclarations = 0
73
+ for (const declaration of declarations) {
74
+ const source = declaration.source.value
75
+ if (typeof source !== 'string' || !isMarkdownImport(source)) continue
76
+ markdownDeclarations++
77
+
78
+ const specifier = declaration.specifiers.find(
79
+ (specifier): specifier is Estree.ImportDefaultSpecifier =>
80
+ specifier.type === 'ImportDefaultSpecifier',
81
+ )
82
+ if (!specifier) continue
83
+ imports.set(specifier.local.name, { source })
84
+ }
85
+
86
+ if (markdownDeclarations === declarations.length) {
87
+ const replacement = toReplacement(node, '')
88
+ if (replacement) replacements.push(replacement)
89
+ }
90
+ }
91
+ }
92
+
93
+ function collectComponentReplacements(
94
+ node: MdAst.Root | MdAst.RootContent,
95
+ imports: Map<string, MarkdownImport>,
96
+ filePath: string,
97
+ seen: Set<string>,
98
+ replacements: Replacement[],
99
+ ) {
100
+ if (isMdxJsxElement(node)) {
101
+ const name = node.name ?? undefined
102
+ const markdownImport = name ? imports.get(name) : undefined
103
+ if (markdownImport && isStaticMdxComponent(node)) {
104
+ const importedPath = resolveMarkdownImport(markdownImport.source, filePath)
105
+ if (importedPath) {
106
+ const importedSource = fs.readFileSync(importedPath, 'utf-8')
107
+ const value = inline(importedSource, importedPath, new Set(seen))
108
+ const replacement = toReplacement(node, value)
109
+ if (replacement) replacements.push(replacement)
110
+ }
111
+ }
112
+ }
113
+
114
+ if ('children' in node && Array.isArray(node.children))
115
+ for (const child of node.children)
116
+ collectComponentReplacements(child, imports, filePath, seen, replacements)
117
+ }
118
+
119
+ function isMdxJsxElement(node: MdAst.Root | MdAst.RootContent): node is MdxJsxElement {
120
+ return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'
121
+ }
122
+
123
+ function isStaticMdxComponent(node: MdxJsxElement): boolean {
124
+ return (node.attributes?.length ?? 0) === 0 && (node.children?.length ?? 0) === 0
125
+ }
126
+
127
+ function resolveMarkdownImport(source: string, filePath: string): string | undefined {
128
+ if (!source.startsWith('.') && !path.isAbsolute(source)) return undefined
129
+
130
+ const resolved = path.isAbsolute(source) ? source : path.resolve(path.dirname(filePath), source)
131
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved
132
+ return undefined
133
+ }
134
+
135
+ function isMarkdownImport(source: string): boolean {
136
+ return /\.(md|mdx)$/.test(source)
137
+ }
138
+
139
+ function toReplacement(
140
+ node: MdAst.Root | MdAst.RootContent,
141
+ value: string,
142
+ ): Replacement | undefined {
143
+ const start = node.position?.start.offset
144
+ const end = node.position?.end.offset
145
+ if (start === undefined || end === undefined) return undefined
146
+ return { end, start, value }
147
+ }
148
+
149
+ function applyReplacements(source: string, replacements: Replacement[]): string {
150
+ let result = source
151
+ for (const replacement of replacements.sort((a, b) => b.start - a.start))
152
+ result = result.slice(0, replacement.start) + replacement.value + result.slice(replacement.end)
153
+ return result
154
+ }
@@ -1,3 +1,9 @@
1
+ import type {
2
+ Options as MiniSearchOptions,
3
+ SearchOptions as MiniSearchSearchOptions,
4
+ } from 'minisearch'
5
+ import type * as Config from './config.js'
6
+
1
7
  export const searchFields = ['category', 'subtitle', 'text', 'title', 'titles'] as const
2
8
  export const storeFields = [
3
9
  'category',
@@ -40,3 +46,26 @@ export function tokenize(text: string): string[] {
40
46
 
41
47
  return tokens.filter((w) => w.length > 0)
42
48
  }
49
+
50
+ export namespace SearchConfig {
51
+ export function getIndexOptions(config?: Config.Config): MiniSearchOptions {
52
+ const index = config?.search.index ?? {}
53
+ return {
54
+ ...index,
55
+ fields: index.fields ? [...index.fields] : [...searchFields],
56
+ storeFields: mergeStoreFields(index.storeFields),
57
+ tokenize: index.tokenize ?? tokenize,
58
+ }
59
+ }
60
+
61
+ export function getQueryOptions(config: Config.Config): MiniSearchSearchOptions {
62
+ return {
63
+ ...config.search.query,
64
+ tokenize: config.search.query?.tokenize ?? config.search.index?.tokenize ?? tokenize,
65
+ }
66
+ }
67
+
68
+ function mergeStoreFields(fields: readonly string[] | undefined): string[] {
69
+ return [...new Set([...storeFields, ...(fields ?? [])])]
70
+ }
71
+ }
@@ -1,3 +1,6 @@
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
1
4
  import type * as MdAst from 'mdast'
2
5
  import { describe, expect, it } from 'vitest'
3
6
  import * as Config from './config.js'
@@ -859,6 +862,40 @@ Some content here.
859
862
  })
860
863
  })
861
864
 
865
+ describe('SearchDocuments.fromConfig', () => {
866
+ it('indexes imported markdown content from file-backed pages', async () => {
867
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vocs-search-'))
868
+ const pagesDir = path.join(rootDir, 'src/pages')
869
+ fs.mkdirSync(pagesDir, { recursive: true })
870
+
871
+ fs.writeFileSync(
872
+ path.join(rootDir, 'notes.md'),
873
+ '# Notes\n\nSome indexable prose about widgets and links.',
874
+ )
875
+ fs.writeFileSync(
876
+ path.join(pagesDir, 'notes.mdx'),
877
+ `---
878
+ title: Notes
879
+ ---
880
+
881
+ import Notes from '../../notes.md'
882
+
883
+ <Notes />`,
884
+ )
885
+
886
+ const documents = await Search.SearchDocuments.fromConfig(Config.define({ rootDir }))
887
+
888
+ expect(documents).toContainEqual(
889
+ expect.objectContaining({
890
+ href: '/notes#notes',
891
+ text: ' Some indexable prose about widgets and links.',
892
+ title: 'Notes',
893
+ type: 'page',
894
+ }),
895
+ )
896
+ })
897
+ })
898
+
862
899
  describe('Search fields and storeFields', () => {
863
900
  it('exports search fields', () => {
864
901
  expect(Search.searchFields).toMatchInlineSnapshot(`
@@ -902,6 +939,65 @@ describe('Config.search defaults', () => {
902
939
  expect(config.search.fuzzy).toBe(0.2)
903
940
  expect(config.search.prefix).toBe(true)
904
941
  expect(typeof config.search.boostDocument).toBe('function')
942
+ expect(config.search.query?.boost).toEqual(config.search.boost)
943
+ expect(config.search.query?.fuzzy).toBe(0.2)
944
+ expect(config.search.query?.prefix).toBe(true)
945
+ })
946
+
947
+ it('normalizes legacy search options into query options', () => {
948
+ const config = Config.define({
949
+ search: {
950
+ boost: { text: 10 },
951
+ combineWith: 'OR',
952
+ fuzzy: false,
953
+ prefix: false,
954
+ },
955
+ })
956
+
957
+ expect(config.search.query?.boost).toMatchObject({ title: 4, text: 10 })
958
+ expect(config.search.query?.combineWith).toBe('OR')
959
+ expect(config.search.query?.fuzzy).toBe(false)
960
+ expect(config.search.query?.prefix).toBe(false)
961
+ })
962
+ })
963
+
964
+ describe('SearchConfig', () => {
965
+ it('returns default index options', () => {
966
+ expect(Search.SearchConfig.getIndexOptions(config)).toMatchObject({
967
+ fields: ['category', 'subtitle', 'text', 'title', 'titles'],
968
+ storeFields: [
969
+ 'category',
970
+ 'href',
971
+ 'searchPriority',
972
+ 'subtitle',
973
+ 'text',
974
+ 'title',
975
+ 'titles',
976
+ 'type',
977
+ ],
978
+ })
979
+ })
980
+
981
+ it('keeps required store fields when custom store fields are configured', () => {
982
+ const config = Config.define({
983
+ search: {
984
+ index: {
985
+ storeFields: ['href', 'custom'],
986
+ },
987
+ },
988
+ })
989
+
990
+ expect(Search.SearchConfig.getIndexOptions(config).storeFields).toEqual([
991
+ 'category',
992
+ 'href',
993
+ 'searchPriority',
994
+ 'subtitle',
995
+ 'text',
996
+ 'title',
997
+ 'titles',
998
+ 'type',
999
+ 'custom',
1000
+ ])
905
1001
  })
906
1002
  })
907
1003
 
@@ -1006,6 +1102,105 @@ describe('SearchIndex.fromSearchDocuments', () => {
1006
1102
  expect(results[0]?.score).toBeGreaterThan(results[1]?.score ?? 0)
1007
1103
  })
1008
1104
 
1105
+ it('indexes custom virtual fields', () => {
1106
+ const docs: SearchDocuments.Document[] = [
1107
+ {
1108
+ category: '',
1109
+ href: '/docs/runtime-api',
1110
+ id: '/docs/runtime-api.mdx#runtime-api',
1111
+ searchPriority: undefined,
1112
+ subtitle: '',
1113
+ text: 'Reference content.',
1114
+ title: 'Runtime API',
1115
+ titles: [],
1116
+ type: 'page',
1117
+ },
1118
+ ]
1119
+ const config = Config.define({
1120
+ search: {
1121
+ index: {
1122
+ fields: ['path'],
1123
+ extractField(document, fieldName) {
1124
+ if (fieldName === 'path') return document.href
1125
+ return document[fieldName as keyof typeof document]
1126
+ },
1127
+ },
1128
+ },
1129
+ })
1130
+
1131
+ const index = SearchIndex.fromSearchDocuments(docs, config)
1132
+
1133
+ expect(index.search('runtime', Search.SearchConfig.getQueryOptions(config))[0]?.id).toBe(
1134
+ '/docs/runtime-api.mdx#runtime-api',
1135
+ )
1136
+ })
1137
+
1138
+ it('stores custom fields', () => {
1139
+ const docs: SearchDocuments.Document[] = [
1140
+ {
1141
+ category: 'Docs',
1142
+ href: '/guide',
1143
+ id: '/docs/guide.mdx#guide',
1144
+ searchPriority: undefined,
1145
+ subtitle: '',
1146
+ text: 'Install instructions.',
1147
+ title: 'Guide',
1148
+ titles: [],
1149
+ type: 'page',
1150
+ },
1151
+ ]
1152
+ const config = Config.define({
1153
+ search: {
1154
+ index: {
1155
+ storeFields: ['path'],
1156
+ extractField(document, fieldName) {
1157
+ if (fieldName === 'path') return document.href
1158
+ return document[fieldName as keyof typeof document]
1159
+ },
1160
+ },
1161
+ },
1162
+ })
1163
+
1164
+ const index = SearchIndex.fromSearchDocuments(docs, config)
1165
+ const result = index.search('guide', Search.SearchConfig.getQueryOptions(config))[0]
1166
+
1167
+ expect(result).toMatchObject({ href: '/guide', path: '/guide', title: 'Guide' })
1168
+ })
1169
+
1170
+ it('applies query fields, weights, and filter at runtime', () => {
1171
+ const docs = [
1172
+ ...buildDoc(
1173
+ '/docs/title.mdx',
1174
+ '/title',
1175
+ `# Alpha
1176
+
1177
+ No keyword.
1178
+ `,
1179
+ ),
1180
+ ...buildDoc(
1181
+ '/docs/text.mdx',
1182
+ '/text',
1183
+ `# Other
1184
+
1185
+ Alpha keyword.
1186
+ `,
1187
+ ),
1188
+ ]
1189
+ const config = Config.define({
1190
+ search: {
1191
+ query: {
1192
+ fields: ['text'],
1193
+ filter: (result) => result.id !== '/docs/text.mdx#other',
1194
+ weights: { fuzzy: 0.5, prefix: 0.5 },
1195
+ },
1196
+ },
1197
+ })
1198
+
1199
+ const index = SearchIndex.fromSearchDocuments(docs, config)
1200
+
1201
+ expect(index.search('alpha', Search.SearchConfig.getQueryOptions(config))).toEqual([])
1202
+ })
1203
+
1009
1204
  it('boosts shallow paths over deep paths', () => {
1010
1205
  const shallowDoc: SearchDocuments.Document = {
1011
1206
  category: '',
@@ -14,11 +14,12 @@ import { unified } from 'unified'
14
14
  import * as UnistUtil from 'unist-util-visit'
15
15
  import * as yaml from 'yaml'
16
16
  import type * as Config from './config.js'
17
+ import * as MarkdownImports from './markdown-imports.js'
17
18
  import { extractSubheading, getPhrasingContentText } from './mdx.js'
18
19
  import * as OpenApiRegistry from './openapi/registry.js'
19
20
  import * as OpenApiSearch from './openapi/search.js'
20
21
  import * as Path from './path.js'
21
- import { searchFields, storeFields, tokenize } from './search.client.js'
22
+ import { SearchConfig } from './search.client.js'
22
23
  import * as Sidebar from './sidebar.js'
23
24
  import * as TaskRunner from './task-runner.js'
24
25
  import * as TopNav from './topNav.js'
@@ -61,7 +62,8 @@ export namespace SearchDocuments {
61
62
  for (const page of mdxPages)
62
63
  taskRunner.run(async () => {
63
64
  const filePath = path.join(pagesDirPath, page)
64
- const content = await fs.promises.readFile(filePath, 'utf-8')
65
+ const rawContent = await fs.promises.readFile(filePath, 'utf-8')
66
+ const content = MarkdownImports.inlineMarkdownImports(rawContent, filePath)
65
67
 
66
68
  const { searchPriority, sections } = extract(content, config)
67
69
  if (sections.length === 0) return
@@ -206,12 +208,11 @@ export namespace SearchIndex {
206
208
  /**
207
209
  * Create a search index from documents.
208
210
  */
209
- export function fromSearchDocuments(documents: SearchDocuments.Document[]): SearchIndex {
210
- const index = new MiniSearch<SearchDocuments.Document>({
211
- fields: [...searchFields],
212
- storeFields: [...storeFields],
213
- tokenize,
214
- })
211
+ export function fromSearchDocuments(
212
+ documents: SearchDocuments.Document[],
213
+ config?: Config.Config,
214
+ ): SearchIndex {
215
+ const index = new MiniSearch<SearchDocuments.Document>(SearchConfig.getIndexOptions(config))
215
216
  index.addAll(documents)
216
217
  return index
217
218
  }
@@ -220,17 +221,15 @@ export namespace SearchIndex {
220
221
  * Load a search index from a JSON file.
221
222
  */
222
223
  export function loadFromFile(options: loadFromFile.Options): SearchIndex {
223
- const { filePath } = options
224
+ const { config, filePath } = options
224
225
 
225
226
  const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
226
- return MiniSearch.loadJSON(json, {
227
- fields: [...searchFields],
228
- storeFields: [...storeFields],
229
- })
227
+ return MiniSearch.loadJSON(json, SearchConfig.getIndexOptions(config))
230
228
  }
231
229
 
232
230
  export declare namespace loadFromFile {
233
231
  type Options = {
232
+ config?: Config.Config
234
233
  filePath: string
235
234
  }
236
235
  }
@@ -260,7 +259,8 @@ export namespace SearchIndex {
260
259
  ): string[] {
261
260
  const { pagesDir, previousIds = [], config } = options
262
261
 
263
- const content = fs.readFileSync(filePath, 'utf-8')
262
+ const rawContent = fs.readFileSync(filePath, 'utf-8')
263
+ const content = MarkdownImports.inlineMarkdownImports(rawContent, filePath)
264
264
  const { searchPriority, sections } = extract(content, config)
265
265
 
266
266
  // Remove old entries for this file
@@ -636,7 +636,7 @@ export function search(config: Config.Config): PluginOption {
636
636
  async function buildIndex(): Promise<SearchIndex.SearchIndex> {
637
637
  logger.info('Building search index...', { timestamp: true })
638
638
  const docs = await SearchDocuments.fromConfig(config)
639
- const index = SearchIndex.fromSearchDocuments(docs)
639
+ const index = SearchIndex.fromSearchDocuments(docs, config)
640
640
 
641
641
  // Populate fileIds map for HMR
642
642
  for (const doc of docs) {
@@ -1,7 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
4
- import { useRouter, Link as WakuLink } from 'waku'
3
+ import { useContext, useEffect, useState } from 'react'
4
+ import { Link as WakuLink } from 'waku'
5
+ import { unstable_RouterContext as WakuRouterContext } from 'waku/router/client'
5
6
  import * as Path from '../internal/path.js'
6
7
 
7
8
  const viewportPrefetchDelayMs = 2_000
@@ -57,14 +58,18 @@ function useViewportPrefetchReady(enabled: boolean) {
57
58
 
58
59
  export function Link(props: Link.Props) {
59
60
  const { to, unstable_prefetchOnEnter = true, unstable_prefetchOnView = true, ...rest } = props
60
- const { path } = useRouter()
61
- const prefetchOnView = useViewportPrefetchReady(Boolean(unstable_prefetchOnView))
61
+ const router = useContext(WakuRouterContext)
62
+ const routerPath = router?.route.path
63
+ const isExternal = Path.isExternal(props.to)
64
+ const prefetchOnView = useViewportPrefetchReady(
65
+ Boolean(unstable_prefetchOnView) && !isExternal && routerPath !== undefined,
66
+ )
62
67
 
63
- if (Path.isExternal(props.to))
64
- return <a {...rest} href={props.to} rel="noopener noreferrer" target="_blank" />
68
+ if (isExternal) return <a {...rest} href={props.to} rel="noopener noreferrer" target="_blank" />
65
69
 
66
70
  const [before, after] = (props.to || '').split('#')
67
- const resolvedTo = `${before ? before : path}${after ? `#${after}` : ''}`
71
+ const resolvedTo = `${before ? before : (routerPath ?? '')}${after ? `#${after}` : ''}`
72
+ if (routerPath === undefined) return <a {...rest} href={resolvedTo || props.to} />
68
73
  return (
69
74
  <WakuLink
70
75
  {...rest}
@@ -11,7 +11,7 @@ import LucideFile from '~icons/lucide/file'
11
11
  import LucideHash from '~icons/lucide/hash'
12
12
  import LucideSearch from '~icons/lucide/search'
13
13
  import * as Path from '../../internal/path.js'
14
- import { searchFields, storeFields, tokenize } from '../../internal/search.client.js'
14
+ import { SearchConfig } from '../../internal/search.client.js'
15
15
  import { Link } from '../Link.js'
16
16
  import { useConfig } from '../useConfig.js'
17
17
  import { DialogTrigger } from './DialogTrigger.js'
@@ -86,14 +86,12 @@ export function Search(props: Search.Props) {
86
86
  const json = await getSearchIndex()
87
87
  setIndex(
88
88
  MiniSearch.loadJSON<SearchResult>(json, {
89
- fields: [...searchFields],
90
- storeFields: [...storeFields],
91
- tokenize,
89
+ ...SearchConfig.getIndexOptions(config),
92
90
  }),
93
91
  )
94
92
  })
95
93
  .catch((error) => console.error('Failed to load search index:', error))
96
- }, [open, index])
94
+ }, [open, index, config])
97
95
 
98
96
  React.useEffect(() => {
99
97
  try {
@@ -108,12 +106,11 @@ export function Search(props: Search.Props) {
108
106
  return
109
107
  }
110
108
 
111
- const results = (index.search(query, { ...config.search, tokenize }) as SearchResult[]).slice(
112
- 0,
113
- 20,
114
- )
109
+ const results = (
110
+ index.search(query, SearchConfig.getQueryOptions(config)) as SearchResult[]
111
+ ).slice(0, 20)
115
112
  setSearch((s) => ({ ...s, results, selectedIndex: 0 }))
116
- }, [query, index, config.search])
113
+ }, [query, index, config])
117
114
 
118
115
  React.useEffect(() => {
119
116
  if (disableKeyboardShortcut) return