aplosjs 0.15.0

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 (56) hide show
  1. package/README.md +28 -0
  2. package/aplos.config.dist.js +30 -0
  3. package/bin/aplos +60 -0
  4. package/create-aplos/index.js +95 -0
  5. package/create-aplos/package.json +29 -0
  6. package/create-aplos/templates/minimal/README.md +38 -0
  7. package/create-aplos/templates/minimal/_gitignore +7 -0
  8. package/create-aplos/templates/minimal/aplos.config.js +13 -0
  9. package/create-aplos/templates/minimal/package.json +22 -0
  10. package/create-aplos/templates/minimal/public/favicon.svg +4 -0
  11. package/create-aplos/templates/minimal/src/pages/_app.tsx +6 -0
  12. package/create-aplos/templates/minimal/src/pages/about.tsx +24 -0
  13. package/create-aplos/templates/minimal/src/pages/index.tsx +40 -0
  14. package/create-aplos/templates/minimal/src/styles/global.css +53 -0
  15. package/create-aplos/templates/minimal/tsconfig.json +18 -0
  16. package/package.json +92 -0
  17. package/postcss.config.js +9 -0
  18. package/rspack.config.js +306 -0
  19. package/rspack.ssr.config.js +129 -0
  20. package/src/build/config.js +42 -0
  21. package/src/build/css-noop-loader.cjs +3 -0
  22. package/src/build/router.js +609 -0
  23. package/src/build/ssg.js +198 -0
  24. package/src/client/public/index.html +8 -0
  25. package/src/command/build.js +105 -0
  26. package/src/command/create.js +91 -0
  27. package/src/command/devServer.js +198 -0
  28. package/src/command/router.js +137 -0
  29. package/src/components/head.jsx +65 -0
  30. package/src/components/navigation.jsx +11 -0
  31. package/src/config.js +5 -0
  32. package/src/pages/_app.tsx +9 -0
  33. package/src/pages/blog/[slug].tsx +6 -0
  34. package/src/pages/crash.tsx +6 -0
  35. package/src/pages/index.tsx +10 -0
  36. package/src/pages/test.tsx +5 -0
  37. package/src/runtime/DefaultErrorPage.jsx +76 -0
  38. package/src/runtime/ErrorBoundary.jsx +40 -0
  39. package/src/runtime/MiddlewareGate.jsx +149 -0
  40. package/src/runtime/app-ssr.jsx +42 -0
  41. package/src/runtime/app.jsx +126 -0
  42. package/src/runtime/default-middleware.js +10 -0
  43. package/src/runtime/default-not-found.jsx +3 -0
  44. package/src/runtime/passthrough-layout.jsx +5 -0
  45. package/src/runtime/redirect.js +46 -0
  46. package/src/runtime/ssr-entry.jsx +104 -0
  47. package/templates/minimal/README.md +38 -0
  48. package/templates/minimal/_gitignore +7 -0
  49. package/templates/minimal/aplos.config.js +13 -0
  50. package/templates/minimal/package.json +22 -0
  51. package/templates/minimal/public/favicon.svg +4 -0
  52. package/templates/minimal/src/pages/_app.tsx +6 -0
  53. package/templates/minimal/src/pages/about.tsx +24 -0
  54. package/templates/minimal/src/pages/index.tsx +40 -0
  55. package/templates/minimal/src/styles/global.css +53 -0
  56. package/templates/minimal/tsconfig.json +18 -0
@@ -0,0 +1,609 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export async function buildRouter(aplos) {
10
+ const appExtensions = ['.tsx', '.jsx', '.js'];
11
+
12
+ let projectDirectory = process.cwd();
13
+ const pageDirectory = path.join(projectDirectory, 'src', 'pages');
14
+
15
+ const pages = [];
16
+ const capitalize = s => s && s[0].toUpperCase() + s.slice(1)
17
+
18
+ if (!fs.existsSync(pageDirectory)) {
19
+ console.warn("No pages directory found");
20
+ process.exit(0);
21
+ return;
22
+ }
23
+
24
+ const filenames = await getFiles(pageDirectory, appExtensions);
25
+ if (filenames.length === 0) {
26
+ console.warn('No page files found in:', pageDirectory);
27
+ }
28
+
29
+ // Build layout tree for nested layouts
30
+ const layoutTree = buildLayoutTree(pageDirectory, appExtensions);
31
+
32
+ const routes = aplos.routes || [];
33
+
34
+ const generateComponentName = (nameParts, fileName) => {
35
+ const parts = nameParts.filter(part => part && part !== fileName);
36
+
37
+ const processedParts = parts.map(part => {
38
+ if (part.startsWith('[')) {
39
+ const parentDir = parts[parts.indexOf(part) - 1];
40
+ return parentDir ?
41
+ capitalize(formatPath(parentDir)) + capitalize(formatPath(part)) :
42
+ capitalize(formatPath(part));
43
+ }
44
+ return capitalize(formatPath(part));
45
+ });
46
+
47
+ processedParts.push(capitalize(formatPath(fileName)));
48
+
49
+ return processedParts.join('');
50
+ }
51
+
52
+ filenames.forEach(file => {
53
+ let name = file.replace('~', '').replace(/\.(js|tsx|jsx)$/, '');
54
+ let nameParts = name.split('/');
55
+ let fileName = nameParts.pop();
56
+
57
+ let capitalizeName = generateComponentName(nameParts, fileName);
58
+
59
+ let path;
60
+ if (fileName === 'index') {
61
+ path = nameParts.join('/') || '/';
62
+ } else {
63
+ path = name;
64
+ }
65
+
66
+ let found = routes.find(element => element.source === path);
67
+ if (found && found.destination) {
68
+ path = found.destination;
69
+ }
70
+
71
+ path = path.replace(/\[\.\.\..*?]/g, '*');
72
+ path = path.replace(/\[(.*?)]/g, ':$1');
73
+
74
+ const absolutePageFile = `${pageDirectory}${file.replace('~', '')}`;
75
+ const staticDirective = hasUseStaticDirective(absolutePageFile);
76
+
77
+ let existingPage = pages.find(p => p.path === path);
78
+ if (!existingPage) {
79
+ const config = {
80
+ "path": path,
81
+ "component": capitalizeName,
82
+ "file": file.replaceAll('//', '/'),
83
+ "requirement": {},
84
+ "static": staticDirective
85
+ };
86
+
87
+ routes.push(config);
88
+ pages.push(config);
89
+ }
90
+ });
91
+
92
+ // Expand `paths` declared in route entries into concrete static pages that
93
+ // share the component of their matching catch-all. Lets the SSG pre-render
94
+ // each URL without asking the user to split the catch-all into many files.
95
+ const configuredRoutes = aplos.routes || [];
96
+ for (const entry of configuredRoutes) {
97
+ if (!entry || !entry.paths || !entry.source) {
98
+ continue;
99
+ }
100
+ const catchAllPath = entry.source.replace(/\[\.\.\..*?]/g, '*').replace(/\[(.*?)]/g, ':$1');
101
+ const catchAll = pages.find(p => p.path === catchAllPath);
102
+ if (!catchAll) {
103
+ console.warn(`Route config: no page matches source "${entry.source}" — skipping paths expansion.`);
104
+ continue;
105
+ }
106
+ let paths = entry.paths;
107
+ if (typeof paths === 'function') {
108
+ try {
109
+ paths = paths();
110
+ } catch (error) {
111
+ console.error(`Route config: paths() threw for source "${entry.source}":`, error.message);
112
+ continue;
113
+ }
114
+ }
115
+ if (!Array.isArray(paths)) {
116
+ console.warn(`Route config: paths for "${entry.source}" must be an array or a function returning one.`);
117
+ continue;
118
+ }
119
+ for (const rawEntry of paths) {
120
+ let concretePath;
121
+ let inlineMeta = null;
122
+ if (typeof rawEntry === 'string') {
123
+ concretePath = rawEntry;
124
+ } else if (rawEntry && typeof rawEntry === 'object' && typeof rawEntry.path === 'string') {
125
+ concretePath = rawEntry.path;
126
+ if (rawEntry.meta && typeof rawEntry.meta === 'object') {
127
+ inlineMeta = rawEntry.meta;
128
+ }
129
+ } else {
130
+ continue;
131
+ }
132
+ if (!concretePath.startsWith('/')) {
133
+ continue;
134
+ }
135
+ if (pages.find(p => p.path === concretePath)) {
136
+ continue;
137
+ }
138
+ pages.push({
139
+ path: concretePath,
140
+ component: catchAll.component,
141
+ file: catchAll.file,
142
+ requirement: {},
143
+ static: true,
144
+ sourcePath: entry.source,
145
+ inlineMeta,
146
+ });
147
+ }
148
+ }
149
+
150
+ // Detect _app, _404, _error files
151
+ const appFile = findSpecialFile(pageDirectory, '_app', appExtensions);
152
+ const notFoundFile = findSpecialFile(pageDirectory, '_404', appExtensions);
153
+ const errorFile = findSpecialFile(pageDirectory, '_error', appExtensions);
154
+
155
+ // Detect the optional route middleware at src/middleware.{ts,tsx,js,jsx}.
156
+ // Lives next to src/pages (project-level), not inside it, so it never
157
+ // becomes a route. Resolved at runtime via the @aplos_middleware alias.
158
+ const srcDirectory = path.join(projectDirectory, 'src');
159
+ const middlewareFile = findSpecialFile(srcDirectory, 'middleware', appExtensions);
160
+
161
+ // Generate pages.js (re-exports for all pages + special files). Pages
162
+ // expanded from `routes[].paths` reuse the same component as their
163
+ // catch-all parent, so we dedupe exports by component name here.
164
+ const pagesExports = [];
165
+ const exportedComponents = new Set();
166
+ pages.forEach(page => {
167
+ if (exportedComponents.has(page.component)) {
168
+ return;
169
+ }
170
+ exportedComponents.add(page.component);
171
+ const componentFileName = page.file.replace('~', '');
172
+ pagesExports.push(`export { default as ${page.component} } from "${projectDirectory}/src/pages${componentFileName}";`);
173
+ // Re-export the optional `meta` named export under a deterministic alias.
174
+ // Only emit the namespace import when the source file actually exports
175
+ // `meta` — otherwise rspack/webpack fires an ESModulesLinkingWarning
176
+ // per page that doesn't opt in, drowning real warnings on large sites.
177
+ const absolutePageFile = `${pageDirectory}${componentFileName}`;
178
+ if (pageHasMetaExport(absolutePageFile)) {
179
+ pagesExports.push(`import * as ${page.component}__module from "${projectDirectory}/src/pages${componentFileName}";`);
180
+ pagesExports.push(`export const ${page.component}__meta = ${page.component}__module.meta || null;`);
181
+ } else {
182
+ pagesExports.push(`export const ${page.component}__meta = null;`);
183
+ }
184
+ });
185
+
186
+ // Layout exports
187
+ layoutTree.forEach(layout => {
188
+ pagesExports.push(`export { default as ${layout.component} } from "${projectDirectory}/src/pages/${layout.file}";`);
189
+ pagesExports.push(`export const ${layout.component}__meta = null;`);
190
+ });
191
+
192
+ // AppLayout
193
+ if (appFile) {
194
+ pagesExports.push(`export { default as AppLayout } from "${projectDirectory}/src/pages/${appFile}";`);
195
+ } else {
196
+ pagesExports.push(`export { default as AppLayout } from "aplos/internal/passthrough-layout";`);
197
+ }
198
+ pagesExports.push(`export const AppLayout__meta = null;`);
199
+
200
+ // NoMatch (404)
201
+ if (notFoundFile) {
202
+ pagesExports.push(`export { default as NoMatch } from "${projectDirectory}/src/pages/${notFoundFile}";`);
203
+ } else {
204
+ pagesExports.push(`export { default as NoMatch } from "aplos/internal/default-not-found";`);
205
+ }
206
+ pagesExports.push(`export const NoMatch__meta = null;`);
207
+
208
+ // CustomError (optional)
209
+ if (errorFile) {
210
+ pagesExports.push(`export { default as CustomError } from "${projectDirectory}/src/pages/${errorFile}";`);
211
+ } else {
212
+ pagesExports.push(`export const CustomError = null;`);
213
+ }
214
+
215
+ // Build nested route tree as JS data
216
+ const nestedRoutes = buildNestedRoutes(pages, layoutTree);
217
+
218
+ // Generate routes.js
219
+ const routesFileContent = generateRoutesFile(nestedRoutes);
220
+
221
+ // Generate head.js
222
+ const headFileContent = generateHeadFile(aplos.head || {}, aplos.reactStrictMode);
223
+
224
+ // Generate middleware.js — re-export the project middleware as default, or
225
+ // fall back to the framework no-op so the runtime import always resolves.
226
+ const middlewareFileContent = middlewareFile
227
+ ? `export { default } from "${projectDirectory}/src/${middlewareFile}";\n`
228
+ : `export { default } from "aplos/internal/default-middleware";\n`;
229
+
230
+ // Write each cache file only when its content changed (writeIfChanged)
231
+ // so an unchanged router cache keeps a stable mtime and doesn't trigger
232
+ // a full reload.
233
+ try {
234
+ const cacheDir = path.join(projectDirectory, '.aplos', 'cache');
235
+ await fs.promises.mkdir(cacheDir, { recursive: true });
236
+
237
+ await writeIfChanged(
238
+ path.join(cacheDir, 'router.js'),
239
+ JSON.stringify(routes)
240
+ );
241
+ await writeIfChanged(
242
+ path.join(cacheDir, 'pages.js'),
243
+ pagesExports.join('\n') + '\n'
244
+ );
245
+ await writeIfChanged(
246
+ path.join(cacheDir, 'routes.js'),
247
+ routesFileContent
248
+ );
249
+ await writeIfChanged(
250
+ path.join(cacheDir, 'head.js'),
251
+ headFileContent
252
+ );
253
+ await writeIfChanged(
254
+ path.join(cacheDir, 'middleware.js'),
255
+ middlewareFileContent
256
+ );
257
+ await writeIfChanged(
258
+ path.join(cacheDir, 'config.js'),
259
+ `export default ${JSON.stringify(aplos, null, 2)};`
260
+ );
261
+ } catch (error) {
262
+ console.error('Failed to write cache files:', error.message);
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Write `content` to `filePath` only when it differs from disk, keeping the
269
+ * mtime stable when nothing changed.
270
+ *
271
+ * @param {string} filePath
272
+ * @param {string} content
273
+ * @returns {Promise<void>}
274
+ */
275
+ async function writeIfChanged(filePath, content) {
276
+ try {
277
+ const existing = await fs.promises.readFile(filePath, 'utf-8');
278
+ if (existing === content) {
279
+ return;
280
+ }
281
+ } catch (error) {
282
+ if (error.code !== 'ENOENT') {
283
+ throw error;
284
+ }
285
+ }
286
+ await fs.promises.writeFile(filePath, content);
287
+ }
288
+
289
+ /**
290
+ * Find a special file (_app, _404, _error) in the pages directory
291
+ */
292
+ function findSpecialFile(pageDirectory, baseName, extensions) {
293
+ return extensions
294
+ .map(ext => `${baseName}${ext}`)
295
+ .find(file => {
296
+ try {
297
+ return fs.existsSync(path.join(pageDirectory, file));
298
+ } catch (error) {
299
+ return false;
300
+ }
301
+ }) || null;
302
+ }
303
+
304
+ /**
305
+ * Generate routes.js content — pure JS, no JSX
306
+ */
307
+ function generateRoutesFile(routeTree) {
308
+ const names = collectComponentNames(routeTree);
309
+ const metaNames = names.map(n => `${n}__meta`);
310
+ const lines = [];
311
+ lines.push(`import { ${[...names, ...metaNames].join(', ')} } from './pages.js';`);
312
+ lines.push('');
313
+ lines.push(`export const routeTree = ${serializeRouteTree(routeTree)};`);
314
+ return lines.join('\n') + '\n';
315
+ }
316
+
317
+ /**
318
+ * Collect all component names referenced in the route tree
319
+ */
320
+ function collectComponentNames(nodes) {
321
+ const names = new Set();
322
+ function walk(nodes) {
323
+ for (const node of nodes) {
324
+ if (node.component) names.add(node.component);
325
+ if (node.children) walk(node.children);
326
+ }
327
+ }
328
+ walk(nodes);
329
+ return Array.from(names);
330
+ }
331
+
332
+ /**
333
+ * Serialize route tree to JS source (references to imported components, not strings)
334
+ */
335
+ function serializeRouteTree(nodes, indent = '') {
336
+ const inner = indent + ' ';
337
+ const items = nodes.map(node => {
338
+ const parts = [];
339
+ if (node.component) {
340
+ parts.push(`${inner}element: ${node.component}`);
341
+ if (node.inlineMeta) {
342
+ parts.push(`${inner}meta: ${JSON.stringify(node.inlineMeta)}`);
343
+ } else {
344
+ parts.push(`${inner}meta: ${node.component}__meta`);
345
+ }
346
+ }
347
+ if (node.path !== undefined) parts.push(`${inner}path: ${JSON.stringify(node.path)}`);
348
+ if (node.sourcePath) parts.push(`${inner}sourcePath: ${JSON.stringify(node.sourcePath)}`);
349
+ if (node.static === true) parts.push(`${inner}static: true`);
350
+ if (node.children) {
351
+ parts.push(`${inner}children: ${serializeRouteTree(node.children, inner)}`);
352
+ }
353
+ return `${indent}{\n${parts.join(',\n')}\n${indent}}`;
354
+ });
355
+ return `[\n${items.join(',\n')}\n${indent}]`;
356
+ }
357
+
358
+ /**
359
+ * Generate head.js content — pure JS
360
+ */
361
+ function generateHeadFile(head, reactStrictMode) {
362
+ const { defaultTitle, titleTemplate, meta = [], link = [], script = [] } = head;
363
+ const headObj = {};
364
+ if (defaultTitle) headObj.defaultTitle = defaultTitle;
365
+ if (titleTemplate) headObj.titleTemplate = titleTemplate;
366
+ if (meta.length > 0) headObj.meta = meta;
367
+ if (link.length > 0) headObj.link = link;
368
+ if (script.length > 0) headObj.script = script;
369
+
370
+ const lines = [];
371
+ lines.push(`export default ${JSON.stringify(headObj, null, 2)};`);
372
+ lines.push(`export const reactStrictMode = ${!!reactStrictMode};`);
373
+ return lines.join('\n') + '\n';
374
+ }
375
+
376
+ /**
377
+ * Detect whether a page module exports a `meta` named binding. Used by the
378
+ * router generator to decide whether to emit the `import * as ...__module`
379
+ * re-export pair (rspack warns on namespace access for missing named exports).
380
+ *
381
+ * We use a deliberately permissive regex: false positives only emit the same
382
+ * code we used to emit unconditionally — no harm done. The patterns covered:
383
+ * export const meta
384
+ * export let meta
385
+ * export var meta
386
+ * export function meta
387
+ * export async function meta
388
+ * export { meta } (also { meta as ... } / { foo as meta })
389
+ *
390
+ * Exports inside line/block comments are filtered out before scanning so
391
+ * commented-out examples in the source don't trigger a match.
392
+ *
393
+ * @param {string} filePath
394
+ * @returns {boolean}
395
+ */
396
+ function pageHasMetaExport(filePath) {
397
+ let source;
398
+ try {
399
+ source = fs.readFileSync(filePath, 'utf-8');
400
+ } catch (error) {
401
+ return false;
402
+ }
403
+ // Strip block and line comments to avoid matching commented-out exports.
404
+ const stripped = source
405
+ .replace(/\/\*[\s\S]*?\*\//g, '')
406
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
407
+ if (/export\s+(?:const|let|var|function|async\s+function)\s+meta\b/.test(stripped)) {
408
+ return true;
409
+ }
410
+ if (/export\s*\{[^}]*\bmeta\b[^}]*\}/.test(stripped)) {
411
+ return true;
412
+ }
413
+ return false;
414
+ }
415
+
416
+ /**
417
+ * Detect a leading `'use static'` / `"use static"` directive in a page file.
418
+ * Scans the first non-empty, non-comment lines before any code/import.
419
+ * @param {string} filePath
420
+ * @returns {boolean}
421
+ */
422
+ function hasUseStaticDirective(filePath) {
423
+ try {
424
+ const source = fs.readFileSync(filePath, 'utf-8');
425
+ const lines = source.split('\n');
426
+ for (const rawLine of lines) {
427
+ const line = rawLine.trim();
428
+ if (line === '') {
429
+ continue;
430
+ }
431
+ if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
432
+ continue;
433
+ }
434
+ if (line === `'use static';` || line === `"use static";` || line === `'use static'` || line === `"use static"`) {
435
+ return true;
436
+ }
437
+ return false;
438
+ }
439
+ } catch (error) {
440
+ return false;
441
+ }
442
+ return false;
443
+ }
444
+
445
+ /**
446
+ *
447
+ * @param {string} dirPath
448
+ * @param {string[]} extensions
449
+ * @returns {Promise<string[]>}
450
+ */
451
+ export async function getFiles(dirPath, extensions) {
452
+ const patterns = extensions.map(ext => `**/*${ext}`);
453
+ const globPattern = patterns.length === 1 ? patterns[0] : `{${patterns.join(',')}}`;
454
+
455
+ try {
456
+ const files = await glob(globPattern, {
457
+ cwd: dirPath,
458
+ ignore: ['**/_*'] // ignore files starting with _
459
+ });
460
+
461
+ return files.map(file => '/' + file);
462
+ } catch (error) {
463
+ console.error(`Error globbing files in ${dirPath}:`, error);
464
+ return [];
465
+ }
466
+ }
467
+
468
+ /**
469
+ *
470
+ * @param {string} path
471
+ * @returns {string}
472
+ */
473
+ export function formatPath(path) {
474
+ return path.replace(/\.\.\./g, '').replace(/[\[\]_-]/g, '');
475
+ }
476
+
477
+ /**
478
+ * Build nested layout tree by scanning for _layout files
479
+ * @param {string} pageDirectory
480
+ * @param {string[]} extensions
481
+ * @returns {Map<string, object>}
482
+ */
483
+ function buildLayoutTree(pageDirectory, extensions) {
484
+ const layouts = new Map();
485
+
486
+ function generateLayoutName(pathPrefix) {
487
+ if (!pathPrefix || pathPrefix === '/') return 'RootLayout';
488
+ return pathPrefix.split('/').filter(Boolean).map(part =>
489
+ part.charAt(0).toUpperCase() + formatPath(part.slice(1))
490
+ ).join('') + 'Layout';
491
+ }
492
+
493
+ function scanLayouts(dir, pathPrefix = '') {
494
+ const layoutFile = extensions
495
+ .map(ext => `_layout${ext}`)
496
+ .find(file => {
497
+ try {
498
+ return fs.existsSync(path.join(dir, file));
499
+ } catch (error) {
500
+ return false;
501
+ }
502
+ });
503
+
504
+ if (layoutFile) {
505
+ const layoutPath = pathPrefix ? path.join(pathPrefix, layoutFile) : layoutFile;
506
+ layouts.set(pathPrefix || '/', {
507
+ file: layoutPath,
508
+ component: generateLayoutName(pathPrefix),
509
+ path: pathPrefix || '/'
510
+ });
511
+ }
512
+
513
+ // Scan subdirectories recursively
514
+ try {
515
+ const subdirs = fs.readdirSync(dir, { withFileTypes: true })
516
+ .filter(dirent => dirent.isDirectory() && !dirent.name.startsWith('_'))
517
+ .map(dirent => dirent.name);
518
+
519
+ subdirs.forEach(subdir => {
520
+ const newPathPrefix = pathPrefix ? path.join(pathPrefix, subdir) : subdir;
521
+ scanLayouts(
522
+ path.join(dir, subdir),
523
+ newPathPrefix
524
+ );
525
+ });
526
+ } catch (error) {
527
+ // Ignore directories we can't read
528
+ }
529
+ }
530
+
531
+ scanLayouts(pageDirectory);
532
+ return layouts;
533
+ }
534
+
535
+ /**
536
+ * Build nested route structure with layouts
537
+ * Returns a JS array (data) instead of JSX strings
538
+ * @param {Array} pages
539
+ * @param {Map} layoutTree
540
+ * @returns {Array}
541
+ */
542
+ function buildNestedRoutes(pages, layoutTree) {
543
+ // Group pages by their directory path
544
+ const routesByPath = new Map();
545
+
546
+ pages.forEach(page => {
547
+ const pagePath = page.path;
548
+ const segments = pagePath.split('/').filter(Boolean);
549
+
550
+ // Find the most specific layout for this page
551
+ let layoutPath = '/';
552
+ for (let i = segments.length; i > 0; i--) {
553
+ const testPath = '/' + segments.slice(0, i).join('/');
554
+ if (layoutTree.has(testPath)) {
555
+ layoutPath = testPath;
556
+ break;
557
+ }
558
+ }
559
+
560
+ if (!routesByPath.has(layoutPath)) {
561
+ routesByPath.set(layoutPath, []);
562
+ }
563
+ routesByPath.get(layoutPath).push(page);
564
+ });
565
+
566
+ function buildRouteLevel(currentPath = '/') {
567
+ const nodes = [];
568
+
569
+ // Add pages for this level
570
+ const pagesAtLevel = routesByPath.get(currentPath) || [];
571
+ pagesAtLevel.forEach(page => {
572
+ const node = { path: page.path, component: page.component };
573
+ if (page.static) {
574
+ node.static = true;
575
+ }
576
+ if (page.sourcePath) {
577
+ node.sourcePath = page.sourcePath;
578
+ }
579
+ if (page.inlineMeta) {
580
+ node.inlineMeta = page.inlineMeta;
581
+ }
582
+ nodes.push(node);
583
+ });
584
+
585
+ // Add nested layouts
586
+ Array.from(layoutTree.keys())
587
+ .filter(layoutPath => layoutPath.startsWith(currentPath) && layoutPath !== currentPath)
588
+ .forEach(layoutPath => {
589
+ const layout = layoutTree.get(layoutPath);
590
+ nodes.push({
591
+ component: layout.component,
592
+ children: buildRouteLevel(layoutPath)
593
+ });
594
+ });
595
+
596
+ return nodes;
597
+ }
598
+
599
+ let innerRoutes = buildRouteLevel();
600
+
601
+ // Wrap with root layout if exists
602
+ if (layoutTree.has('/')) {
603
+ const rootLayout = layoutTree.get('/');
604
+ innerRoutes = [{ component: rootLayout.component, children: innerRoutes }];
605
+ }
606
+
607
+ // Wrap everything with AppLayout
608
+ return [{ component: 'AppLayout', children: innerRoutes }];
609
+ }