@nuraly/lumenjs 0.1.3 → 0.1.4

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 (333) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +73 -0
  11. package/dist/auth/native-auth.js +293 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +98 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/utils.d.ts +7 -0
  31. package/dist/auth/routes/utils.js +35 -0
  32. package/dist/auth/routes/verify.d.ts +3 -0
  33. package/dist/auth/routes/verify.js +26 -0
  34. package/dist/auth/routes.d.ts +8 -0
  35. package/dist/auth/routes.js +110 -0
  36. package/dist/auth/session.d.ts +8 -0
  37. package/dist/auth/session.js +54 -0
  38. package/dist/auth/token.d.ts +33 -0
  39. package/dist/auth/token.js +90 -0
  40. package/dist/auth/types.d.ts +156 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/build/build-client.d.ts +15 -0
  43. package/dist/build/build-client.js +45 -0
  44. package/dist/build/build-prerender.d.ts +11 -0
  45. package/dist/build/build-prerender.js +159 -0
  46. package/dist/build/build-server.d.ts +17 -0
  47. package/dist/build/build-server.js +98 -0
  48. package/dist/build/build.js +48 -120
  49. package/dist/build/scan.d.ts +17 -0
  50. package/dist/build/scan.js +76 -6
  51. package/dist/build/serve-api.js +8 -2
  52. package/dist/build/serve-loaders.d.ts +4 -4
  53. package/dist/build/serve-loaders.js +26 -18
  54. package/dist/build/serve-ssr.js +38 -11
  55. package/dist/build/serve-static.js +3 -3
  56. package/dist/build/serve.js +218 -15
  57. package/dist/cli.js +37 -6
  58. package/dist/communication/encryption.d.ts +35 -0
  59. package/dist/communication/encryption.js +90 -0
  60. package/dist/communication/handlers/context.d.ts +27 -0
  61. package/dist/communication/handlers/context.js +1 -0
  62. package/dist/communication/handlers/conversation.d.ts +24 -0
  63. package/dist/communication/handlers/conversation.js +113 -0
  64. package/dist/communication/handlers/file-upload.d.ts +17 -0
  65. package/dist/communication/handlers/file-upload.js +62 -0
  66. package/dist/communication/handlers/messaging.d.ts +30 -0
  67. package/dist/communication/handlers/messaging.js +237 -0
  68. package/dist/communication/handlers/presence.d.ts +15 -0
  69. package/dist/communication/handlers/presence.js +76 -0
  70. package/dist/communication/handlers.d.ts +5 -0
  71. package/dist/communication/handlers.js +5 -0
  72. package/dist/communication/index.d.ts +9 -0
  73. package/dist/communication/index.js +7 -0
  74. package/dist/communication/link-preview.d.ts +18 -0
  75. package/dist/communication/link-preview.js +115 -0
  76. package/dist/communication/schema.d.ts +10 -0
  77. package/dist/communication/schema.js +101 -0
  78. package/dist/communication/server.d.ts +86 -0
  79. package/dist/communication/server.js +212 -0
  80. package/dist/communication/signaling.d.ts +43 -0
  81. package/dist/communication/signaling.js +271 -0
  82. package/dist/communication/store.d.ts +71 -0
  83. package/dist/communication/store.js +289 -0
  84. package/dist/communication/types.d.ts +454 -0
  85. package/dist/communication/types.js +1 -0
  86. package/dist/create.d.ts +1 -0
  87. package/dist/create.js +55 -0
  88. package/dist/db/auto-migrate.d.ts +3 -0
  89. package/dist/db/auto-migrate.js +100 -0
  90. package/dist/db/client.d.ts +3 -0
  91. package/dist/db/client.js +18 -0
  92. package/dist/db/index.d.ts +17 -13
  93. package/dist/db/index.js +205 -26
  94. package/dist/db/seed.d.ts +12 -0
  95. package/dist/db/seed.js +88 -0
  96. package/dist/db/table.d.ts +10 -0
  97. package/dist/db/table.js +12 -0
  98. package/dist/dev-server/config.d.ts +11 -0
  99. package/dist/dev-server/config.js +23 -20
  100. package/dist/dev-server/index-html.d.ts +3 -0
  101. package/dist/dev-server/index-html.js +18 -6
  102. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  103. package/dist/dev-server/nuralyui-aliases.js +115 -94
  104. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  105. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  106. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  107. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  108. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  109. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  110. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  111. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  113. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  114. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  115. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  116. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  117. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  118. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  119. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  120. package/dist/dev-server/plugins/vite-plugin-routes.js +15 -5
  121. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  122. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  123. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  125. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  126. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  127. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
  128. package/dist/dev-server/server.js +127 -13
  129. package/dist/dev-server/ssr-render.d.ts +2 -1
  130. package/dist/dev-server/ssr-render.js +107 -48
  131. package/dist/editor/ai/backend.d.ts +20 -0
  132. package/dist/editor/ai/backend.js +104 -0
  133. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  134. package/dist/editor/ai/claude-code-client.js +145 -0
  135. package/dist/editor/ai/opencode-client.d.ts +14 -0
  136. package/dist/editor/ai/opencode-client.js +125 -0
  137. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  138. package/dist/editor/ai/snapshot-store.js +35 -0
  139. package/dist/editor/ai/types.d.ts +30 -0
  140. package/dist/editor/ai/types.js +136 -0
  141. package/dist/editor/ai-chat-panel.d.ts +13 -0
  142. package/dist/editor/ai-chat-panel.js +587 -0
  143. package/dist/editor/ai-markdown.d.ts +10 -0
  144. package/dist/editor/ai-markdown.js +70 -0
  145. package/dist/editor/ai-project-panel.d.ts +11 -0
  146. package/dist/editor/ai-project-panel.js +332 -0
  147. package/dist/editor/ast-modification.d.ts +11 -0
  148. package/dist/editor/ast-modification.js +1 -0
  149. package/dist/editor/ast-service.d.ts +30 -0
  150. package/dist/editor/ast-service.js +180 -0
  151. package/dist/editor/css-rules.d.ts +54 -0
  152. package/dist/editor/css-rules.js +423 -0
  153. package/dist/editor/editor-api-client.d.ts +51 -0
  154. package/dist/editor/editor-api-client.js +162 -0
  155. package/dist/editor/editor-bridge.d.ts +1 -0
  156. package/dist/editor/editor-bridge.js +17 -8
  157. package/dist/editor/editor-toolbar.d.ts +14 -0
  158. package/dist/editor/editor-toolbar.js +115 -0
  159. package/dist/editor/file-editor.d.ts +9 -0
  160. package/dist/editor/file-editor.js +236 -0
  161. package/dist/editor/file-service.d.ts +16 -0
  162. package/dist/editor/file-service.js +52 -0
  163. package/dist/editor/i18n-key-gen.d.ts +1 -0
  164. package/dist/editor/i18n-key-gen.js +7 -0
  165. package/dist/editor/inline-text-edit.d.ts +5 -0
  166. package/dist/editor/inline-text-edit.js +173 -92
  167. package/dist/editor/overlay-events.d.ts +5 -0
  168. package/dist/editor/overlay-events.js +364 -0
  169. package/dist/editor/overlay-hmr.d.ts +2 -0
  170. package/dist/editor/overlay-hmr.js +75 -0
  171. package/dist/editor/overlay-selection.d.ts +29 -0
  172. package/dist/editor/overlay-selection.js +148 -0
  173. package/dist/editor/overlay-utils.d.ts +12 -0
  174. package/dist/editor/overlay-utils.js +59 -0
  175. package/dist/editor/properties-panel-persist.d.ts +14 -0
  176. package/dist/editor/properties-panel-persist.js +70 -0
  177. package/dist/editor/properties-panel-rows.d.ts +10 -0
  178. package/dist/editor/properties-panel-rows.js +349 -0
  179. package/dist/editor/properties-panel-styles.d.ts +4 -0
  180. package/dist/editor/properties-panel-styles.js +174 -0
  181. package/dist/editor/properties-panel.d.ts +4 -0
  182. package/dist/editor/properties-panel.js +148 -0
  183. package/dist/editor/property-registry.d.ts +16 -0
  184. package/dist/editor/property-registry.js +303 -0
  185. package/dist/editor/standalone-file-panel.d.ts +0 -0
  186. package/dist/editor/standalone-file-panel.js +1 -0
  187. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  188. package/dist/editor/standalone-overlay-dom.js +1 -0
  189. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  190. package/dist/editor/standalone-overlay-styles.js +1 -0
  191. package/dist/editor/standalone-overlay.d.ts +1 -0
  192. package/dist/editor/standalone-overlay.js +76 -0
  193. package/dist/editor/syntax-highlighter.d.ts +4 -0
  194. package/dist/editor/syntax-highlighter.js +81 -0
  195. package/dist/editor/text-toolbar.d.ts +11 -0
  196. package/dist/editor/text-toolbar.js +327 -0
  197. package/dist/editor/toolbar-styles.d.ts +4 -0
  198. package/dist/editor/toolbar-styles.js +198 -0
  199. package/dist/email/index.d.ts +32 -0
  200. package/dist/email/index.js +154 -0
  201. package/dist/email/providers/resend.d.ts +2 -0
  202. package/dist/email/providers/resend.js +24 -0
  203. package/dist/email/providers/sendgrid.d.ts +2 -0
  204. package/dist/email/providers/sendgrid.js +31 -0
  205. package/dist/email/providers/smtp.d.ts +13 -0
  206. package/dist/email/providers/smtp.js +125 -0
  207. package/dist/email/template-engine.d.ts +18 -0
  208. package/dist/email/template-engine.js +116 -0
  209. package/dist/email/templates/base.d.ts +9 -0
  210. package/dist/email/templates/base.js +65 -0
  211. package/dist/email/templates/password-reset.d.ts +5 -0
  212. package/dist/email/templates/password-reset.js +15 -0
  213. package/dist/email/templates/verify-email.d.ts +5 -0
  214. package/dist/email/templates/verify-email.js +15 -0
  215. package/dist/email/templates/welcome.d.ts +5 -0
  216. package/dist/email/templates/welcome.js +13 -0
  217. package/dist/email/types.d.ts +49 -0
  218. package/dist/email/types.js +1 -0
  219. package/dist/llms/generate.d.ts +46 -0
  220. package/dist/llms/generate.js +185 -0
  221. package/dist/permissions/guard.d.ts +28 -0
  222. package/dist/permissions/guard.js +30 -0
  223. package/dist/permissions/index.d.ts +6 -0
  224. package/dist/permissions/index.js +3 -0
  225. package/dist/permissions/service.d.ts +80 -0
  226. package/dist/permissions/service.js +210 -0
  227. package/dist/permissions/tables.d.ts +5 -0
  228. package/dist/permissions/tables.js +68 -0
  229. package/dist/permissions/types.d.ts +33 -0
  230. package/dist/permissions/types.js +1 -0
  231. package/dist/runtime/app-shell.js +163 -0
  232. package/dist/runtime/auth.d.ts +10 -0
  233. package/dist/runtime/auth.js +30 -0
  234. package/dist/runtime/communication.d.ts +137 -0
  235. package/dist/runtime/communication.js +228 -0
  236. package/dist/runtime/error-boundary.d.ts +23 -0
  237. package/dist/runtime/error-boundary.js +120 -0
  238. package/dist/runtime/i18n.d.ts +6 -1
  239. package/dist/runtime/i18n.js +42 -21
  240. package/dist/runtime/router-data.d.ts +3 -0
  241. package/dist/runtime/router-data.js +102 -17
  242. package/dist/runtime/router-hydration.js +25 -0
  243. package/dist/runtime/router.d.ts +16 -1
  244. package/dist/runtime/router.js +188 -42
  245. package/dist/runtime/socket-client.d.ts +2 -0
  246. package/dist/runtime/socket-client.js +30 -0
  247. package/dist/runtime/webrtc.d.ts +47 -0
  248. package/dist/runtime/webrtc.js +178 -0
  249. package/dist/shared/graceful-shutdown.d.ts +8 -0
  250. package/dist/shared/graceful-shutdown.js +36 -0
  251. package/dist/shared/health.d.ts +8 -0
  252. package/dist/shared/health.js +25 -0
  253. package/dist/shared/llms-txt.d.ts +31 -0
  254. package/dist/shared/llms-txt.js +85 -0
  255. package/dist/shared/logger.d.ts +32 -0
  256. package/dist/shared/logger.js +93 -0
  257. package/dist/shared/meta.d.ts +27 -0
  258. package/dist/shared/meta.js +71 -0
  259. package/dist/shared/middleware-runner.d.ts +9 -0
  260. package/dist/shared/middleware-runner.js +29 -0
  261. package/dist/shared/rate-limit.d.ts +18 -0
  262. package/dist/shared/rate-limit.js +71 -0
  263. package/dist/shared/request-id.d.ts +5 -0
  264. package/dist/shared/request-id.js +18 -0
  265. package/dist/shared/route-matching.js +16 -1
  266. package/dist/shared/security-headers.d.ts +18 -0
  267. package/dist/shared/security-headers.js +38 -0
  268. package/dist/shared/socket-io-setup.d.ts +11 -0
  269. package/dist/shared/socket-io-setup.js +51 -0
  270. package/dist/shared/types.d.ts +14 -0
  271. package/dist/shared/utils.d.ts +33 -7
  272. package/dist/shared/utils.js +164 -27
  273. package/dist/storage/adapters/local.d.ts +44 -0
  274. package/dist/storage/adapters/local.js +85 -0
  275. package/dist/storage/adapters/s3.d.ts +32 -0
  276. package/dist/storage/adapters/s3.js +116 -0
  277. package/dist/storage/adapters/types.d.ts +53 -0
  278. package/dist/storage/adapters/types.js +1 -0
  279. package/dist/storage/index.d.ts +76 -0
  280. package/dist/storage/index.js +83 -0
  281. package/package.json +19 -7
  282. package/templates/blog/api/posts.ts +4 -18
  283. package/templates/blog/data/migrations/001_init.sql +6 -5
  284. package/templates/blog/lumenjs.config.ts +3 -0
  285. package/templates/blog/package.json +14 -0
  286. package/templates/blog/pages/_layout.ts +25 -0
  287. package/templates/blog/pages/index.ts +48 -22
  288. package/templates/blog/pages/posts/[slug].ts +45 -20
  289. package/templates/blog/pages/tag/[tag].ts +44 -0
  290. package/templates/dashboard/api/stats.ts +8 -5
  291. package/templates/dashboard/lumenjs.config.ts +3 -0
  292. package/templates/dashboard/package.json +14 -0
  293. package/templates/dashboard/pages/_layout.ts +25 -0
  294. package/templates/dashboard/pages/index.ts +54 -23
  295. package/templates/dashboard/pages/settings/index.ts +29 -0
  296. package/templates/default/lumenjs.config.ts +3 -0
  297. package/templates/default/package.json +14 -0
  298. package/templates/default/pages/index.ts +24 -0
  299. package/templates/social/api/posts/[id].ts +14 -0
  300. package/templates/social/api/posts.ts +11 -0
  301. package/templates/social/api/profile/[username].ts +10 -0
  302. package/templates/social/api/upload.ts +19 -0
  303. package/templates/social/data/migrations/001_init.sql +78 -0
  304. package/templates/social/data/migrations/002_add_image_url.sql +1 -0
  305. package/templates/social/data/migrations/003_auth.sql +7 -0
  306. package/templates/social/docs/architecture.md +76 -0
  307. package/templates/social/docs/components.md +100 -0
  308. package/templates/social/docs/data.md +89 -0
  309. package/templates/social/docs/pages.md +96 -0
  310. package/templates/social/docs/theming.md +52 -0
  311. package/templates/social/lib/media.ts +130 -0
  312. package/templates/social/lumenjs.auth.ts +21 -0
  313. package/templates/social/lumenjs.config.ts +3 -0
  314. package/templates/social/package.json +5 -0
  315. package/templates/social/pages/_layout.ts +239 -0
  316. package/templates/social/pages/apps/[id].ts +173 -0
  317. package/templates/social/pages/apps/index.ts +116 -0
  318. package/templates/social/pages/auth/login.ts +92 -0
  319. package/templates/social/pages/bookmarks.ts +57 -0
  320. package/templates/social/pages/explore.ts +73 -0
  321. package/templates/social/pages/index.ts +351 -0
  322. package/templates/social/pages/messages.ts +298 -0
  323. package/templates/social/pages/new.ts +77 -0
  324. package/templates/social/pages/notifications.ts +73 -0
  325. package/templates/social/pages/post/[id].ts +124 -0
  326. package/templates/social/pages/profile/[username].ts +100 -0
  327. package/templates/social/pages/settings/accessibility.ts +153 -0
  328. package/templates/social/pages/settings/account.ts +260 -0
  329. package/templates/social/pages/settings/help.ts +141 -0
  330. package/templates/social/pages/settings/language.ts +103 -0
  331. package/templates/social/pages/settings/privacy.ts +183 -0
  332. package/templates/social/pages/settings/security.ts +133 -0
  333. package/templates/social/pages/settings.ts +185 -0
@@ -0,0 +1,159 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { pathToFileURL } from 'url';
4
+ import { filePathToTagName, stripOuterLitMarkers, dirToLayoutTagName, isRedirectResponse, patchLoaderDataSpread } from '../shared/utils.js';
5
+ import { installDomShims } from '../shared/dom-shims.js';
6
+ import { getLayoutDirsForPage } from './scan.js';
7
+ export async function prerenderPages(opts) {
8
+ const { serverDir, clientDir, pagesDir, pageEntries, layoutEntries, manifest } = opts;
9
+ const prerenderEntries = pageEntries.filter(e => e.prerender);
10
+ if (prerenderEntries.length === 0)
11
+ return;
12
+ console.log(`[LumenJS] Pre-rendering ${prerenderEntries.length} page(s)...`);
13
+ // Load SSR runtime (installs global DOM shims)
14
+ const ssrRuntimePath = pathToFileURL(path.join(serverDir, 'ssr-runtime.js')).href;
15
+ const ssrRuntime = await import(ssrRuntimePath);
16
+ const { render, html, unsafeStatic } = ssrRuntime;
17
+ // Install additional DOM shims
18
+ installDomShims();
19
+ // Read the built index.html shell
20
+ const indexHtmlShell = fs.readFileSync(path.join(clientDir, 'index.html'), 'utf-8');
21
+ let prerenderCount = 0;
22
+ for (const page of prerenderEntries) {
23
+ // Resolve server module path (Rollup sanitizes brackets in filenames)
24
+ let modulePath = path.join(serverDir, `pages/${page.name}.js`);
25
+ if (!fs.existsSync(modulePath)) {
26
+ modulePath = path.join(serverDir, `pages/${page.name}.js`.replace(/\[/g, '_').replace(/\]/g, '_'));
27
+ }
28
+ if (!fs.existsSync(modulePath)) {
29
+ console.warn(` Skipping ${page.routePath}: server module not found`);
30
+ continue;
31
+ }
32
+ const mod = await import(pathToFileURL(modulePath).href);
33
+ // Determine paths to pre-render
34
+ const isDynamic = page.routePath.includes(':');
35
+ let pathsToRender = [];
36
+ if (isDynamic) {
37
+ // Dynamic route — call prerenderPaths() for param combinations
38
+ if (typeof mod.prerenderPaths === 'function') {
39
+ const paramsList = await mod.prerenderPaths();
40
+ for (const params of paramsList) {
41
+ // Build pathname from route pattern and params
42
+ let pathname = page.routePath;
43
+ for (const [key, value] of Object.entries(params)) {
44
+ // Handle both :param and :...param (catch-all) patterns
45
+ pathname = pathname.replace(`:...${key}`, value).replace(`:${key}`, value);
46
+ }
47
+ pathsToRender.push({ pathname, params: params });
48
+ }
49
+ }
50
+ else {
51
+ console.warn(` Skipping ${page.routePath}: dynamic route without prerenderPaths()`);
52
+ continue;
53
+ }
54
+ }
55
+ else {
56
+ // Static route — render once
57
+ pathsToRender.push({ pathname: page.routePath, params: {} });
58
+ }
59
+ for (const { pathname, params } of pathsToRender) {
60
+ // Run loader if present
61
+ let loaderData = undefined;
62
+ if (mod.loader && typeof mod.loader === 'function') {
63
+ loaderData = await mod.loader({ params, query: {}, url: pathname, headers: {} });
64
+ if (isRedirectResponse(loaderData)) {
65
+ console.warn(` Skipping ${pathname}: loader returned redirect`);
66
+ continue;
67
+ }
68
+ }
69
+ // Get tag name
70
+ const relPath = path.relative(pagesDir, page.filePath).replace(/\\/g, '/');
71
+ const tagName = filePathToTagName(relPath);
72
+ // Load and render layout chain
73
+ const routeLayouts = getLayoutDirsForPage(page.filePath, pagesDir, layoutEntries);
74
+ const layoutModules = [];
75
+ const layoutsData = [];
76
+ for (const dir of routeLayouts) {
77
+ const layout = layoutEntries.find(l => l.dir === dir);
78
+ if (!layout)
79
+ continue;
80
+ let layoutLoaderData = undefined;
81
+ if (layout.hasLoader) {
82
+ const manifestLayout = manifest.layouts.find(l => l.dir === dir);
83
+ if (manifestLayout?.module) {
84
+ const layoutModPath = path.join(serverDir, manifestLayout.module);
85
+ if (fs.existsSync(layoutModPath)) {
86
+ const layoutMod = await import(pathToFileURL(layoutModPath).href);
87
+ if (layoutMod.loader && typeof layoutMod.loader === 'function') {
88
+ layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: {} });
89
+ if (isRedirectResponse(layoutLoaderData))
90
+ continue;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ const layoutTagName = dirToLayoutTagName(dir);
96
+ layoutModules.push({ tagName: layoutTagName, loaderData: layoutLoaderData });
97
+ layoutsData.push({ loaderPath: dir, data: layoutLoaderData });
98
+ }
99
+ // Patch element classes to spread loaderData
100
+ for (const lm of layoutModules) {
101
+ patchLoaderDataSpread(lm.tagName);
102
+ }
103
+ patchLoaderDataSpread(tagName);
104
+ // SSR render page
105
+ const pageTag = unsafeStatic(tagName);
106
+ const pageTemplate = html `<${pageTag} .loaderData=${loaderData}></${pageTag}>`;
107
+ let ssrHtml = '';
108
+ for (const chunk of render(pageTemplate)) {
109
+ ssrHtml += typeof chunk === 'string' ? chunk : String(chunk);
110
+ }
111
+ ssrHtml = stripOuterLitMarkers(ssrHtml);
112
+ // Wrap in layout chain (inside-out, deepest first)
113
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
114
+ const lTag = unsafeStatic(layoutModules[i].tagName);
115
+ const lData = layoutModules[i].loaderData;
116
+ const lTemplate = html `<${lTag} .loaderData=${lData}></${lTag}>`;
117
+ let lHtml = '';
118
+ for (const chunk of render(lTemplate)) {
119
+ lHtml += typeof chunk === 'string' ? chunk : String(chunk);
120
+ }
121
+ if (i > 0) {
122
+ lHtml = stripOuterLitMarkers(lHtml);
123
+ }
124
+ const closingTag = `</${layoutModules[i].tagName}>`;
125
+ const closingIdx = lHtml.lastIndexOf(closingTag);
126
+ if (closingIdx !== -1) {
127
+ ssrHtml = lHtml.slice(0, closingIdx) + ssrHtml + lHtml.slice(closingIdx);
128
+ }
129
+ else {
130
+ ssrHtml = lHtml + ssrHtml;
131
+ }
132
+ }
133
+ // Build SSR data script
134
+ const ssrDataObj = layoutsData.length > 0
135
+ ? { page: loaderData, layouts: layoutsData }
136
+ : loaderData;
137
+ const loaderDataScript = ssrDataObj !== undefined
138
+ ? `<script type="application/json" id="__nk_ssr_data__">${JSON.stringify(ssrDataObj).replace(/</g, '\\u003c')}</script>`
139
+ : '';
140
+ const hydrateScript = `<script type="module">import '@lit-labs/ssr-client/lit-element-hydrate-support.js';</script>`;
141
+ // Build final HTML from the shell
142
+ let htmlOut = indexHtmlShell;
143
+ htmlOut = htmlOut.replace('<script type="module"', `${hydrateScript}\n <script type="module"`);
144
+ htmlOut = htmlOut.replace(/<nk-app><\/nk-app>/, `${loaderDataScript}<nk-app data-nk-ssr><div id="nk-router-outlet">${ssrHtml}</div></nk-app>`);
145
+ // Write pre-rendered HTML file
146
+ const outPath = pathname === '/'
147
+ ? path.join(clientDir, 'index.html')
148
+ : path.join(clientDir, pathname, 'index.html');
149
+ // Don't overwrite the root index.html for non-root pages
150
+ if (pathname !== '/') {
151
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
152
+ }
153
+ fs.writeFileSync(outPath, htmlOut);
154
+ prerenderCount++;
155
+ console.log(` Pre-rendered: ${pathname}`);
156
+ }
157
+ }
158
+ console.log(`[LumenJS] Pre-rendered ${prerenderCount} page(s).`);
159
+ }
@@ -0,0 +1,17 @@
1
+ import { type UserConfig, type Plugin } from 'vite';
2
+ import type { PageEntry, LayoutEntry, ApiEntry } from './scan.js';
3
+ export interface BuildServerOptions {
4
+ projectDir: string;
5
+ serverDir: string;
6
+ pageEntries: PageEntry[];
7
+ layoutEntries: LayoutEntry[];
8
+ apiEntries: ApiEntry[];
9
+ hasAuthConfig: boolean;
10
+ authConfigPath: string;
11
+ shared: {
12
+ resolve: UserConfig['resolve'];
13
+ esbuild: UserConfig['esbuild'];
14
+ plugins: Plugin[];
15
+ };
16
+ }
17
+ export declare function buildServer(opts: BuildServerOptions): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { build as viteBuild } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ export async function buildServer(opts) {
5
+ const { projectDir, serverDir, pageEntries, layoutEntries, apiEntries, hasAuthConfig, authConfigPath, shared } = opts;
6
+ console.log('[LumenJS] Building server bundle...');
7
+ // Collect server entry points (pages with loaders + layouts with loaders + API routes)
8
+ const serverEntries = {};
9
+ for (const entry of pageEntries) {
10
+ if (entry.hasLoader || entry.hasSubscribe || entry.prerender) {
11
+ serverEntries[`pages/${entry.name}`] = entry.filePath;
12
+ }
13
+ }
14
+ for (const entry of layoutEntries) {
15
+ if (entry.hasLoader || entry.hasSubscribe) {
16
+ const entryName = entry.dir ? `layouts/${entry.dir}/_layout` : 'layouts/_layout';
17
+ serverEntries[entryName] = entry.filePath;
18
+ }
19
+ }
20
+ for (const entry of apiEntries) {
21
+ serverEntries[`api/${entry.name}`] = entry.filePath;
22
+ }
23
+ if (hasAuthConfig) {
24
+ serverEntries['auth-config'] = authConfigPath;
25
+ }
26
+ // If data/seed.ts exists, include it in the server bundle for prod seed support
27
+ const seedPath = path.join(projectDir, 'data', 'seed.ts');
28
+ if (fs.existsSync(seedPath)) {
29
+ serverEntries['seed'] = seedPath;
30
+ }
31
+ // Create SSR runtime entry — bundles @lit-labs/ssr alongside Lit so all
32
+ // server modules share one Lit instance (avoids _$EM mismatches).
33
+ const ssrEntryPath = path.join(projectDir, '__nk_ssr_entry.js');
34
+ const hasPageLoaders = pageEntries.some(e => e.hasLoader);
35
+ const hasLayoutLoaders = layoutEntries.some(e => e.hasLoader);
36
+ const hasPrerenderPages = pageEntries.some(e => e.prerender);
37
+ if (hasPageLoaders || hasLayoutLoaders || hasPrerenderPages) {
38
+ fs.writeFileSync(ssrEntryPath, [
39
+ "import '@lit-labs/ssr/lib/install-global-dom-shim.js';",
40
+ "export { render } from '@lit-labs/ssr';",
41
+ "export { html, unsafeStatic } from 'lit/static-html.js';",
42
+ ].join('\n'));
43
+ serverEntries['ssr-runtime'] = ssrEntryPath;
44
+ }
45
+ try {
46
+ if (Object.keys(serverEntries).length > 0) {
47
+ await viteBuild({
48
+ root: projectDir,
49
+ resolve: shared.resolve,
50
+ plugins: shared.plugins,
51
+ esbuild: shared.esbuild,
52
+ build: {
53
+ outDir: serverDir,
54
+ emptyOutDir: true,
55
+ ssr: true,
56
+ rollupOptions: {
57
+ input: serverEntries,
58
+ output: {
59
+ format: 'esm',
60
+ entryFileNames: '[name].js',
61
+ chunkFileNames: 'assets/[name]-[hash].js',
62
+ manualChunks(id) {
63
+ // Force all Lit packages into a single shared chunk so SSR runtime
64
+ // and page modules use the exact same Lit class instances.
65
+ if (id.includes('/node_modules/lit/') ||
66
+ id.includes('/node_modules/lit-html/') ||
67
+ id.includes('/node_modules/lit-element/') ||
68
+ id.includes('/node_modules/@lit/reactive-element/')) {
69
+ return 'lit-shared';
70
+ }
71
+ },
72
+ },
73
+ external: [
74
+ /^node:/,
75
+ 'os', 'fs', 'path', 'url', 'util', 'crypto', 'http', 'https', 'net',
76
+ 'stream', 'zlib', 'events', 'buffer', 'querystring', 'child_process',
77
+ 'worker_threads', 'cluster', 'dns', 'tls', 'assert', 'constants',
78
+ // Native addons — must not be bundled, loaded from node_modules at runtime
79
+ 'better-sqlite3',
80
+ ],
81
+ },
82
+ },
83
+ logLevel: 'warn',
84
+ ssr: {
85
+ noExternal: true,
86
+ },
87
+ });
88
+ }
89
+ else {
90
+ fs.mkdirSync(serverDir, { recursive: true });
91
+ }
92
+ }
93
+ finally {
94
+ if (fs.existsSync(ssrEntryPath)) {
95
+ fs.unlinkSync(ssrEntryPath);
96
+ }
97
+ }
98
+ }
@@ -1,11 +1,12 @@
1
- import { build as viteBuild } from 'vite';
2
1
  import path from 'path';
3
2
  import fs from 'fs';
4
3
  import { getSharedViteConfig } from '../dev-server/server.js';
5
4
  import { readProjectConfig } from '../dev-server/config.js';
6
- import { generateIndexHtml } from '../dev-server/index-html.js';
7
5
  import { filePathToTagName } from '../shared/utils.js';
8
6
  import { scanPages, scanLayouts, scanApiRoutes, getLayoutDirsForPage } from './scan.js';
7
+ import { buildClient } from './build-client.js';
8
+ import { buildServer } from './build-server.js';
9
+ import { prerenderPages } from './build-prerender.js';
9
10
  export async function buildProject(options) {
10
11
  const { projectDir } = options;
11
12
  const outDir = options.outDir || path.join(projectDir, '.lumenjs');
@@ -19,130 +20,42 @@ export async function buildProject(options) {
19
20
  fs.rmSync(outDir, { recursive: true });
20
21
  }
21
22
  fs.mkdirSync(outDir, { recursive: true });
22
- const { title, integrations, i18n: i18nConfig } = readProjectConfig(projectDir);
23
+ const { title, integrations, i18n: i18nConfig, prefetch: prefetchStrategy, prerender: globalPrerender } = readProjectConfig(projectDir);
23
24
  const shared = getSharedViteConfig(projectDir, { mode: 'production', integrations });
24
25
  // Scan pages, layouts, and API routes for the manifest
25
26
  const pageEntries = scanPages(pagesDir);
26
27
  const layoutEntries = scanLayouts(pagesDir);
27
28
  const apiEntries = scanApiRoutes(apiDir);
28
- // --- Client build ---
29
- console.log('[LumenJS] Building client bundle...');
30
- // Generate index.html as build entry
31
- const indexHtml = generateIndexHtml({ title, editorMode: false, integrations });
32
- const tempIndexPath = path.join(projectDir, '__nk_build_index.html');
33
- fs.writeFileSync(tempIndexPath, indexHtml);
34
- try {
35
- await viteBuild({
36
- root: projectDir,
37
- publicDir: fs.existsSync(publicDir) ? publicDir : undefined,
38
- resolve: shared.resolve,
39
- plugins: shared.plugins,
40
- esbuild: shared.esbuild,
41
- build: {
42
- outDir: clientDir,
43
- emptyOutDir: true,
44
- rollupOptions: {
45
- input: tempIndexPath,
46
- },
47
- },
48
- logLevel: 'warn',
49
- });
50
- }
51
- finally {
52
- // Clean up temp file
53
- if (fs.existsSync(tempIndexPath)) {
54
- fs.unlinkSync(tempIndexPath);
29
+ // Check for auth config
30
+ const authConfigPath = path.join(projectDir, 'lumenjs.auth.ts');
31
+ const hasAuthConfig = fs.existsSync(authConfigPath);
32
+ // Apply global prerender flag from config
33
+ if (globalPrerender) {
34
+ for (const entry of pageEntries) {
35
+ entry.prerender = true;
55
36
  }
56
37
  }
57
- // Rename the built HTML file from __nk_build_index.html to index.html
58
- const builtHtmlPath = path.join(clientDir, '__nk_build_index.html');
59
- const finalHtmlPath = path.join(clientDir, 'index.html');
60
- if (fs.existsSync(builtHtmlPath)) {
61
- fs.renameSync(builtHtmlPath, finalHtmlPath);
62
- }
38
+ // --- Client build ---
39
+ await buildClient({
40
+ projectDir,
41
+ clientDir,
42
+ title,
43
+ integrations,
44
+ prefetchStrategy,
45
+ publicDir,
46
+ shared,
47
+ });
63
48
  // --- Server build ---
64
- console.log('[LumenJS] Building server bundle...');
65
- // Collect server entry points (pages with loaders + layouts with loaders + API routes)
66
- const serverEntries = {};
67
- for (const entry of pageEntries) {
68
- if (entry.hasLoader || entry.hasSubscribe) {
69
- serverEntries[`pages/${entry.name}`] = entry.filePath;
70
- }
71
- }
72
- for (const entry of layoutEntries) {
73
- if (entry.hasLoader || entry.hasSubscribe) {
74
- const entryName = entry.dir ? `layouts/${entry.dir}/_layout` : 'layouts/_layout';
75
- serverEntries[entryName] = entry.filePath;
76
- }
77
- }
78
- for (const entry of apiEntries) {
79
- serverEntries[`api/${entry.name}`] = entry.filePath;
80
- }
81
- // Create SSR runtime entry — bundles @lit-labs/ssr alongside Lit so all
82
- // server modules share one Lit instance (avoids _$EM mismatches).
83
- const ssrEntryPath = path.join(projectDir, '__nk_ssr_entry.js');
84
- const hasPageLoaders = pageEntries.some(e => e.hasLoader);
85
- const hasLayoutLoaders = layoutEntries.some(e => e.hasLoader);
86
- if (hasPageLoaders || hasLayoutLoaders) {
87
- fs.writeFileSync(ssrEntryPath, [
88
- "import '@lit-labs/ssr/lib/install-global-dom-shim.js';",
89
- "export { render } from '@lit-labs/ssr';",
90
- "export { html, unsafeStatic } from 'lit/static-html.js';",
91
- ].join('\n'));
92
- serverEntries['ssr-runtime'] = ssrEntryPath;
93
- }
94
- try {
95
- if (Object.keys(serverEntries).length > 0) {
96
- await viteBuild({
97
- root: projectDir,
98
- resolve: shared.resolve,
99
- plugins: shared.plugins,
100
- esbuild: shared.esbuild,
101
- build: {
102
- outDir: serverDir,
103
- emptyOutDir: true,
104
- ssr: true,
105
- rollupOptions: {
106
- input: serverEntries,
107
- output: {
108
- format: 'esm',
109
- entryFileNames: '[name].js',
110
- chunkFileNames: 'assets/[name]-[hash].js',
111
- manualChunks(id) {
112
- // Force all Lit packages into a single shared chunk so SSR runtime
113
- // and page modules use the exact same Lit class instances.
114
- if (id.includes('/node_modules/lit/') ||
115
- id.includes('/node_modules/lit-html/') ||
116
- id.includes('/node_modules/lit-element/') ||
117
- id.includes('/node_modules/@lit/reactive-element/')) {
118
- return 'lit-shared';
119
- }
120
- },
121
- },
122
- external: [
123
- /^node:/,
124
- 'os', 'fs', 'path', 'url', 'util', 'crypto', 'http', 'https', 'net',
125
- 'stream', 'zlib', 'events', 'buffer', 'querystring', 'child_process',
126
- 'worker_threads', 'cluster', 'dns', 'tls', 'assert', 'constants',
127
- 'better-sqlite3',
128
- ],
129
- },
130
- },
131
- logLevel: 'warn',
132
- ssr: {
133
- noExternal: true,
134
- },
135
- });
136
- }
137
- else {
138
- fs.mkdirSync(serverDir, { recursive: true });
139
- }
140
- }
141
- finally {
142
- if (fs.existsSync(ssrEntryPath)) {
143
- fs.unlinkSync(ssrEntryPath);
144
- }
145
- }
49
+ await buildServer({
50
+ projectDir,
51
+ serverDir,
52
+ pageEntries,
53
+ layoutEntries,
54
+ apiEntries,
55
+ hasAuthConfig,
56
+ authConfigPath,
57
+ shared,
58
+ });
146
59
  // --- Copy locales ---
147
60
  if (i18nConfig) {
148
61
  const localesDir = path.join(projectDir, 'locales');
@@ -160,15 +73,19 @@ export async function buildProject(options) {
160
73
  // --- Write manifest ---
161
74
  const manifest = {
162
75
  routes: pageEntries.map(e => {
163
- const routeLayouts = getLayoutDirsForPage(e.filePath, pagesDir, layoutEntries);
76
+ const routeLayouts = e.hasStandalone ? [] : getLayoutDirsForPage(e.filePath, pagesDir, layoutEntries);
164
77
  const relPath = path.relative(pagesDir, e.filePath).replace(/\\/g, '/');
165
78
  return {
166
79
  path: e.routePath,
167
- module: (e.hasLoader || e.hasSubscribe) ? `pages/${e.name}.js` : '',
80
+ module: (e.hasLoader || e.hasSubscribe || e.prerender) ? `pages/${e.name}.js` : '',
168
81
  hasLoader: e.hasLoader,
169
82
  hasSubscribe: e.hasSubscribe,
170
83
  tagName: filePathToTagName(relPath),
171
84
  ...(routeLayouts.length > 0 ? { layouts: routeLayouts } : {}),
85
+ ...(e.hasAuth ? { hasAuth: true } : {}),
86
+ ...(e.hasMeta ? { hasMeta: true } : {}),
87
+ ...(e.hasStandalone ? { hasStandalone: true } : {}),
88
+ ...(e.prerender ? { prerender: true } : {}),
172
89
  };
173
90
  }),
174
91
  apiRoutes: apiEntries.map(e => ({
@@ -184,8 +101,19 @@ export async function buildProject(options) {
184
101
  hasSubscribe: e.hasSubscribe,
185
102
  })),
186
103
  ...(i18nConfig ? { i18n: i18nConfig } : {}),
104
+ ...(hasAuthConfig ? { auth: { configModule: 'auth-config.js' } } : {}),
105
+ prefetch: prefetchStrategy,
187
106
  };
188
107
  fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
108
+ // --- Pre-render phase ---
109
+ await prerenderPages({
110
+ serverDir,
111
+ clientDir,
112
+ pagesDir,
113
+ pageEntries,
114
+ layoutEntries,
115
+ manifest,
116
+ });
189
117
  console.log('[LumenJS] Build complete.');
190
118
  console.log(` Output: ${outDir}`);
191
119
  console.log(` Client assets: ${clientDir}`);
@@ -4,6 +4,10 @@ export interface PageEntry {
4
4
  routePath: string;
5
5
  hasLoader: boolean;
6
6
  hasSubscribe: boolean;
7
+ hasAuth: boolean;
8
+ hasMeta: boolean;
9
+ hasStandalone: boolean;
10
+ prerender: boolean;
7
11
  }
8
12
  export interface LayoutEntry {
9
13
  dir: string;
@@ -21,3 +25,16 @@ export declare function scanLayouts(pagesDir: string): LayoutEntry[];
21
25
  export declare function scanApiRoutes(apiDir: string): ApiEntry[];
22
26
  /** Get the layout directory chain for a given page file */
23
27
  export declare function getLayoutDirsForPage(pageFilePath: string, pagesDir: string, layouts: LayoutEntry[]): string[];
28
+ export interface MiddlewareEntry {
29
+ dir: string;
30
+ filePath: string;
31
+ }
32
+ /**
33
+ * Scan for _middleware.ts files in the pages directory tree.
34
+ */
35
+ export declare function scanMiddleware(pagesDir: string): MiddlewareEntry[];
36
+ /**
37
+ * Get middleware directories that match a given URL pathname.
38
+ * Returns matching middleware entries from root → deepest.
39
+ */
40
+ export declare function getMiddlewareDirsForPathname(pathname: string, entries: MiddlewareEntry[]): MiddlewareEntry[];
@@ -1,6 +1,32 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { fileHasLoader, fileHasSubscribe, filePathToRoute } from '../shared/utils.js';
3
+ import { filePathToRoute, fileHasLoader, fileHasSubscribe } from '../shared/utils.js';
4
+ /** Read a page file once and check all flags from the same content. */
5
+ function analyzePageFile(filePath) {
6
+ try {
7
+ const content = fs.readFileSync(filePath, 'utf-8');
8
+ const classStart = content.search(/export\s+class\s+\w+/);
9
+ const hasExportBefore = (regex) => {
10
+ const match = regex.exec(content);
11
+ if (!match)
12
+ return false;
13
+ if (classStart >= 0 && match.index > classStart)
14
+ return false;
15
+ return true;
16
+ };
17
+ return {
18
+ hasLoader: hasExportBefore(/export\s+(async\s+)?function\s+loader\s*\(/),
19
+ hasSubscribe: hasExportBefore(/export\s+(async\s+)?function\s+subscribe\s*\(/),
20
+ hasAuth: hasExportBefore(/export\s+const\s+auth\s*=/),
21
+ hasMeta: hasExportBefore(/export\s+(const\s+meta\s*=|(async\s+)?function\s+meta\s*\()/),
22
+ hasStandalone: hasExportBefore(/export\s+const\s+standalone\s*=/),
23
+ prerender: /export\s+const\s+prerender\s*=\s*true/.test(content),
24
+ };
25
+ }
26
+ catch {
27
+ return { hasLoader: false, hasSubscribe: false, hasAuth: false, hasMeta: false, hasStandalone: false, prerender: false };
28
+ }
29
+ }
4
30
  export function scanPages(pagesDir) {
5
31
  if (!fs.existsSync(pagesDir))
6
32
  return [];
@@ -44,18 +70,23 @@ export function getLayoutDirsForPage(pageFilePath, pagesDir, layouts) {
44
70
  function walkDir(baseDir, relativePath, entries, pagesDir) {
45
71
  const fullDir = path.join(baseDir, relativePath);
46
72
  const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
73
+ // Check if this subdirectory contains an index file (folder route)
74
+ // Only applies to subdirectories, not the root pages directory
75
+ const hasIndex = relativePath !== '' && dirEntries.some(e => e.isFile() && /^index\.(ts|js)$/.test(e.name));
47
76
  for (const entry of dirEntries) {
48
77
  const entryRelative = path.join(relativePath, entry.name);
49
- if (entry.isDirectory()) {
78
+ if (entry.isDirectory() && !entry.name.startsWith('_')) {
50
79
  walkDir(baseDir, entryRelative, entries, pagesDir);
51
80
  }
52
81
  else if (entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.startsWith('_')) {
82
+ // In a folder route (has index file), only register the index file and dynamic param files
83
+ if (hasIndex && !/^index\.(ts|js)$/.test(entry.name) && !entry.name.startsWith('['))
84
+ continue;
53
85
  const filePath = path.join(pagesDir, entryRelative);
54
86
  const name = entryRelative.replace(/\.(ts|js)$/, '').replace(/\\/g, '/');
55
87
  const routePath = filePathToRoute(entryRelative);
56
- const hasLoader = fileHasLoader(filePath);
57
- const hasSubscribe = fileHasSubscribe(filePath);
58
- entries.push({ name, filePath, routePath, hasLoader, hasSubscribe });
88
+ const flags = analyzePageFile(filePath);
89
+ entries.push({ name, filePath, routePath, ...flags });
59
90
  }
60
91
  }
61
92
  }
@@ -68,11 +99,50 @@ function walkForLayouts(baseDir, relativePath, entries) {
68
99
  const dir = relativePath.replace(/\\/g, '/');
69
100
  entries.push({ dir, filePath, hasLoader: fileHasLoader(filePath), hasSubscribe: fileHasSubscribe(filePath) });
70
101
  }
71
- if (entry.isDirectory()) {
102
+ if (entry.isDirectory() && !entry.name.startsWith('_')) {
72
103
  walkForLayouts(baseDir, path.join(relativePath, entry.name), entries);
73
104
  }
74
105
  }
75
106
  }
107
+ /**
108
+ * Scan for _middleware.ts files in the pages directory tree.
109
+ */
110
+ export function scanMiddleware(pagesDir) {
111
+ if (!fs.existsSync(pagesDir))
112
+ return [];
113
+ const entries = [];
114
+ walkForMiddleware(pagesDir, '', entries);
115
+ return entries;
116
+ }
117
+ /**
118
+ * Get middleware directories that match a given URL pathname.
119
+ * Returns matching middleware entries from root → deepest.
120
+ */
121
+ export function getMiddlewareDirsForPathname(pathname, entries) {
122
+ const urlSegments = pathname.replace(/^\//, '').split('/').filter(Boolean);
123
+ return entries.filter(entry => {
124
+ if (entry.dir === '')
125
+ return true; // Root middleware applies to all routes
126
+ const dirSegments = entry.dir.split('/').filter(Boolean);
127
+ if (dirSegments.length > urlSegments.length)
128
+ return false;
129
+ return dirSegments.every((seg, i) => seg === urlSegments[i]);
130
+ }).sort((a, b) => a.dir.split('/').length - b.dir.split('/').length);
131
+ }
132
+ function walkForMiddleware(baseDir, relativePath, entries) {
133
+ const fullDir = path.join(baseDir, relativePath);
134
+ const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
135
+ for (const entry of dirEntries) {
136
+ if (entry.isFile() && /^_middleware\.(ts|js)$/.test(entry.name)) {
137
+ const filePath = path.join(fullDir, entry.name);
138
+ const dir = relativePath.replace(/\\/g, '/');
139
+ entries.push({ dir, filePath });
140
+ }
141
+ if (entry.isDirectory() && !entry.name.startsWith('_')) {
142
+ walkForMiddleware(baseDir, path.join(relativePath, entry.name), entries);
143
+ }
144
+ }
145
+ }
76
146
  function walkApiDir(baseDir, relativePath, entries, apiDir) {
77
147
  const fullDir = path.join(baseDir, relativePath);
78
148
  const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { readBody } from '../shared/utils.js';
4
4
  import { matchRoute } from '../shared/route-matching.js';
5
+ import { useStorage } from '../storage/index.js';
5
6
  export async function handleApiRoute(manifest, serverDir, pathname, queryString, method, req, res) {
6
7
  const matched = matchRoute(manifest.apiRoutes, pathname);
7
8
  if (!matched) {
@@ -9,7 +10,9 @@ export async function handleApiRoute(manifest, serverDir, pathname, queryString,
9
10
  res.end(JSON.stringify({ error: 'API route not found' }));
10
11
  return;
11
12
  }
12
- const modulePath = path.join(serverDir, matched.route.module);
13
+ // Vite replaces [param] with _param_ in output filenames
14
+ const resolvedModule = matched.route.module.replace(/\[([^\]]+)\]/g, '_$1_');
15
+ const modulePath = path.join(serverDir, resolvedModule);
13
16
  if (!fs.existsSync(modulePath)) {
14
17
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
15
18
  res.end(JSON.stringify({ error: 'API route module not found' }));
@@ -43,13 +46,16 @@ export async function handleApiRoute(manifest, serverDir, pathname, queryString,
43
46
  params: matched.params,
44
47
  body,
45
48
  headers: req.headers,
49
+ storage: useStorage(),
50
+ nkAuth: req.nkAuth,
46
51
  });
47
52
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
48
53
  res.end(JSON.stringify(result));
49
54
  }
50
55
  catch (err) {
51
56
  const status = err?.status || 500;
52
- const message = err?.message || 'Internal server error';
57
+ // Only expose error messages for client errors (4xx); hide internals for 5xx
58
+ const message = status < 500 ? (err?.message || 'Request error') : 'Internal server error';
53
59
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
54
60
  res.end(JSON.stringify({ error: message }));
55
61
  }