polen 0.10.0-next.4 → 0.10.0-next.6

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 (147) hide show
  1. package/README.md +2 -1
  2. package/build/api/config/load.js +5 -5
  3. package/build/api/config/load.js.map +1 -1
  4. package/build/api/config-resolver/resolve.js +2 -2
  5. package/build/api/config-resolver/resolve.js.map +1 -1
  6. package/build/api/content/$$.d.ts +5 -0
  7. package/build/api/content/$$.d.ts.map +1 -0
  8. package/build/api/content/$$.js +5 -0
  9. package/build/api/content/$$.js.map +1 -0
  10. package/build/api/content/$.d.ts +2 -0
  11. package/build/api/content/$.d.ts.map +1 -0
  12. package/build/api/content/$.js +2 -0
  13. package/build/api/content/$.js.map +1 -0
  14. package/build/api/content/metadata.d.ts +10 -0
  15. package/build/api/content/metadata.d.ts.map +1 -0
  16. package/build/api/content/metadata.js +9 -0
  17. package/build/api/content/metadata.js.map +1 -0
  18. package/build/api/content/page.d.ts +11 -0
  19. package/build/api/content/page.d.ts.map +1 -0
  20. package/build/api/content/page.js +2 -0
  21. package/build/api/content/page.js.map +1 -0
  22. package/build/api/content/scan.d.ts +19 -0
  23. package/build/api/content/scan.d.ts.map +1 -0
  24. package/build/api/content/scan.js +57 -0
  25. package/build/api/content/scan.js.map +1 -0
  26. package/build/{lib/file-router/sidebar/types.d.ts → api/content/sidebar.d.ts} +8 -1
  27. package/build/api/content/sidebar.d.ts.map +1 -0
  28. package/build/api/content/sidebar.js +90 -0
  29. package/build/api/content/sidebar.js.map +1 -0
  30. package/build/api/schema/data-sources/schema-directory/schema-directory.js +1 -1
  31. package/build/api/schema/data-sources/schema-directory/schema-directory.js.map +1 -1
  32. package/build/api/vite/plugins/branding/index.js +4 -4
  33. package/build/api/vite/plugins/branding/index.js.map +1 -1
  34. package/build/api/vite/plugins/core.js +4 -4
  35. package/build/api/vite/plugins/core.js.map +1 -1
  36. package/build/api/vite/plugins/pages.d.ts +6 -8
  37. package/build/api/vite/plugins/pages.d.ts.map +1 -1
  38. package/build/api/vite/plugins/pages.js +99 -155
  39. package/build/api/vite/plugins/pages.js.map +1 -1
  40. package/build/api/vite/plugins/serve.js +5 -5
  41. package/build/api/vite/plugins/serve.js.map +1 -1
  42. package/build/cli/_/self-contained-mode.js +5 -5
  43. package/build/cli/_/self-contained-mode.js.map +1 -1
  44. package/build/exports/components.d.ts +2 -0
  45. package/build/exports/components.d.ts.map +1 -0
  46. package/build/exports/components.js +2 -0
  47. package/build/exports/components.js.map +1 -0
  48. package/build/lib/demos/config-schema.d.ts +14 -14
  49. package/build/lib/file-router/file-router.d.ts +0 -2
  50. package/build/lib/file-router/file-router.d.ts.map +1 -1
  51. package/build/lib/file-router/file-router.js +0 -2
  52. package/build/lib/file-router/file-router.js.map +1 -1
  53. package/build/lib/file-router/route.d.ts +2 -0
  54. package/build/lib/file-router/route.d.ts.map +1 -1
  55. package/build/lib/file-router/route.js.map +1 -1
  56. package/build/lib/file-router/scan.d.ts.map +1 -1
  57. package/build/lib/file-router/scan.js +16 -12
  58. package/build/lib/file-router/scan.js.map +1 -1
  59. package/build/singletons/debug.d.ts +1 -1
  60. package/build/singletons/debug.d.ts.map +1 -1
  61. package/build/singletons/debug.js +1 -1
  62. package/build/singletons/debug.js.map +1 -1
  63. package/build/template/components/ThemeToggle.d.ts +3 -0
  64. package/build/template/components/ThemeToggle.d.ts.map +1 -0
  65. package/build/template/components/ThemeToggle.jsx +10 -0
  66. package/build/template/components/ThemeToggle.jsx.map +1 -0
  67. package/build/template/components/content/$$.d.ts +2 -0
  68. package/build/template/components/content/$$.d.ts.map +1 -0
  69. package/build/template/components/content/$$.js +2 -0
  70. package/build/template/components/content/$$.js.map +1 -0
  71. package/build/template/components/sidebar/Sidebar.d.ts +2 -2
  72. package/build/template/components/sidebar/Sidebar.d.ts.map +1 -1
  73. package/build/template/components/sidebar/SidebarItem.d.ts +3 -3
  74. package/build/template/components/sidebar/SidebarItem.d.ts.map +1 -1
  75. package/build/template/components/sidebar/SidebarItem.jsx +1 -1
  76. package/build/template/components/sidebar/SidebarItem.jsx.map +1 -1
  77. package/build/template/contexts/ThemeContext.d.ts +12 -0
  78. package/build/template/contexts/ThemeContext.d.ts.map +1 -0
  79. package/build/template/contexts/ThemeContext.jsx +41 -0
  80. package/build/template/contexts/ThemeContext.jsx.map +1 -0
  81. package/build/template/routes/root.d.ts.map +1 -1
  82. package/build/template/routes/root.jsx +15 -9
  83. package/build/template/routes/root.jsx.map +1 -1
  84. package/package.json +10 -3
  85. package/src/api/config/load.ts +5 -5
  86. package/src/api/config-resolver/resolve.ts +2 -2
  87. package/src/api/content/$$.ts +4 -0
  88. package/src/api/content/$.test.ts +72 -0
  89. package/src/api/content/$.ts +1 -0
  90. package/src/api/content/metadata.ts +11 -0
  91. package/src/api/content/page.ts +12 -0
  92. package/src/api/content/scan.ts +82 -0
  93. package/src/api/content/sidebar.ts +136 -0
  94. package/src/api/schema/data-sources/schema-directory/schema-directory.ts +1 -1
  95. package/src/api/vite/plugins/branding/index.ts +4 -4
  96. package/src/api/vite/plugins/core.ts +4 -4
  97. package/src/api/vite/plugins/pages.ts +117 -171
  98. package/src/api/vite/plugins/serve.ts +5 -5
  99. package/src/cli/_/self-contained-mode.ts +5 -5
  100. package/src/exports/components.ts +1 -0
  101. package/src/lib/deployment/$$.ts +1 -1
  102. package/src/lib/deployment/$.test.ts +3 -3
  103. package/src/lib/deployment/$.ts +1 -1
  104. package/src/lib/file-router/file-router.ts +0 -2
  105. package/src/lib/file-router/linter.test.ts +2 -0
  106. package/src/lib/file-router/route.ts +2 -0
  107. package/src/lib/file-router/scan.ts +19 -13
  108. package/src/lib/task/$.test.ts +3 -3
  109. package/src/singletons/debug.ts +1 -1
  110. package/src/template/components/ThemeToggle.tsx +21 -0
  111. package/src/template/components/content/$$.ts +1 -0
  112. package/src/template/components/sidebar/Sidebar.tsx +2 -2
  113. package/src/template/components/sidebar/SidebarItem.tsx +8 -8
  114. package/src/template/contexts/ThemeContext.tsx +60 -0
  115. package/src/template/routes/root.tsx +15 -9
  116. package/build/lib/file-router/scan-tree.d.ts +0 -20
  117. package/build/lib/file-router/scan-tree.d.ts.map +0 -1
  118. package/build/lib/file-router/scan-tree.js +0 -158
  119. package/build/lib/file-router/scan-tree.js.map +0 -1
  120. package/build/lib/file-router/sidebar/index.d.ts +0 -3
  121. package/build/lib/file-router/sidebar/index.d.ts.map +0 -1
  122. package/build/lib/file-router/sidebar/index.js +0 -4
  123. package/build/lib/file-router/sidebar/index.js.map +0 -1
  124. package/build/lib/file-router/sidebar/sidebar-tree.d.ts +0 -9
  125. package/build/lib/file-router/sidebar/sidebar-tree.d.ts.map +0 -1
  126. package/build/lib/file-router/sidebar/sidebar-tree.js +0 -85
  127. package/build/lib/file-router/sidebar/sidebar-tree.js.map +0 -1
  128. package/build/lib/file-router/sidebar/types.d.ts.map +0 -1
  129. package/build/lib/file-router/sidebar/types.js +0 -2
  130. package/build/lib/file-router/sidebar/types.js.map +0 -1
  131. package/build/lib/tree/index.d.ts +0 -3
  132. package/build/lib/tree/index.d.ts.map +0 -1
  133. package/build/lib/tree/index.js +0 -2
  134. package/build/lib/tree/index.js.map +0 -1
  135. package/build/lib/tree/tree.d.ts +0 -62
  136. package/build/lib/tree/tree.d.ts.map +0 -1
  137. package/build/lib/tree/tree.js +0 -134
  138. package/build/lib/tree/tree.js.map +0 -1
  139. package/src/lib/file-router/scan-tree.test.ts +0 -189
  140. package/src/lib/file-router/scan-tree.ts +0 -205
  141. package/src/lib/file-router/sidebar/index.ts +0 -3
  142. package/src/lib/file-router/sidebar/sidebar-tree.test.ts +0 -123
  143. package/src/lib/file-router/sidebar/sidebar-tree.ts +0 -110
  144. package/src/lib/file-router/sidebar/types.ts +0 -19
  145. package/src/lib/tree/index.ts +0 -2
  146. package/src/lib/tree/tree.test.ts +0 -117
  147. package/src/lib/tree/tree.ts +0 -183
@@ -1,36 +1,37 @@
1
1
  import type { Config } from '#api/config/index'
2
+ import { Content } from '#api/content/$'
2
3
  import type { NavbarDataRegistry } from '#api/vite/data/navbar'
3
4
  import { polenVirtual } from '#api/vite/vi'
4
5
  import type { Vite } from '#dep/vite/index'
5
6
  import { reportDiagnostics } from '#lib/file-router/diagnostic-reporter'
6
7
  import { FileRouter } from '#lib/file-router/index'
7
- import { Tree } from '#lib/tree/index'
8
- import { debug } from '#singletons/debug'
8
+ import { debugPolen } from '#singletons/debug'
9
9
  import { superjson } from '#singletons/superjson'
10
10
  import mdx from '@mdx-js/rollup'
11
11
  import rehypeShiki from '@shikijs/rehype'
12
- import { Path, Str } from '@wollybeard/kit'
12
+ import { Tree } from '@wollybeard/kit'
13
+ import { Arr, Cache, Path, Str } from '@wollybeard/kit'
14
+ import remarkFrontmatter from 'remark-frontmatter'
13
15
  import remarkGfm from 'remark-gfm'
14
16
 
15
- const _debug = debug.sub(`vite-plugin-pages`)
17
+ const debug = debugPolen.sub(`vite-plugin-pages`)
16
18
 
17
19
  export const viProjectPages = polenVirtual([`project`, `pages.jsx`], { allowPluginProcessing: true })
18
20
  export const viProjectPagesData = polenVirtual([`project`, `data`, 'pages.jsonsuper'], { allowPluginProcessing: true })
19
21
 
20
- export interface PagesTreePluginOptions {
22
+ export interface Options {
21
23
  config: Config.Config
22
24
  navbarData?: NavbarDataRegistry
23
- onPagesChange?: (pages: FileRouter.ScanResult) => void
24
- onTreeChange?: (tree: FileRouter.RouteTreeNode) => void
25
+ onChange?: (scanResult: Content.ScanResult) => void
25
26
  }
26
27
 
27
28
  export interface ProjectDataPages {
28
29
  sidebarIndex: SidebarIndex
29
- pagesScanResult: FileRouter.ScanResult
30
+ pages: Content.Page[]
30
31
  }
31
32
 
32
33
  export interface SidebarIndex {
33
- [pathExpression: string]: FileRouter.Sidebar.Sidebar
34
+ [pathExpression: string]: Content.Sidebar
34
35
  }
35
36
 
36
37
  /**
@@ -39,58 +40,22 @@ export interface SidebarIndex {
39
40
  export const Pages = ({
40
41
  config,
41
42
  navbarData,
42
- onPagesChange,
43
- onTreeChange,
44
- }: PagesTreePluginOptions): Vite.Plugin[] => {
45
- let currentPagesData: FileRouter.ScanResult | null = null
46
- let currentTreeData: FileRouter.RouteTreeNode | null = null
47
-
48
- // State management
49
- let pagesCache: FileRouter.ScanResult | null = null
50
- let treeCache: FileRouter.RouteTreeNode | null = null
51
-
52
- // Helper functions
53
- const scanPages = async () => {
54
- if (!pagesCache) {
55
- _debug(`Scanning pages - cache is null, loading fresh data`)
56
- pagesCache = await FileRouter.scan({
57
- dir: config.paths.project.absolute.pages,
58
- glob: `**/*.{md,mdx}`,
59
- })
60
- _debug(`Found ${String(pagesCache.routes.length)} pages`)
61
- } else {
62
- _debug(`Using cached pages`)
63
- }
64
- return pagesCache
65
- }
66
-
67
- const scanTree = async () => {
68
- if (!treeCache) {
69
- _debug(`Scanning tree - cache is null, loading fresh data`)
70
- const result = await FileRouter.scanTree({
71
- dir: config.paths.project.absolute.pages,
72
- glob: `**/*.{md,mdx}`,
73
- })
74
- treeCache = result.routeTree
75
- _debug(`Built route tree`)
76
- } else {
77
- _debug(`Using cached tree`)
78
- }
79
- return treeCache
80
- }
81
-
82
- const clearCache = () => {
83
- _debug(`Clearing pages and tree cache`)
84
- pagesCache = null
85
- treeCache = null
86
- }
43
+ onChange,
44
+ }: Options): Vite.Plugin[] => {
45
+ const scanPages = Cache.memoize(debug.trace(async function scanPages() {
46
+ const result = await Content.scan({
47
+ dir: config.paths.project.absolute.pages,
48
+ glob: `**/*.{md,mdx}`,
49
+ })
50
+ return result
51
+ }))
87
52
 
88
53
  const isPageFile = (file: string) => {
89
54
  return (file.endsWith(`.md`) || file.endsWith(`.mdx`))
90
55
  && file.includes(config.paths.project.absolute.pages)
91
56
  }
92
57
 
93
- const generatePagesModule = (pagesScanResult: FileRouter.ScanResult): string => {
58
+ const generatePagesModule = (pages: Content.Page[]): string => {
94
59
  const $ = {
95
60
  pages: `pages`,
96
61
  }
@@ -99,17 +64,21 @@ export const Pages = ({
99
64
  s`export const ${$.pages} = []`
100
65
 
101
66
  // Generate imports and route objects
102
- for (const route of pagesScanResult.routes) {
67
+ for (const { route, metadata } of pages) {
103
68
  const filePathExp = Path.format(route.file.path.absolute)
104
69
  const pathExp = FileRouter.routeToPathExpression(route)
105
- const ident = Str.Case.camel(`page ` + Str.titlizeSlug(pathExp))
70
+ const $$ = {
71
+ ...$,
72
+ Component: Str.Case.camel(`page ` + Str.titlizeSlug(pathExp)),
73
+ }
106
74
 
107
75
  s`
108
- import ${ident} from '${filePathExp}'
76
+ import ${$$.Component} from '${filePathExp}'
109
77
 
110
- ${$.pages}.push({
78
+ ${$$.pages}.push({
111
79
  path: '${pathExp}',
112
- Component: ${ident}
80
+ Component: ${$$.Component},
81
+ metadata: ${JSON.stringify(metadata)}
113
82
  })
114
83
  `
115
84
  }
@@ -123,7 +92,11 @@ export const Pages = ({
123
92
  enforce: `pre` as const,
124
93
  ...mdx({
125
94
  jsxImportSource: `polen/react`,
126
- remarkPlugins: [remarkGfm],
95
+ remarkPlugins: [
96
+ // Parse frontmatter blocks so they're removed from content
97
+ remarkFrontmatter,
98
+ remarkGfm,
99
+ ],
127
100
  rehypePlugins: [
128
101
  [
129
102
  rehypeShiki,
@@ -150,62 +123,58 @@ export const Pages = ({
150
123
  // Dev server configuration
151
124
  configureServer(server) {
152
125
  // Add pages directory to watcher
153
- _debug(`configureServer: watch pages directory`, config.paths.project.absolute.pages)
126
+ debug(`configureServer: watch pages directory`, config.paths.project.absolute.pages)
154
127
  server.watcher.add(config.paths.project.absolute.pages)
155
128
  },
156
129
 
157
130
  // Hot update handling
158
131
  async handleHotUpdate({ file, server, modules }) {
159
- _debug(`handleHotUpdate`, file)
132
+ debug(`handleHotUpdate`, file)
160
133
  if (!isPageFile(file)) return
161
134
 
162
- _debug(`Page file changed:`, file)
135
+ debug(`Page file changed:`, file)
163
136
 
164
- // Check if this is a content-only change to an existing page
165
- const oldPages = pagesCache
137
+ // Get current pages before clearing cache
138
+ const oldPages = await scanPages()
166
139
 
167
140
  // Clear cache and rescan
168
- clearCache()
169
- const newPages = await scanPages()
170
- currentPagesData = newPages
141
+ scanPages.clear()
142
+ const newScanResult = await scanPages()
171
143
 
172
144
  // Check if page structure changed (added/removed pages)
173
- const structureChanged = !oldPages
174
- || oldPages.routes.length !== newPages.routes.length
175
- || !oldPages.routes.every((oldRoute, i) =>
176
- oldRoute.file.path.absolute === newPages.routes[i]?.file.path.absolute
177
- )
178
-
179
- if (structureChanged) {
180
- _debug(`Page structure changed, triggering full reload`)
181
-
182
- // Invalidate virtual module
183
- const mod = server.moduleGraph.getModuleById(viProjectPages.id)
184
- if (mod) {
185
- server.moduleGraph.invalidateModule(mod)
186
- _debug(`Invalidated pages virtual module`)
187
- }
145
+ const isJustContentChange = oldPages && !Arr.equalShallowly(
146
+ oldPages.list.map(p => Path.format(p.route.file.path.absolute)),
147
+ newScanResult.list.map(p => Path.format(p.route.file.path.absolute)),
148
+ )
188
149
 
189
- // Notify about changes
190
- if (onPagesChange) {
191
- reportDiagnostics(newPages.diagnostics)
192
- onPagesChange(newPages)
193
- }
194
-
195
- if (onTreeChange) {
196
- const tree = await scanTree()
197
- onTreeChange(tree)
198
- currentTreeData = tree
199
- }
200
-
201
- // Trigger full reload for structure changes
202
- server.ws.send({ type: `full-reload` })
203
- return []
204
- } else {
205
- _debug(`Page content changed, allowing HMR`)
150
+ if (isJustContentChange) {
151
+ debug(`Page content changed, allowing HMR`)
206
152
  // Let default HMR handle the MDX file change
207
153
  return modules
208
154
  }
155
+
156
+ //
157
+ // ━━ Manual Invalidation
158
+ //
159
+
160
+ debug(`Page structure changed, triggering full reload`)
161
+
162
+ // Invalidate virtual module
163
+ const mod = server.moduleGraph.getModuleById(viProjectPages.id)
164
+ if (mod) {
165
+ server.moduleGraph.invalidateModule(mod)
166
+ debug(`Invalidated pages virtual module`)
167
+ }
168
+
169
+ // Notify about changes
170
+ if (onChange) {
171
+ reportDiagnostics(newScanResult.diagnostics)
172
+ onChange(newScanResult)
173
+ }
174
+
175
+ // Trigger full reload for structure changes
176
+ server.ws.send({ type: `full-reload` })
177
+ return []
209
178
  },
210
179
  resolveId(id) {
211
180
  if (id === viProjectPagesData.id) {
@@ -218,25 +187,13 @@ export const Pages = ({
218
187
  // },
219
188
  async handler(id) {
220
189
  if (id !== viProjectPagesData.resolved) return
221
- _debug(`viProjectDataPages`)
222
-
223
- // Get pages data from the pages plugin or load initially
224
- if (!currentPagesData) {
225
- _debug(`loadingPagesDataInitially`)
226
- currentPagesData = await FileRouter.scan({
227
- dir: config.paths.project.absolute.pages,
228
- glob: `**/*.{md,mdx}`,
229
- })
230
- // Report any diagnostics from initial scan
231
- reportDiagnostics(currentPagesData.diagnostics)
232
- }
233
- if (!currentTreeData) {
234
- _debug(`loadingTreeDataInitially`)
235
- currentTreeData = await getRouteTree(config)
236
- }
237
- const pagesScanResult = currentPagesData
238
- const routeTree = currentTreeData
239
- _debug(`usingPageRoutesFromPagesPlugin`, pagesScanResult.routes.length)
190
+ debug(`viProjectDataPages`)
191
+
192
+ const scanResult = await scanPages()
193
+
194
+ // Report any diagnostics
195
+ reportDiagnostics(scanResult.diagnostics)
196
+ debug(`Found ${String(scanResult.list.length)} visible pages`)
240
197
 
241
198
  //
242
199
  // ━━ Build Navbar
@@ -248,34 +205,26 @@ export const Pages = ({
248
205
  navbarPages.length = 0 // Clear existing
249
206
 
250
207
  // Process first-level children as navigation items
251
- for (const child of routeTree.children) {
252
- if (child.value.type === 'directory') {
253
- // Check if this directory has an index file
254
- const hasIndex = child.children.some(c => c.value.type === 'file' && c.value.name === 'index')
255
-
256
- if (hasIndex) {
257
- const pathExp = FileRouter.pathToExpression([child.value.name])
258
- const title = Str.titlizeSlug(child.value.name)
208
+ if (scanResult.tree.root) {
209
+ for (const child of scanResult.tree.root.children) {
210
+ // Now we have Page objects in the tree
211
+ const page = child.value
212
+ const pathExp = FileRouter.routeToPathExpression(page.route)
213
+
214
+ // Skip hidden pages and index files at root level
215
+ if (page.metadata.hidden || page.route.logical.path.slice(-1)[0] === 'index') {
216
+ continue
217
+ }
218
+
219
+ // Only include top-level pages (files directly in pages directory)
220
+ if (page.route.logical.path.length === 1) {
221
+ const title = Str.titlizeSlug(page.route.logical.path[0]!)
259
222
  navbarPages.push({
260
223
  // IMPORTANT: Always ensure paths start with '/' for React Router compatibility.
261
- // Without the leading slash, React Router treats paths as relative, which causes
262
- // hydration mismatches between SSR (where base path is prepended) and client
263
- // (where basename is configured). This ensures consistent behavior.
264
224
  pathExp: pathExp.startsWith('/') ? pathExp : '/' + pathExp,
265
225
  title,
266
226
  })
267
227
  }
268
- } else if (child.value.type === 'file' && child.value.name !== 'index') {
269
- const pathExp = FileRouter.pathToExpression([child.value.name])
270
- const title = Str.titlizeSlug(child.value.name)
271
- navbarPages.push({
272
- // IMPORTANT: Always ensure paths start with '/' for React Router compatibility.
273
- // Without the leading slash, React Router treats paths as relative, which causes
274
- // hydration mismatches between SSR (where base path is prepended) and client
275
- // (where basename is configured). This ensures consistent behavior.
276
- pathExp: pathExp.startsWith('/') ? pathExp : '/' + pathExp,
277
- title,
278
- })
279
228
  }
280
229
  }
281
230
  }
@@ -286,17 +235,25 @@ export const Pages = ({
286
235
 
287
236
  const sidebarIndex: SidebarIndex = {}
288
237
 
289
- // Build sidebar for each top-level directory
290
- for (const child of routeTree.children) {
291
- if (child.value.type === 'directory') {
292
- const pathExp = `/${child.value.name}`
293
- // Create a subtree starting from this directory
294
- const subtree = Tree.node(child.value, child.children)
295
- // Pass the directory name as base path so paths are built correctly
296
- const sidebar = FileRouter.Sidebar.buildFromTree(subtree, [child.value.name])
297
- _debug(`Built sidebar for ${pathExp}:`, sidebar)
298
- sidebarIndex[pathExp] = sidebar
299
- }
238
+ // Build sidebar for each top-level directory using the page tree
239
+ if (scanResult.tree.root) {
240
+ Tree.visit(scanResult.tree, (node) => {
241
+ if (!node.value) return
242
+ const page = node.value as any
243
+ // Only process top-level directories (pages with logical path length > 1 indicate nested structure)
244
+ if (page.route.logical.path.length === 1 && node.children.length > 0) {
245
+ const topLevelDir = page.route.logical.path[0]!
246
+ const pathExp = `/${topLevelDir}`
247
+
248
+ // Create a subtree for this directory
249
+ const subtree = Tree.Tree(Tree.Node(page, node.children)) as Tree.Tree<any>
250
+
251
+ // Build sidebar using the new page tree builder
252
+ const sidebar = Content.buildFromPageTree(subtree, [topLevelDir])
253
+ debug(`Built sidebar for ${pathExp}:`, sidebar)
254
+ sidebarIndex[pathExp] = sidebar
255
+ }
256
+ })
300
257
  }
301
258
 
302
259
  //
@@ -305,7 +262,7 @@ export const Pages = ({
305
262
 
306
263
  const projectDataPages: ProjectDataPages = {
307
264
  sidebarIndex,
308
- pagesScanResult: pagesScanResult,
265
+ pages: scanResult.list,
309
266
  }
310
267
 
311
268
  // Return just the JSON string - let the JSON plugin handle the transformation
@@ -313,7 +270,7 @@ export const Pages = ({
313
270
  },
314
271
  },
315
272
  },
316
- // Plugin 4: Virtual Module for Pages Routes
273
+ // Plugin 3: Virtual Module for Pages Routes
317
274
  {
318
275
  name: 'polen:pages:routes',
319
276
  resolveId(id) {
@@ -328,17 +285,15 @@ export const Pages = ({
328
285
  handler: async (id) => {
329
286
  if (id !== viProjectPages.resolved) return
330
287
 
331
- _debug(`Loading viProjectPages virtual module`)
288
+ debug(`Loading viProjectPages virtual module`)
332
289
 
333
- // Ensure we have pages data
334
- if (!currentPagesData) {
335
- currentPagesData = await scanPages()
336
- reportDiagnostics(currentPagesData.diagnostics)
337
- }
290
+ const scanResult = await scanPages()
291
+ reportDiagnostics(scanResult.diagnostics)
292
+ const code = generatePagesModule(scanResult.list)
338
293
 
339
294
  // Generate the module code
340
295
  return {
341
- code: generatePagesModule(currentPagesData),
296
+ code,
342
297
  moduleType: 'js',
343
298
  }
344
299
  },
@@ -346,12 +301,3 @@ export const Pages = ({
346
301
  },
347
302
  ]
348
303
  }
349
-
350
- // Helper to get tree
351
- export const getRouteTree = async (config: Config.Config): Promise<FileRouter.RouteTreeNode> => {
352
- const result = await FileRouter.scanTree({
353
- dir: config.paths.project.absolute.pages,
354
- glob: `**/*.{md,mdx}`,
355
- })
356
- return result.routeTree
357
- }
@@ -3,7 +3,7 @@ import { reportError } from '#api/server/report-error'
3
3
  import type { Hono } from '#dep/hono/index'
4
4
  import type { Vite } from '#dep/vite/index'
5
5
  import { ResponseInternalServerError } from '#lib/kit-temp'
6
- import { debug } from '#singletons/debug'
6
+ import { debugPolen } from '#singletons/debug'
7
7
  import * as HonoNodeServer from '@hono/node-server'
8
8
  import { Err } from '@wollybeard/kit'
9
9
 
@@ -16,11 +16,11 @@ interface AppServerModule {
16
16
  export const Serve = (
17
17
  config: Config.Config,
18
18
  ): Vite.PluginOption => {
19
- const _debug = debug.sub(`serve`)
19
+ const debug = debugPolen.sub(`serve`)
20
20
  let appPromise: Promise<App | Error>
21
21
 
22
22
  const reloadApp = async ({ server }: { server: Vite.ViteDevServer }): Promise<App | Error> => {
23
- _debug('reloadApp')
23
+ debug('reloadApp')
24
24
  return server.ssrLoadModule(config.paths.framework.template.server.app)
25
25
  .then(module => module as AppServerModule)
26
26
  .then(module => module.app)
@@ -55,12 +55,12 @@ export const Serve = (
55
55
  }
56
56
  },
57
57
  handleHotUpdate({ server }) {
58
- _debug('handleHotUpdate')
58
+ debug('handleHotUpdate')
59
59
  // Reload app server immediately in the background
60
60
  appPromise = reloadApp({ server })
61
61
  },
62
62
  async configureServer(server) {
63
- _debug('configureServer')
63
+ debug('configureServer')
64
64
  // Initial load
65
65
  appPromise = reloadApp({ server })
66
66
 
@@ -1,7 +1,7 @@
1
1
  import type { Vite } from '#dep/vite/index'
2
2
  import { type ImportEvent, isSpecifierFromPackage } from '#lib/kit-temp'
3
3
  import { packagePaths } from '#package-paths'
4
- import { debug } from '#singletons/debug'
4
+ import { debugPolen } from '#singletons/debug'
5
5
  import type * as Module from 'node:module'
6
6
  import { fileURLToPath } from 'node:url'
7
7
 
@@ -18,7 +18,7 @@ export function initialize(data: SelfContainedModeHooksData) {
18
18
  export const resolve: Module.ResolveHook = async (specifier, context, nextResolve) => {
19
19
  if (!data_) throw new Error(`Self-contained mode not initialized`)
20
20
 
21
- const _debug = debug.sub(`node-module-hooks`)
21
+ const debug = debugPolen.sub(`node-module-hooks`)
22
22
 
23
23
  const from: ImportEvent = {
24
24
  specifier,
@@ -32,7 +32,7 @@ export const resolve: Module.ResolveHook = async (specifier, context, nextResolv
32
32
  importerPathExpOrFileUrlExp: from.context.parentURL,
33
33
  })
34
34
  ) {
35
- _debug(`resolve check`, { specifier, context })
35
+ debug(`resolve check`, { specifier, context })
36
36
 
37
37
  const to: ImportEvent = {
38
38
  specifier: from.specifier,
@@ -43,7 +43,7 @@ export const resolve: Module.ResolveHook = async (specifier, context, nextResolv
43
43
  },
44
44
  }
45
45
 
46
- _debug(`resolve`, { from, to })
46
+ debug(`resolve`, { from, to })
47
47
 
48
48
  await nextResolve(to.specifier, to.context)
49
49
  }
@@ -66,7 +66,7 @@ export const checkIsSelfImportFromProject = (input: {
66
66
  }
67
67
 
68
68
  export const VitePluginSelfContainedMode = ({ projectDirPathExp }: { projectDirPathExp: string }): Vite.Plugin => {
69
- const d = debug.sub(`vite-plugin:self-contained-import`)
69
+ const d = debugPolen.sub(`vite-plugin:self-contained-import`)
70
70
 
71
71
  return {
72
72
  name: `polen:self-contained-import`,
@@ -0,0 +1 @@
1
+ export * from '#template/components/content/$$'
@@ -1,2 +1,2 @@
1
1
  export * from './metadata.ts'
2
- export * from './path-manager.ts'
2
+ export * from './path-manager.ts'
@@ -26,7 +26,7 @@ describe('metadata', () => {
26
26
  }
27
27
 
28
28
  await Deployment.metadata.write(metadata, testDir)
29
-
29
+
30
30
  const result = await Deployment.metadata.read(testDir)
31
31
  expect(result).toEqual(metadata)
32
32
  })
@@ -46,8 +46,8 @@ describe('metadata', () => {
46
46
  }
47
47
 
48
48
  await Deployment.metadata.write(metadata, testDir)
49
-
49
+
50
50
  const fileExists = await Fs.exists(`${testDir}/.deployment.json`)
51
51
  expect(fileExists).toBe(true)
52
52
  })
53
- })
53
+ })
@@ -1 +1 @@
1
- export * as Deployment from './$$.ts'
1
+ export * as Deployment from './$$.ts'
@@ -1,5 +1,3 @@
1
1
  export * from './linter.ts'
2
2
  export * from './route.ts'
3
- export * from './scan-tree.ts'
4
3
  export * from './scan.ts'
5
- export * from './sidebar/index.ts'
@@ -28,6 +28,8 @@ const createRoute = (path: string[], order?: number, isIndex = false): Route =>
28
28
  },
29
29
  },
30
30
  },
31
+ id: `/project/pages/${path.join('/')}/${name}.md`,
32
+ parentId: path.length > 1 ? `/project/pages/${path.slice(0, -1).join('/')}` : null,
31
33
  }
32
34
  }
33
35
 
@@ -36,6 +36,8 @@ export const pathToExpression = (path: Path) => {
36
36
  export interface Route {
37
37
  logical: RouteLogical
38
38
  file: RouteFile
39
+ id: string // Absolute file path for unique identification
40
+ parentId: string | null // Parent directory path, null for root-level files
39
41
  }
40
42
 
41
43
  export interface RouteLogical {
@@ -1,9 +1,7 @@
1
1
  import { TinyGlobby } from '#dep/tiny-globby/index'
2
- import { Tree } from '#lib/tree/index'
3
2
  import { Path, Str } from '@wollybeard/kit'
4
3
  import { type Diagnostic, lint } from './linter.ts'
5
- import { type Route, type RouteFile, type RouteLogical, routeToPathExpression } from './route.ts'
6
- import { scanTree } from './scan-tree.ts'
4
+ import { type Route, type RouteFile, type RouteLogical } from './route.ts'
7
5
 
8
6
  //
9
7
  //
@@ -47,21 +45,21 @@ export const scan = async (parameters: {
47
45
  dir: string
48
46
  glob?: string
49
47
  }): Promise<ScanResult> => {
50
- // Use tree-based scanner
51
- const treeResult = await scanTree(parameters)
52
-
53
- // Flatten tree to get routes
54
- const routes: Route[] = []
55
- Tree.visit(treeResult.routeTree, (node) => {
56
- if (node.value.type === 'file' && node.value.route) {
57
- routes.push(node.value.route)
58
- }
48
+ const { dir, glob = `**/*.{md,mdx}` } = parameters
49
+
50
+ // Get all files directly
51
+ const filePaths = await TinyGlobby.glob(glob, {
52
+ absolute: true,
53
+ cwd: dir,
54
+ onlyFiles: true,
59
55
  })
60
56
 
57
+ // Convert to routes
58
+ const routes = filePaths.map(filePath => filePathToRoute(filePath, dir))
59
+
61
60
  // Apply linting
62
61
  const lintResult = lint(routes)
63
62
 
64
- // Routes are already sorted by the tree structure
65
63
  return lintResult
66
64
  }
67
65
 
@@ -74,9 +72,17 @@ export const filePathToRoute = (filePathExpression: string, rootDir: string): Ro
74
72
  }
75
73
  const logical = filePathToRouteLogical(file.path.relative)
76
74
 
75
+ // Generate id and parentId for tree building
76
+ const id = filePathExpression // Use absolute path as unique ID
77
+ const relativePath = Path.relative(rootDir, filePathExpression)
78
+ const parentDir = Path.dirname(relativePath)
79
+ const parentId = parentDir === '.' ? null : Path.join(rootDir, parentDir)
80
+
77
81
  return {
78
82
  logical,
79
83
  file,
84
+ id,
85
+ parentId,
80
86
  }
81
87
  }
82
88
 
@@ -137,10 +137,10 @@ describe('runAndExit', () => {
137
137
  const double = async (x: number) => x * 2
138
138
 
139
139
  await expect(() => Task.runAndExit(double, 5, { name: 'double' })).rejects.toThrow('process.exit called')
140
-
140
+
141
141
  expect(mockExit).toHaveBeenCalledWith(0)
142
142
  expect(mockLog).toHaveBeenCalled()
143
-
143
+
144
144
  // Verify the logged output contains expected content
145
145
  const loggedOutput = mockLog.mock.calls[0]?.[0]
146
146
  expect(loggedOutput).toContain('double')
@@ -161,7 +161,7 @@ describe('runAndExit', () => {
161
161
  }
162
162
 
163
163
  await expect(() => Task.runAndExit(failing, null, { name: 'failing' })).rejects.toThrow('process.exit called')
164
-
164
+
165
165
  expect(mockExit).toHaveBeenCalledWith(1)
166
166
  expect(mockLog).toHaveBeenCalled()
167
167
 
@@ -1,3 +1,3 @@
1
1
  import { Debug } from '@wollybeard/kit'
2
2
 
3
- export const debug = Debug.create(`polen`)
3
+ export const debugPolen = Debug.create(`polen`)