@zenithbuild/core 0.6.2 → 1.1.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.
- package/cli/commands/dev.ts +107 -48
- package/compiler/discovery/componentDiscovery.ts +75 -11
- package/compiler/output/types.ts +15 -1
- package/compiler/parse/parseTemplate.ts +29 -0
- package/compiler/runtime/dataExposure.ts +27 -12
- package/compiler/runtime/generateDOM.ts +12 -3
- package/compiler/runtime/transformIR.ts +39 -3
- package/compiler/runtime/wrapExpression.ts +32 -13
- package/compiler/runtime/wrapExpressionWithLoop.ts +24 -10
- package/compiler/ssg-build.ts +71 -7
- package/compiler/test/component-stacking.test.ts +365 -0
- package/compiler/transform/componentResolver.ts +42 -4
- package/compiler/transform/fragmentLowering.ts +153 -1
- package/compiler/transform/generateBindings.ts +31 -10
- package/compiler/transform/transformNode.ts +114 -1
- package/core/config/index.ts +5 -3
- package/core/config/types.ts +67 -37
- package/core/plugins/bridge.ts +193 -0
- package/core/plugins/registry.ts +51 -6
- package/dist/cli.js +10 -0
- package/dist/zen-build.js +673 -1723
- package/dist/zen-dev.js +673 -1723
- package/dist/zen-preview.js +673 -1723
- package/dist/zenith.js +673 -1723
- package/package.json +11 -3
- package/runtime/bundle-generator.ts +36 -17
- package/runtime/client-runtime.ts +21 -1
- package/cli/utils/content.ts +0 -112
- package/router/manifest.ts +0 -314
- package/router/navigation/ZenLink.zen +0 -231
- package/router/navigation/index.ts +0 -78
- package/router/navigation/zen-link.ts +0 -584
- package/router/runtime.ts +0 -458
- package/router/types.ts +0 -168
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Core library for the Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,15 @@
|
|
|
47
47
|
"start": "bun run build && bun run dev",
|
|
48
48
|
"build:cli": "bun build bin/zenith.ts bin/zen-dev.ts bin/zen-build.ts bin/zen-preview.ts --outdir dist --target bun --bundle && for f in dist/*.js; do echo '#!/usr/bin/env bun' | cat - \"$f\" > \"$f.tmp\" && mv \"$f.tmp\" \"$f\" && chmod +x \"$f\"; done",
|
|
49
49
|
"format": "prettier --write \"**/*.ts\"",
|
|
50
|
-
"format:check": "prettier --check \"**/*.ts\""
|
|
50
|
+
"format:check": "prettier --check \"**/*.ts\"",
|
|
51
|
+
"release": "bun run scripts/release.ts",
|
|
52
|
+
"release:dry": "bun run scripts/release.ts --dry-run",
|
|
53
|
+
"release:patch": "bun run scripts/release.ts --bump=patch",
|
|
54
|
+
"release:minor": "bun run scripts/release.ts --bump=minor",
|
|
55
|
+
"release:major": "bun run scripts/release.ts --bump=major"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
51
59
|
},
|
|
52
60
|
"private": false,
|
|
53
61
|
"devDependencies": {
|
|
@@ -69,4 +77,4 @@
|
|
|
69
77
|
"parse5": "^8.0.0",
|
|
70
78
|
"picocolors": "^1.1.1"
|
|
71
79
|
}
|
|
72
|
-
}
|
|
80
|
+
}
|
|
@@ -640,7 +640,16 @@ export function generateBundleJS(): string {
|
|
|
640
640
|
function defineSchema(name, schema) { schemaRegistry.set(name, schema); }
|
|
641
641
|
|
|
642
642
|
function zenCollection(collectionName) {
|
|
643
|
-
|
|
643
|
+
// Access plugin data from the neutral envelope
|
|
644
|
+
// Content plugin stores all items under 'content' namespace
|
|
645
|
+
const pluginData = global.__ZENITH_PLUGIN_DATA__ || {};
|
|
646
|
+
const contentItems = pluginData.content || [];
|
|
647
|
+
|
|
648
|
+
// Filter by collection name (plugin owns data structure, runtime just filters)
|
|
649
|
+
const data = Array.isArray(contentItems)
|
|
650
|
+
? contentItems.filter(item => item && item.collection === collectionName)
|
|
651
|
+
: [];
|
|
652
|
+
|
|
644
653
|
return new ZenCollection(data);
|
|
645
654
|
}
|
|
646
655
|
|
|
@@ -880,9 +889,7 @@ export function generateBundleJS(): string {
|
|
|
880
889
|
// SPA Router Runtime
|
|
881
890
|
// ============================================
|
|
882
891
|
|
|
883
|
-
|
|
884
|
-
'use strict';
|
|
885
|
-
|
|
892
|
+
// Router state
|
|
886
893
|
// Current route state
|
|
887
894
|
var currentRoute = {
|
|
888
895
|
path: '/',
|
|
@@ -1096,6 +1103,18 @@ export function generateBundleJS(): string {
|
|
|
1096
1103
|
return;
|
|
1097
1104
|
}
|
|
1098
1105
|
|
|
1106
|
+
// Dev mode: If no route manifest is loaded, use browser navigation
|
|
1107
|
+
// This allows ZenLink to work in dev server where pages are served fresh
|
|
1108
|
+
if (routeManifest.length === 0) {
|
|
1109
|
+
var url = normalizedPath + (Object.keys(query).length ? '?' + new URLSearchParams(query) : '');
|
|
1110
|
+
if (options.replace) {
|
|
1111
|
+
location.replace(url);
|
|
1112
|
+
} else {
|
|
1113
|
+
location.href = url;
|
|
1114
|
+
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1099
1118
|
resolveAndRender(path, query, true, options.replace || false);
|
|
1100
1119
|
}
|
|
1101
1120
|
|
|
@@ -1166,19 +1185,19 @@ export function generateBundleJS(): string {
|
|
|
1166
1185
|
);
|
|
1167
1186
|
}
|
|
1168
1187
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1188
|
+
// Expose router API globally
|
|
1189
|
+
global.__zenith_router = {
|
|
1190
|
+
navigate: navigate,
|
|
1191
|
+
getRoute: getRoute,
|
|
1192
|
+
onRouteChange: onRouteChange,
|
|
1193
|
+
isActive: isActive,
|
|
1194
|
+
prefetch: prefetch,
|
|
1195
|
+
initRouter: initRouter
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// Also expose navigate directly for convenience
|
|
1199
|
+
global.navigate = navigate;
|
|
1200
|
+
|
|
1182
1201
|
|
|
1183
1202
|
// ============================================
|
|
1184
1203
|
// HMR Client (Development Only)
|
|
@@ -485,6 +485,25 @@ export function cleanup(container?: Element | Document): void {
|
|
|
485
485
|
triggerUnmount();
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
// ============================================
|
|
489
|
+
// Plugin Runtime Data Access
|
|
490
|
+
// ============================================
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Access plugin data from the neutral envelope
|
|
494
|
+
*
|
|
495
|
+
* Plugins use this to retrieve their data at runtime.
|
|
496
|
+
* The CLI injected this data via window.__ZENITH_PLUGIN_DATA__
|
|
497
|
+
*
|
|
498
|
+
* @param namespace - Plugin namespace (e.g., 'content', 'router')
|
|
499
|
+
* @returns The plugin's data, or undefined if not present
|
|
500
|
+
*/
|
|
501
|
+
export function getPluginRuntimeData(namespace: string): unknown {
|
|
502
|
+
if (typeof window === 'undefined') return undefined;
|
|
503
|
+
const envelope = (window as any).__ZENITH_PLUGIN_DATA__;
|
|
504
|
+
return envelope?.[namespace];
|
|
505
|
+
}
|
|
506
|
+
|
|
488
507
|
// ============================================
|
|
489
508
|
// Browser Globals Setup
|
|
490
509
|
// ============================================
|
|
@@ -506,7 +525,8 @@ export function setupGlobals(): void {
|
|
|
506
525
|
onMount: zenOnMount,
|
|
507
526
|
onUnmount: zenOnUnmount,
|
|
508
527
|
triggerMount,
|
|
509
|
-
triggerUnmount
|
|
528
|
+
triggerUnmount,
|
|
529
|
+
getPluginData: getPluginRuntimeData // Access plugin data from envelope
|
|
510
530
|
};
|
|
511
531
|
|
|
512
532
|
// Expression registry
|
package/cli/utils/content.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { marked } from 'marked';
|
|
4
|
-
|
|
5
|
-
export interface ContentItem {
|
|
6
|
-
id?: string | number;
|
|
7
|
-
slug?: string | null;
|
|
8
|
-
collection?: string | null;
|
|
9
|
-
content?: string | null;
|
|
10
|
-
[key: string]: any | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Load all content from the content directory
|
|
15
|
-
*/
|
|
16
|
-
export function loadContent(contentDir: string): Record<string, ContentItem[]> {
|
|
17
|
-
if (!fs.existsSync(contentDir)) {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const collections: Record<string, ContentItem[]> = {};
|
|
22
|
-
const files = getAllFiles(contentDir);
|
|
23
|
-
|
|
24
|
-
for (const filePath of files) {
|
|
25
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
26
|
-
const relativePath = path.relative(contentDir, filePath);
|
|
27
|
-
const collection = relativePath.split(path.sep)[0];
|
|
28
|
-
if (!collection) continue;
|
|
29
|
-
|
|
30
|
-
const slug = relativePath.replace(/\.(md|mdx|json)$/, '').replace(/\\/g, '/');
|
|
31
|
-
const id = slug;
|
|
32
|
-
|
|
33
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
-
|
|
35
|
-
if (!collections[collection]) {
|
|
36
|
-
collections[collection] = [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (ext === '.json') {
|
|
40
|
-
try {
|
|
41
|
-
const data = JSON.parse(rawContent);
|
|
42
|
-
collections[collection].push({
|
|
43
|
-
id,
|
|
44
|
-
slug,
|
|
45
|
-
collection,
|
|
46
|
-
content: '',
|
|
47
|
-
...data
|
|
48
|
-
});
|
|
49
|
-
} catch (e) {
|
|
50
|
-
console.error(`Error parsing JSON file ${filePath}:`, e);
|
|
51
|
-
}
|
|
52
|
-
} else if (ext === '.md' || ext === '.mdx') {
|
|
53
|
-
const { metadata, content } = parseMarkdown(rawContent);
|
|
54
|
-
collections[collection].push({
|
|
55
|
-
id,
|
|
56
|
-
slug,
|
|
57
|
-
collection,
|
|
58
|
-
content,
|
|
59
|
-
...metadata
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return collections;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getAllFiles(dir: string, fileList: string[] = []): string[] {
|
|
68
|
-
const files = fs.readdirSync(dir);
|
|
69
|
-
files.forEach((file: string) => {
|
|
70
|
-
const name = path.join(dir, file);
|
|
71
|
-
if (fs.statSync(name).isDirectory()) {
|
|
72
|
-
getAllFiles(name, fileList);
|
|
73
|
-
} else {
|
|
74
|
-
fileList.push(name);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
return fileList;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function parseMarkdown(content: string): { metadata: Record<string, any>, content: string } {
|
|
81
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
82
|
-
const match = content.match(frontmatterRegex);
|
|
83
|
-
|
|
84
|
-
if (!match) {
|
|
85
|
-
return { metadata: {}, content: content.trim() };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const [, yamlStr, body] = match;
|
|
89
|
-
const metadata: Record<string, any> = {};
|
|
90
|
-
|
|
91
|
-
if (yamlStr) {
|
|
92
|
-
yamlStr.split('\n').forEach(line => {
|
|
93
|
-
const [key, ...values] = line.split(':');
|
|
94
|
-
if (key && values.length > 0) {
|
|
95
|
-
const value = values.join(':').trim();
|
|
96
|
-
// Basic type conversion
|
|
97
|
-
if (value === 'true') metadata[key.trim()] = true;
|
|
98
|
-
else if (value === 'false') metadata[key.trim()] = false;
|
|
99
|
-
else if (!isNaN(Number(value))) metadata[key.trim()] = Number(value);
|
|
100
|
-
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
101
|
-
metadata[key.trim()] = value.slice(1, -1).split(',').map(v => v.trim().replace(/^['"]|['"]$/g, ''));
|
|
102
|
-
}
|
|
103
|
-
else metadata[key.trim()] = value.replace(/^['"]|['"]$/g, '');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
metadata,
|
|
110
|
-
content: marked.parse((body || '').trim()) as string
|
|
111
|
-
};
|
|
112
|
-
}
|
package/router/manifest.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zenith Route Manifest Generator
|
|
3
|
-
*
|
|
4
|
-
* Scans pages/ directory at build time and generates a route manifest
|
|
5
|
-
* with proper scoring for deterministic route matching.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import fs from "fs"
|
|
9
|
-
import path from "path"
|
|
10
|
-
import {
|
|
11
|
-
type RouteDefinition,
|
|
12
|
-
type ParsedSegment,
|
|
13
|
-
SegmentType
|
|
14
|
-
} from "./types"
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Scoring constants for route ranking
|
|
18
|
-
* Higher scores = higher priority
|
|
19
|
-
*/
|
|
20
|
-
const SEGMENT_SCORES = {
|
|
21
|
-
[SegmentType.STATIC]: 10,
|
|
22
|
-
[SegmentType.DYNAMIC]: 5,
|
|
23
|
-
[SegmentType.CATCH_ALL]: 1,
|
|
24
|
-
[SegmentType.OPTIONAL_CATCH_ALL]: 0
|
|
25
|
-
} as const
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Discover all .zen files in the pages directory
|
|
29
|
-
*/
|
|
30
|
-
export function discoverPages(pagesDir: string): string[] {
|
|
31
|
-
const pages: string[] = []
|
|
32
|
-
|
|
33
|
-
function walk(dir: string): void {
|
|
34
|
-
if (!fs.existsSync(dir)) return
|
|
35
|
-
|
|
36
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
37
|
-
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const fullPath = path.join(dir, entry.name)
|
|
40
|
-
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
walk(fullPath)
|
|
43
|
-
} else if (entry.isFile() && entry.name.endsWith(".zen")) {
|
|
44
|
-
pages.push(fullPath)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
walk(pagesDir)
|
|
50
|
-
return pages
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Convert a file path to a route path
|
|
55
|
-
*
|
|
56
|
-
* Examples:
|
|
57
|
-
* pages/index.zen → /
|
|
58
|
-
* pages/about.zen → /about
|
|
59
|
-
* pages/blog/index.zen → /blog
|
|
60
|
-
* pages/blog/[id].zen → /blog/:id
|
|
61
|
-
* pages/posts/[...slug].zen → /posts/*slug
|
|
62
|
-
* pages/[[...all]].zen → /*all (optional)
|
|
63
|
-
*/
|
|
64
|
-
export function filePathToRoutePath(filePath: string, pagesDir: string): string {
|
|
65
|
-
// Get relative path from pages directory
|
|
66
|
-
const relativePath = path.relative(pagesDir, filePath)
|
|
67
|
-
|
|
68
|
-
// Remove .zen extension
|
|
69
|
-
const withoutExt = relativePath.replace(/\.zen$/, "")
|
|
70
|
-
|
|
71
|
-
// Split into segments
|
|
72
|
-
const segments = withoutExt.split(path.sep)
|
|
73
|
-
|
|
74
|
-
// Transform segments
|
|
75
|
-
const routeSegments: string[] = []
|
|
76
|
-
|
|
77
|
-
for (const segment of segments) {
|
|
78
|
-
// Handle index files (they represent the directory root)
|
|
79
|
-
if (segment === "index") {
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Handle optional catch-all: [[...param]]
|
|
84
|
-
const optionalCatchAllMatch = segment.match(/^\[\[\.\.\.(\w+)\]\]$/)
|
|
85
|
-
if (optionalCatchAllMatch) {
|
|
86
|
-
routeSegments.push(`*${optionalCatchAllMatch[1]}?`)
|
|
87
|
-
continue
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Handle required catch-all: [...param]
|
|
91
|
-
const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/)
|
|
92
|
-
if (catchAllMatch) {
|
|
93
|
-
routeSegments.push(`*${catchAllMatch[1]}`)
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Handle dynamic segment: [param]
|
|
98
|
-
const dynamicMatch = segment.match(/^\[(\w+)\]$/)
|
|
99
|
-
if (dynamicMatch) {
|
|
100
|
-
routeSegments.push(`:${dynamicMatch[1]}`)
|
|
101
|
-
continue
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Static segment
|
|
105
|
-
routeSegments.push(segment)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Build route path
|
|
109
|
-
const routePath = "/" + routeSegments.join("/")
|
|
110
|
-
|
|
111
|
-
// Normalize trailing slashes
|
|
112
|
-
return routePath === "/" ? "/" : routePath.replace(/\/$/, "")
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Parse a route path into segments with type information
|
|
117
|
-
*/
|
|
118
|
-
export function parseRouteSegments(routePath: string): ParsedSegment[] {
|
|
119
|
-
if (routePath === "/") {
|
|
120
|
-
return []
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const segments = routePath.slice(1).split("/")
|
|
124
|
-
const parsed: ParsedSegment[] = []
|
|
125
|
-
|
|
126
|
-
for (const segment of segments) {
|
|
127
|
-
// Optional catch-all: *param?
|
|
128
|
-
if (segment.startsWith("*") && segment.endsWith("?")) {
|
|
129
|
-
parsed.push({
|
|
130
|
-
type: SegmentType.OPTIONAL_CATCH_ALL,
|
|
131
|
-
paramName: segment.slice(1, -1),
|
|
132
|
-
raw: segment
|
|
133
|
-
})
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Required catch-all: *param
|
|
138
|
-
if (segment.startsWith("*")) {
|
|
139
|
-
parsed.push({
|
|
140
|
-
type: SegmentType.CATCH_ALL,
|
|
141
|
-
paramName: segment.slice(1),
|
|
142
|
-
raw: segment
|
|
143
|
-
})
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Dynamic: :param
|
|
148
|
-
if (segment.startsWith(":")) {
|
|
149
|
-
parsed.push({
|
|
150
|
-
type: SegmentType.DYNAMIC,
|
|
151
|
-
paramName: segment.slice(1),
|
|
152
|
-
raw: segment
|
|
153
|
-
})
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Static
|
|
158
|
-
parsed.push({
|
|
159
|
-
type: SegmentType.STATIC,
|
|
160
|
-
raw: segment
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return parsed
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Calculate route score based on segments
|
|
169
|
-
* Higher scores = higher priority for matching
|
|
170
|
-
*/
|
|
171
|
-
export function calculateRouteScore(segments: ParsedSegment[]): number {
|
|
172
|
-
if (segments.length === 0) {
|
|
173
|
-
// Root route gets a high score
|
|
174
|
-
return 100
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let score = 0
|
|
178
|
-
|
|
179
|
-
for (const segment of segments) {
|
|
180
|
-
score += SEGMENT_SCORES[segment.type]
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Bonus for having more static segments (specificity)
|
|
184
|
-
const staticCount = segments.filter(s => s.type === SegmentType.STATIC).length
|
|
185
|
-
score += staticCount * 2
|
|
186
|
-
|
|
187
|
-
return score
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Extract parameter names from parsed segments
|
|
192
|
-
*/
|
|
193
|
-
export function extractParamNames(segments: ParsedSegment[]): string[] {
|
|
194
|
-
return segments
|
|
195
|
-
.filter(s => s.paramName !== undefined)
|
|
196
|
-
.map(s => s.paramName!)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Convert route path to regex pattern
|
|
201
|
-
*
|
|
202
|
-
* Examples:
|
|
203
|
-
* /about → /^\/about\/?$/
|
|
204
|
-
* /blog/:id → /^\/blog\/([^/]+)\/?$/
|
|
205
|
-
* /posts/*slug → /^\/posts\/(.+)\/?$/
|
|
206
|
-
* / → /^\/$/
|
|
207
|
-
* /*all? → /^(?:\/(.*))?$/ (optional catch-all)
|
|
208
|
-
*/
|
|
209
|
-
export function routePathToRegex(routePath: string): RegExp {
|
|
210
|
-
if (routePath === "/") {
|
|
211
|
-
return /^\/$/
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const segments = routePath.slice(1).split("/")
|
|
215
|
-
const regexParts: string[] = []
|
|
216
|
-
|
|
217
|
-
for (let i = 0; i < segments.length; i++) {
|
|
218
|
-
const segment = segments[i]
|
|
219
|
-
if (!segment) continue
|
|
220
|
-
|
|
221
|
-
// Optional catch-all: *param?
|
|
222
|
-
if (segment.startsWith("*") && segment.endsWith("?")) {
|
|
223
|
-
// Optional catch-all - matches zero or more path segments
|
|
224
|
-
// Should only be at the end
|
|
225
|
-
regexParts.push("(?:\\/(.*))?")
|
|
226
|
-
continue
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Required catch-all: *param
|
|
230
|
-
if (segment.startsWith("*")) {
|
|
231
|
-
// Required catch-all - matches one or more path segments
|
|
232
|
-
regexParts.push("\\/(.+)")
|
|
233
|
-
continue
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Dynamic: :param
|
|
237
|
-
if (segment.startsWith(":")) {
|
|
238
|
-
regexParts.push("\\/([^/]+)")
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Static segment - escape special regex characters
|
|
243
|
-
const escaped = segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
244
|
-
regexParts.push(`\\/${escaped}`)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Build final regex with optional trailing slash
|
|
248
|
-
const pattern = `^${regexParts.join("")}\\/?$`
|
|
249
|
-
return new RegExp(pattern)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Generate a route definition from a file path
|
|
254
|
-
*/
|
|
255
|
-
export function generateRouteDefinition(
|
|
256
|
-
filePath: string,
|
|
257
|
-
pagesDir: string
|
|
258
|
-
): RouteDefinition {
|
|
259
|
-
const routePath = filePathToRoutePath(filePath, pagesDir)
|
|
260
|
-
const segments = parseRouteSegments(routePath)
|
|
261
|
-
const paramNames = extractParamNames(segments)
|
|
262
|
-
const score = calculateRouteScore(segments)
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
path: routePath,
|
|
266
|
-
segments,
|
|
267
|
-
paramNames,
|
|
268
|
-
score,
|
|
269
|
-
filePath
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Generate route manifest from pages directory
|
|
275
|
-
* Returns route definitions sorted by score (highest first)
|
|
276
|
-
*/
|
|
277
|
-
export function generateRouteManifest(pagesDir: string): RouteDefinition[] {
|
|
278
|
-
const pages = discoverPages(pagesDir)
|
|
279
|
-
|
|
280
|
-
const definitions = pages.map(filePath =>
|
|
281
|
-
generateRouteDefinition(filePath, pagesDir)
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
// Sort by score descending (highest priority first)
|
|
285
|
-
definitions.sort((a, b) => b.score - a.score)
|
|
286
|
-
|
|
287
|
-
return definitions
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Generate the route manifest as JavaScript code for runtime
|
|
292
|
-
*/
|
|
293
|
-
export function generateRouteManifestCode(definitions: RouteDefinition[]): string {
|
|
294
|
-
const routeEntries = definitions.map(def => {
|
|
295
|
-
const regex = routePathToRegex(def.path)
|
|
296
|
-
|
|
297
|
-
return ` {
|
|
298
|
-
path: ${JSON.stringify(def.path)},
|
|
299
|
-
regex: ${regex.toString()},
|
|
300
|
-
paramNames: ${JSON.stringify(def.paramNames)},
|
|
301
|
-
score: ${def.score},
|
|
302
|
-
filePath: ${JSON.stringify(def.filePath)}
|
|
303
|
-
}`
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
return `// Auto-generated route manifest
|
|
307
|
-
// Do not edit directly
|
|
308
|
-
|
|
309
|
-
export const routeManifest = [
|
|
310
|
-
${routeEntries.join(",\n")}
|
|
311
|
-
];
|
|
312
|
-
`
|
|
313
|
-
}
|
|
314
|
-
|