@docsector/docsector-reader 1.7.1 → 2.0.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.
@@ -61,33 +61,370 @@ function getPackageRoot (projectRoot) {
61
61
  }
62
62
 
63
63
  /**
64
- * Create a Vite plugin that watches consumer content files (pages/index.js,
65
- * i18n languages, etc.) and forces the Vite dep optimizer to re-run when
66
- * they change.
64
+ * Normalize paths for cross-platform file matching.
65
+ */
66
+ function normalizePathForMatch (path) {
67
+ return String(path || '').replace(/\\/g, '/')
68
+ }
69
+
70
+ /**
71
+ * List top-level page registry definition files.
72
+ *
73
+ * Includes:
74
+ * - src/pages/index.js (legacy)
75
+ * - src/pages/*.book.js
76
+ * - src/pages/*.index.js
77
+ */
78
+ function getPagesRegistryFiles (projectRoot) {
79
+ const pagesDir = resolve(projectRoot, 'src', 'pages')
80
+ if (!existsSync(pagesDir)) return []
81
+
82
+ const names = readdirSync(pagesDir, { withFileTypes: true })
83
+ .filter(entry => entry.isFile())
84
+ .map(entry => entry.name)
85
+
86
+ return names
87
+ .filter(name => {
88
+ if (name === 'index.js') return true
89
+ return /^[^/]+\.book\.js$/.test(name) || /^[^/]+\.index\.js$/.test(name)
90
+ })
91
+ .sort()
92
+ .map(name => resolve(pagesDir, name))
93
+ }
94
+
95
+ /**
96
+ * Discover configured books from src/pages/*.book.js paired with *.index.js.
97
+ */
98
+ function getBookRegistryEntries (projectRoot) {
99
+ const pagesDir = resolve(projectRoot, 'src', 'pages')
100
+ if (!existsSync(pagesDir)) return []
101
+
102
+ const names = readdirSync(pagesDir, { withFileTypes: true })
103
+ .filter(entry => entry.isFile())
104
+ .map(entry => entry.name)
105
+
106
+ const books = names
107
+ .filter(name => /^[^/]+\.book\.js$/.test(name))
108
+ .sort()
109
+
110
+ const entries = []
111
+ for (const bookFile of books) {
112
+ const baseName = bookFile.slice(0, -'.book.js'.length)
113
+ const indexFile = `${baseName}.index.js`
114
+ if (!names.includes(indexFile)) continue
115
+
116
+ entries.push({
117
+ id: baseName,
118
+ bookFile,
119
+ indexFile,
120
+ bookPath: resolve(pagesDir, bookFile),
121
+ indexPath: resolve(pagesDir, indexFile)
122
+ })
123
+ }
124
+
125
+ return entries
126
+ }
127
+
128
+ /**
129
+ * Resolve book identifier from page config with legacy fallback support.
130
+ */
131
+ function resolvePageBook (config, fallbackBook = 'manual') {
132
+ if (!config || typeof config !== 'object') return fallbackBook
133
+ return config.book ?? config.type ?? fallbackBook
134
+ }
135
+
136
+ const DEFAULT_BOOK_COLORS = Object.freeze({
137
+ active: 'white',
138
+ inactive: 'rgba(255, 255, 255, 0.72)'
139
+ })
140
+
141
+ function normalizeBookColorConfig (rawColor) {
142
+ if (typeof rawColor === 'object' && rawColor !== null && !Array.isArray(rawColor)) {
143
+ const active = typeof rawColor.active === 'string' && rawColor.active.trim().length > 0
144
+ ? rawColor.active.trim()
145
+ : DEFAULT_BOOK_COLORS.active
146
+
147
+ const inactive = typeof rawColor.inactive === 'string' && rawColor.inactive.trim().length > 0
148
+ ? rawColor.inactive.trim()
149
+ : active
150
+
151
+ return { active, inactive }
152
+ }
153
+
154
+ if (typeof rawColor === 'string' && rawColor.trim().length > 0) {
155
+ const normalized = rawColor.trim()
156
+ return {
157
+ active: normalized,
158
+ inactive: normalized
159
+ }
160
+ }
161
+
162
+ return { ...DEFAULT_BOOK_COLORS }
163
+ }
164
+
165
+ /**
166
+ * Build source code for the virtual module `virtual:docsector-books`.
167
+ */
168
+ function buildVirtualBooksModule (projectRoot) {
169
+ const bookEntries = getBookRegistryEntries(projectRoot)
170
+
171
+ // Legacy fallback: support projects that still define src/pages/index.js only.
172
+ if (bookEntries.length === 0) {
173
+ return `import legacyPages from 'pages'
174
+
175
+ const defaultBook = {
176
+ id: 'manual',
177
+ label: 'Manual',
178
+ icon: 'menu_book',
179
+ order: 1,
180
+ color: {
181
+ active: 'white',
182
+ inactive: 'rgba(255, 255, 255, 0.72)'
183
+ }
184
+ }
185
+
186
+ const normalizedPages = legacyPages || {}
187
+
188
+ export const books = {
189
+ manual: {
190
+ config: defaultBook,
191
+ routes: normalizedPages
192
+ }
193
+ }
194
+
195
+ export const allBooks = [defaultBook]
196
+ export const allPages = normalizedPages
197
+
198
+ export default books
199
+ `
200
+ }
201
+
202
+ const imports = []
203
+ const rows = []
204
+
205
+ for (const [index, entry] of bookEntries.entries()) {
206
+ imports.push(`import __book_${index} from 'pages/${entry.bookFile}'`)
207
+ imports.push(`import __routes_${index} from 'pages/${entry.indexFile}'`)
208
+ rows.push(` { fallbackId: ${JSON.stringify(entry.id)}, config: __book_${index}, routes: __routes_${index} }`)
209
+ }
210
+
211
+ return `${imports.join('\n')}
212
+
213
+ const entries = [
214
+ ${rows.join(',\n')}
215
+ ]
216
+
217
+ const DEFAULT_BOOK_COLORS = Object.freeze({
218
+ active: 'white',
219
+ inactive: 'rgba(255, 255, 255, 0.72)'
220
+ })
221
+
222
+ const normalizeBookColor = (rawColor) => {
223
+ if (typeof rawColor === 'object' && rawColor !== null && !Array.isArray(rawColor)) {
224
+ const active = typeof rawColor.active === 'string' && rawColor.active.trim().length > 0
225
+ ? rawColor.active.trim()
226
+ : DEFAULT_BOOK_COLORS.active
227
+
228
+ const inactive = typeof rawColor.inactive === 'string' && rawColor.inactive.trim().length > 0
229
+ ? rawColor.inactive.trim()
230
+ : active
231
+
232
+ return { active, inactive }
233
+ }
234
+
235
+ if (typeof rawColor === 'string' && rawColor.trim().length > 0) {
236
+ const normalized = rawColor.trim()
237
+ return {
238
+ active: normalized,
239
+ inactive: normalized
240
+ }
241
+ }
242
+
243
+ return { ...DEFAULT_BOOK_COLORS }
244
+ }
245
+
246
+ export const books = entries.reduce((accumulator, entry, index) => {
247
+ const config = entry.config || {}
248
+ const resolvedId = config.id || entry.fallbackId || ('book-' + (index + 1))
249
+ const label = config.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
250
+ const normalizedConfig = {
251
+ ...config,
252
+ id: resolvedId,
253
+ label,
254
+ icon: config.icon || 'menu_book',
255
+ order: config.order ?? (index + 1),
256
+ color: normalizeBookColor(config.color)
257
+ }
258
+
259
+ accumulator[resolvedId] = {
260
+ config: normalizedConfig,
261
+ routes: entry.routes || {}
262
+ }
263
+ return accumulator
264
+ }, {})
265
+
266
+ export const allBooks = Object.values(books).map(book => book.config)
267
+ export const allPages = Object.values(books).reduce((accumulator, book) => {
268
+ return {
269
+ ...accumulator,
270
+ ...(book.routes || {})
271
+ }
272
+ }, {})
273
+
274
+ export default books
275
+ `
276
+ }
277
+
278
+ /**
279
+ * Load books and merged pages for build-time plugins (Node context).
280
+ */
281
+ async function loadBooksRegistry (projectRoot) {
282
+ const entries = getBookRegistryEntries(projectRoot)
283
+
284
+ // Legacy fallback
285
+ if (entries.length === 0) {
286
+ const legacyPath = resolve(projectRoot, 'src', 'pages', 'index.js')
287
+ const pages = existsSync(legacyPath)
288
+ ? ((await import(pathToFileURL(legacyPath).href)).default || {})
289
+ : {}
290
+
291
+ const defaultBook = {
292
+ id: 'manual',
293
+ label: 'Manual',
294
+ icon: 'menu_book',
295
+ order: 1,
296
+ color: {
297
+ active: 'white',
298
+ inactive: 'rgba(255, 255, 255, 0.72)'
299
+ }
300
+ }
301
+
302
+ return {
303
+ books: {
304
+ manual: {
305
+ config: defaultBook,
306
+ routes: pages
307
+ }
308
+ },
309
+ allBooks: [defaultBook],
310
+ allPages: pages
311
+ }
312
+ }
313
+
314
+ const books = {}
315
+ const allPages = {}
316
+
317
+ for (const [index, entry] of entries.entries()) {
318
+ const { default: rawConfig = {} } = await import(pathToFileURL(entry.bookPath).href)
319
+ const { default: routes = {} } = await import(pathToFileURL(entry.indexPath).href)
320
+
321
+ const resolvedId = rawConfig.id || entry.id || `book-${index + 1}`
322
+ const label = rawConfig.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
323
+
324
+ const config = {
325
+ ...rawConfig,
326
+ id: resolvedId,
327
+ label,
328
+ icon: rawConfig.icon || 'menu_book',
329
+ order: rawConfig.order ?? (index + 1),
330
+ color: normalizeBookColorConfig(rawConfig.color)
331
+ }
332
+
333
+ books[resolvedId] = {
334
+ config,
335
+ routes
336
+ }
337
+
338
+ Object.assign(allPages, routes || {})
339
+ }
340
+
341
+ const allBooks = Object.values(books).map(book => book.config)
342
+
343
+ return {
344
+ books,
345
+ allBooks,
346
+ allPages
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Return page entries while preserving the book that contributed each route.
352
+ *
353
+ * Why: `allPages` is a legacy flattened registry and cannot represent two
354
+ * books that reuse the same route key, such as `/getting-started` in both
355
+ * `guide.index.js` and `manual.index.js`. Build artifacts that need concrete
356
+ * URLs must iterate per book instead.
357
+ */
358
+ function getBookPageEntries (books = {}) {
359
+ const pageEntries = []
360
+
361
+ for (const [bookId, book] of Object.entries(books || {})) {
362
+ const fallbackBook = book?.config?.id || bookId || 'manual'
363
+
364
+ for (const [pagePath, page] of Object.entries(book?.routes || {})) {
365
+ pageEntries.push({
366
+ book: resolvePageBook(page?.config, fallbackBook),
367
+ pagePath,
368
+ page
369
+ })
370
+ }
371
+ }
372
+
373
+ return pageEntries
374
+ }
375
+
376
+ /**
377
+ * Check if a file path is a book registry definition file.
378
+ */
379
+ function isPagesRegistryFile (projectRoot, changedPath) {
380
+ const pagesDir = normalizePathForMatch(resolve(projectRoot, 'src', 'pages'))
381
+ const normalizedPath = normalizePathForMatch(changedPath)
382
+ const prefix = `${pagesDir}/`
383
+
384
+ if (!normalizedPath.startsWith(prefix)) return false
385
+ const relativePath = normalizedPath.slice(prefix.length)
386
+
387
+ if (relativePath === 'index.js') return true
388
+ return /^[^/]+\.book\.js$/.test(relativePath) || /^[^/]+\.index\.js$/.test(relativePath)
389
+ }
390
+
391
+ /**
392
+ * Create a Vite plugin that exposes discovered books through
393
+ * `virtual:docsector-books` and restarts dev server when definitions change.
67
394
  *
68
395
  * Why: The router module (`routes.js`) imports consumer content via the
69
396
  * `pages` alias. Vite's dep optimizer pre-bundles the router with the
70
397
  * consumer content inlined, but the optimizer cache hash is based on config
71
- * and lockfile only — NOT on consumer source files. So when pages/index.js
72
- * changes during development, the optimizer serves stale pre-bundled code
73
- * until the cache is manually cleared.
74
- *
75
- * Fix: When pages/index.js changes, the watcher plugin clears the dep cache,
76
- * sets an env flag, and restarts the server. On restart, the config reads the
77
- * flag and sets `optimizeDeps.force = true`, which makes Vite generate a new
78
- * browserHash — effectively busting the browser module cache.
398
+ * and lockfile only — NOT on consumer source files. So when page registries
399
+ * change during development, the optimizer can serve stale pre-bundled code.
79
400
  */
80
- function createPagesWatchPlugin (projectRoot) {
81
- const pagesIndex = resolve(projectRoot, 'src', 'pages', 'index.js')
401
+ function createBooksPlugin (projectRoot) {
402
+ const virtualId = 'virtual:docsector-books'
403
+ const resolvedId = '\0' + virtualId
404
+
82
405
  return {
83
- name: 'docsector-pages-watch',
406
+ name: 'docsector-books',
407
+ resolveId (id) {
408
+ if (id === virtualId) return resolvedId
409
+ },
410
+ load (id) {
411
+ if (id !== resolvedId) return null
412
+ return buildVirtualBooksModule(projectRoot)
413
+ },
84
414
  configureServer (server) {
85
- server.watcher.on('change', (changedPath) => {
86
- if (changedPath === pagesIndex) {
415
+ const onPagesRegistryChange = (changedPath) => {
416
+ if (isPagesRegistryFile(projectRoot, changedPath)) {
87
417
  server.config.logger.info(
88
- '\\x1b[36m[docsector]\\x1b[0m pages/index.js changed — clearing dep cache and restarting...',
418
+ `\\x1b[36m[docsector]\\x1b[0m pages registry changed (${changedPath}) — clearing dep cache and restarting...`,
89
419
  { timestamp: true }
90
420
  )
421
+
422
+ // Invalidate virtual module before restart
423
+ const module = server.moduleGraph.getModuleById(resolvedId)
424
+ if (module) {
425
+ server.moduleGraph.invalidateModule(module)
426
+ }
427
+
91
428
  // Signal the restarted config to force a new optimizer hash
92
429
  process.env.__DOCSECTOR_FORCE_OPTIMIZE = '1'
93
430
  // Delete the stale optimizer cache
@@ -95,7 +432,11 @@ function createPagesWatchPlugin (projectRoot) {
95
432
  rmSync(cacheDir, { recursive: true, force: true })
96
433
  server.restart()
97
434
  }
98
- })
435
+ }
436
+
437
+ server.watcher.on('add', onPagesRegistryChange)
438
+ server.watcher.on('change', onPagesRegistryChange)
439
+ server.watcher.on('unlink', onPagesRegistryChange)
99
440
  }
100
441
  }
101
442
  }
@@ -145,11 +486,11 @@ function createPrerenderMetaPlugin (projectRoot) {
145
486
 
146
487
  const baseHtml = readFileSync(baseHtmlPath, 'utf-8')
147
488
 
148
- // Dynamic import pages registry and docsector config
149
- const pagesUrl = pathToFileURL(resolve(projectRoot, 'src', 'pages', 'index.js')).href
489
+ // Dynamic import books registry and docsector config
150
490
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
151
491
 
152
- const { default: pages } = await import(pagesUrl)
492
+ const { books } = await loadBooksRegistry(projectRoot)
493
+ const pageEntries = getBookPageEntries(books)
153
494
  const { default: config } = await import(configUrl)
154
495
 
155
496
  const brandingName = config.branding?.name || ''
@@ -167,10 +508,9 @@ function createPrerenderMetaPlugin (projectRoot) {
167
508
 
168
509
  let count = 0
169
510
 
170
- for (const [pagePath, page] of Object.entries(pages)) {
511
+ for (const { book, pagePath, page } of pageEntries) {
171
512
  if (page.config === null) continue
172
513
 
173
- const type = page.config.type ?? 'manual'
174
514
  const titleData = page.data?.[defaultLang] || page.data?.['*'] || page.data?.['en-US'] || Object.values(page.data || {})[0]
175
515
  const title = titleData?.title || ''
176
516
  const fullTitle = title
@@ -186,7 +526,7 @@ function createPrerenderMetaPlugin (projectRoot) {
186
526
  if (page.config.subpages?.vs) subpages.push('vs')
187
527
 
188
528
  for (const subpage of subpages) {
189
- const routePath = `${type}${pagePath}/${subpage}`
529
+ const routePath = `${book}${pagePath}/${subpage}`
190
530
 
191
531
  const html = baseHtml
192
532
  .replace(/<title>[^<]*<\/title>/, () => `<title>${fullTitle}</title>`)
@@ -759,29 +1099,27 @@ function createMarkdownBuildPlugin (projectRoot) {
759
1099
 
760
1100
  const pagesDir = resolve(projectRoot, 'src', 'pages')
761
1101
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
762
- const pagesUrl = pathToFileURL(resolve(projectRoot, 'src', 'pages', 'index.js')).href
763
1102
 
764
1103
  const { default: config } = await import(configUrl)
765
- const { default: pages } = await import(pagesUrl)
1104
+ const { books } = await loadBooksRegistry(projectRoot)
1105
+ const pageEntries = getBookPageEntries(books)
766
1106
 
767
1107
  const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
768
1108
  let count = 0
769
1109
 
770
- for (const [pagePath, page] of Object.entries(pages)) {
1110
+ for (const { book, pagePath, page } of pageEntries) {
771
1111
  if (page.config === null) continue
772
1112
  if (page.config.status === 'empty') continue
773
1113
 
774
- const type = page.config.type ?? 'manual'
775
-
776
1114
  const subpages = ['overview']
777
1115
  if (page.config.subpages?.showcase) subpages.push('showcase')
778
1116
  if (page.config.subpages?.vs) subpages.push('vs')
779
1117
 
780
1118
  for (const subpage of subpages) {
781
- const srcFile = resolve(pagesDir, `${type}${pagePath}.${subpage}.${defaultLang}.md`)
1119
+ const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
782
1120
  if (!existsSync(srcFile)) continue
783
1121
 
784
- const routePath = `${type}${pagePath}/${subpage}`
1122
+ const routePath = `${book}${pagePath}/${subpage}`
785
1123
  const destFile = resolve(distDir, `${routePath}.md`)
786
1124
  const destDir = resolve(destFile, '..')
787
1125
 
@@ -816,21 +1154,19 @@ function createMarkdownBuildPlugin (projectRoot) {
816
1154
  const today = new Date().toISOString().split('T')[0]
817
1155
  let urls = ''
818
1156
 
819
- for (const [pagePath, page] of Object.entries(pages)) {
1157
+ for (const { book, pagePath, page } of pageEntries) {
820
1158
  if (page.config === null) continue
821
1159
  if (page.config.status === 'empty') continue
822
1160
 
823
- const type = page.config.type ?? 'manual'
824
-
825
1161
  const subpages = ['overview']
826
1162
  if (page.config.subpages?.showcase) subpages.push('showcase')
827
1163
  if (page.config.subpages?.vs) subpages.push('vs')
828
1164
 
829
1165
  for (const subpage of subpages) {
830
- const srcFile = resolve(pagesDir, `${type}${pagePath}.${subpage}.${defaultLang}.md`)
1166
+ const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
831
1167
  if (!existsSync(srcFile)) continue
832
1168
 
833
- const routePath = `/${type}${pagePath}/${subpage}`
1169
+ const routePath = `/${book}${pagePath}/${subpage}`
834
1170
  urls += ` <url>\n <loc>${siteUrl}${routePath}</loc>\n <lastmod>${today}</lastmod>\n </url>\n`
835
1171
  }
836
1172
  }
@@ -849,11 +1185,10 @@ function createMarkdownBuildPlugin (projectRoot) {
849
1185
 
850
1186
  const llmsSections = {}
851
1187
 
852
- for (const [pagePath, page] of Object.entries(pages)) {
1188
+ for (const { book, pagePath, page } of pageEntries) {
853
1189
  if (page.config === null) continue
854
1190
  if (page.config.status === 'empty') continue
855
1191
 
856
- const type = page.config.type ?? 'manual'
857
1192
  const title = page.data?.['*']?.title
858
1193
  || page.data?.[defaultLang]?.title
859
1194
  || page.data?.['en-US']?.title
@@ -865,18 +1200,18 @@ function createMarkdownBuildPlugin (projectRoot) {
865
1200
  if (page.config.subpages?.vs) subpages.push('vs')
866
1201
 
867
1202
  for (const subpage of subpages) {
868
- const srcFile = resolve(pagesDir, `${type}${pagePath}.${subpage}.${defaultLang}.md`)
1203
+ const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
869
1204
  if (!existsSync(srcFile)) continue
870
1205
 
871
- const routePath = `${type}${pagePath}/${subpage}`
1206
+ const routePath = `${book}${pagePath}/${subpage}`
872
1207
  const mdUrl = `${siteUrl}/${routePath}.md`
873
1208
  const pageUrl = `${siteUrl}/${routePath}`
874
1209
 
875
1210
  const desc = page.config.meta?.description
876
1211
  const descText = typeof desc === 'object' ? (desc[defaultLang] || desc['en-US'] || '') : (desc || '')
877
1212
 
878
- if (!llmsSections[type]) llmsSections[type] = []
879
- llmsSections[type].push(
1213
+ if (!llmsSections[book]) llmsSections[book] = []
1214
+ llmsSections[book].push(
880
1215
  descText
881
1216
  ? `- [${title}](${mdUrl}): ${descText}`
882
1217
  : `- [${title}](${mdUrl})`
@@ -1470,11 +1805,10 @@ export async function onRequest (context) {
1470
1805
 
1471
1806
  // Collect page index for MCP
1472
1807
  const mcpPages = []
1473
- for (const [pagePath, page] of Object.entries(pages)) {
1808
+ for (const { book, pagePath, page } of pageEntries) {
1474
1809
  if (page.config === null) continue
1475
1810
  if (page.config.status === 'empty') continue
1476
1811
 
1477
- const type = page.config.type ?? 'manual'
1478
1812
  const defaultTitle = page.data?.['*']?.title
1479
1813
  || page.data?.[defaultLang]?.title
1480
1814
  || page.data?.['en-US']?.title
@@ -1486,13 +1820,14 @@ export async function onRequest (context) {
1486
1820
  if (page.config.subpages?.vs) subpageList.push('vs')
1487
1821
 
1488
1822
  for (const subpage of subpageList) {
1489
- const srcFile = resolve(pagesDir, `${type}${pagePath}.${subpage}.${defaultLang}.md`)
1823
+ const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
1490
1824
  if (!existsSync(srcFile)) continue
1491
1825
 
1492
1826
  mcpPages.push({
1493
- path: `${type}${pagePath}/${subpage}`,
1827
+ path: `${book}${pagePath}/${subpage}`,
1494
1828
  title: defaultTitle,
1495
- type,
1829
+ book,
1830
+ type: book,
1496
1831
  subpage
1497
1832
  })
1498
1833
  }
@@ -1874,12 +2209,12 @@ export function createQuasarConfig (options = {}) {
1874
2209
  vueRouterMode: 'history',
1875
2210
 
1876
2211
  vitePlugins: [
2212
+ createBooksPlugin(projectRoot),
1877
2213
  createHjsonPlugin(),
1878
2214
  createHomePageOverridePlugin(projectRoot),
1879
2215
  createGitDatesPlugin(projectRoot),
1880
2216
  createMarkdownEndpointPlugin(projectRoot),
1881
2217
  createMarkdownBuildPlugin(projectRoot),
1882
- createPagesWatchPlugin(projectRoot),
1883
2218
  createPrerenderMetaPlugin(projectRoot),
1884
2219
  ...vitePlugins
1885
2220
  ],
@@ -1897,16 +2232,23 @@ export function createQuasarConfig (options = {}) {
1897
2232
  viteConf.optimizeDeps.force = true
1898
2233
  }
1899
2234
 
1900
- // Include a hash of pages/index.js in the optimizer config so that
1901
- // Vite's configHash (and thus browserHash) changes whenever page
1902
- // definitions change. This prevents the browser from serving stale
1903
- // pre-bundled router modules from its module cache.
1904
- const pagesFile = resolve(projectRoot, 'src', 'pages', 'index.js')
1905
- if (existsSync(pagesFile)) {
2235
+ // Include a hash of page registry definition files (legacy index.js,
2236
+ // plus *.book.js and *.index.js) in optimizer config so Vite's
2237
+ // configHash/browserHash changes whenever routes/books are edited.
2238
+ const pagesRegistryFiles = getPagesRegistryFiles(projectRoot)
2239
+ if (pagesRegistryFiles.length > 0) {
2240
+ const pagesHashBuilder = createHash('sha256')
2241
+ for (const file of pagesRegistryFiles) {
2242
+ pagesHashBuilder
2243
+ .update(file)
2244
+ .update(readFileSync(file))
2245
+ }
2246
+
1906
2247
  const pagesHash = createHash('sha256')
1907
- .update(readFileSync(pagesFile))
2248
+ .update(pagesHashBuilder.digest('hex'))
1908
2249
  .digest('hex')
1909
2250
  .slice(0, 8)
2251
+
1910
2252
  viteConf.optimizeDeps.esbuildOptions = viteConf.optimizeDeps.esbuildOptions || {}
1911
2253
  viteConf.optimizeDeps.esbuildOptions.define = {
1912
2254
  ...(viteConf.optimizeDeps.esbuildOptions.define || {}),
@@ -1950,7 +2292,7 @@ export function createQuasarConfig (options = {}) {
1950
2292
  // The router is excluded because routes.js imports consumer content via
1951
2293
  // the `pages` alias. If pre-bundled, consumer content gets embedded in
1952
2294
  // the optimizer cache whose hash doesn't track source file changes,
1953
- // causing stale routes after editing pages/index.js.
2295
+ // causing stale routes after editing page registry files.
1954
2296
  viteConf.optimizeDeps.exclude = [
1955
2297
  ...(viteConf.optimizeDeps.exclude || []),
1956
2298
  'boot/i18n', 'boot/store', 'boot/QZoom', 'boot/axios'