@zenithbuild/core 0.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.
Files changed (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
@@ -0,0 +1,116 @@
1
+ # 💫 Contributing to Zenith
2
+ ## 📌 Prerequisites
3
+
4
+ > Before you begin, join the [Discord Server](https://discord.gg/T85bBj8T3n) and **ensure you have the following installed**:
5
+
6
+ - **[Bun](https://bun.sh/)** (v1.0 or later) - Zenith uses Bun as its JavaScript runtime and package manager
7
+ - **[Git](https://git-scm.com/)** - For version control
8
+
9
+ ### Installation
10
+
11
+ 1. **Clone the repository (no fork)**
12
+ ```ts
13
+ git clone https://github.com/judahbsullivan/zenith.js.git
14
+ cd zenith
15
+ ```
16
+ 2. **Install dependencies**
17
+ ```ts
18
+ bun install
19
+ ```
20
+ 3. **Build the application** (required first)
21
+ ```ts
22
+ bun run build
23
+ ```
24
+ This compiles `.zen` files from `app/pages/` into `app/dist/`.
25
+
26
+ 4. **Run the development server**
27
+ ```ts
28
+ bun run dev
29
+
30
+ > **Important**: Use `bun run <script>` to execute npm scripts from package.json. The command `bun build` (without `run`) invokes Bun's built-in bundler, which won't specify our entrypoints by default.
31
+
32
+ ## 🌿 Branching Strategy
33
+ ### Option 1: Issue-Driven Development (Preferred)
34
+
35
+ 1. **Create or find an issue** on GitHub that describes the work you want to do
36
+ 2. **Create a branch from the issue** directly on GitHub
37
+ - Branch name format: `{issue-number}-{short-description}`
38
+ - Example: `33-contributing-md`
39
+ 3. **Pull the branch** to your local machine
40
+ ```ts
41
+ git fetch origin
42
+ git checkout 33-contributing-md
43
+ ```
44
+
45
+ ### Option 2: Feature/Fix Branch
46
+
47
+ If you're working on something without an existing issue:
48
+
49
+ 1. **Branch from `main`**
50
+ ```ts
51
+ git checkout main
52
+ git pull origin main
53
+ git checkout -b {prefix}/{short-description}
54
+ ```
55
+ 2. **Use appropriate prefix subfolder** based on the type of change:
56
+ - `feat/` for new features → MINOR version bump (0.n.0)
57
+ - `fix/` for bug fixes → PATCH version bump (0.0.n)
58
+ - `docs/` for documentation changes
59
+ - `refactor/` for code refactoring
60
+ - `test/` for adding or updating tests
61
+ - `chore/` for maintenance tasks
62
+
63
+ Example: `feat/reactive-state` or `fix/router-navigation`
64
+
65
+ ## 📝 Commit Strategy
66
+ > Zenith follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for consistent versioning and changelog generation.
67
+
68
+ ### Commit Message Format
69
+
70
+ ```ts
71
+ <type>[optional scope]: <description>
72
+ [optional body]
73
+ [optional footer(s)]
74
+ ```
75
+ ### Quick Reference
76
+
77
+ | Prefix | SemVer Impact | Example |
78
+ |----------|---------------|----------------------------------|
79
+ | `fix:` | PATCH (0.0.n) | `fix: html not recognizing state` |
80
+ | `feat:` | MINOR (0.n.0) | `feat: add reactive state system` |
81
+ | `feat!:` | MAJOR (n.0.0) | `feat!: change compiler API` |
82
+ | `fix!:` | MAJOR (n.0.0) | `fix!: remove deprecated methods` |
83
+
84
+ **Breaking Changes**: Use `!` after the type or include `BREAKING CHANGE:` in the footer.
85
+
86
+ **📌 For complete details, see [COMMITS.md](COMMITS.md)**
87
+
88
+ ## 💬 Pull Request Comment Strategy
89
+ > Zenith uses [Conventional Comments](https://conventionalcomments.org/) for clear, actionable PR feedback.
90
+
91
+ ### Comment Format
92
+ ```ts
93
+ <label> [decorations]: <subject>
94
+ [discussion]
95
+ ```
96
+ ### Examples
97
+ ```ts
98
+ suggestion (non-blocking): Consider using a more descriptive variable name
99
+ This would improve readability for future maintainers.
100
+ ```
101
+ ```ts
102
+ issue (blocking): This function will throw an error when input is null
103
+ We need to add null checking before processing.
104
+ ```
105
+
106
+ **📌 For complete details, see [COMMENTS.md](COMMENTS.md)**
107
+
108
+ ## ❓ Questions?
109
+ - Check existing [Issues](https://github.com/judahbsullivan/zenith/issues)
110
+ - Review [README.md](../README.md) for project overview
111
+ - Reach out to maintainers in your PR, Issue or [Discord!](https://discord.gg/T85bBj8T3n)
112
+
113
+ ---
114
+
115
+ **Remember**: Contributing should be enjoyable! Don't hesitate to ask questions if anything is unclear.
116
+
@@ -0,0 +1,62 @@
1
+ # Style Guide
2
+ ## Code Style
3
+ - TypeScript only
4
+ - No implicit any
5
+ - Prefer explicit return types for public APIs
6
+ - Avoid magic strings
7
+ ## Architectural Rules
8
+ - Compiler decisions > runtime decisions
9
+ - HTML is the source of truth
10
+ - Navigation controls rendering, not components
11
+ - Layouts persist unless explicitly changed
12
+ ## Naming Conventions
13
+ - camelCase for variables
14
+ - PascalCase for components
15
+ - kebab-case for files where appropriate
16
+ - `.zenith` for user-facing components
17
+ ## API Design Rules
18
+ - No developer flags for runtime behavior
19
+ - Behavior inferred via static analysis
20
+ - Fail loudly at compile time
21
+ ## What to Avoid
22
+ - Implicit global state
23
+ - Hidden hydration logic
24
+ - Runtime heuristics
25
+ - JSX-only APIs
26
+ ## Guiding Question
27
+ > "Can the compiler decide this instead?"
28
+ If yes — move it out of runtime.
29
+
30
+ ## 🎯 Development Philosophy
31
+
32
+ - **Simplicity over complexity** - Zenith aims to be intuitive and straightforward
33
+ - **HTML-first** - HTML is the source of truth
34
+ - **Consistency** - Follow established patterns in the codebase
35
+ - **Iteration speed** - Don't let perfect be the enemy of good
36
+
37
+ ## 🧪 Testing Your Changes
38
+
39
+ Before submitting a PR:
40
+
41
+ 1. **Build the application** to compile your changes:
42
+ ```bash
43
+ bun run build
44
+ ```
45
+
46
+ 2. **Run the dev server** and verify your changes work:
47
+ ```bash
48
+ bun run dev
49
+ ```
50
+ Visit `http://localhost:3000` to test.
51
+
52
+ 3. **Format your code**:
53
+ ```bash
54
+ bun run format
55
+ ```
56
+
57
+ 4. **Check formatting** (optional):
58
+ ```bash
59
+ bun run format:check
60
+ ```
61
+
62
+ > **Note**: Use `bun run <script>` for npm scripts. Plain `bun build` invokes Bun's bundler, not the project build script.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@zenithbuild/core",
3
+ "version": "0.1.0",
4
+ "description": "Core library for the Zenith framework",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./index.ts",
8
+ "exports": {
9
+ ".": "./index.ts",
10
+ "./compiler": "./compiler/index.ts",
11
+ "./core": "./core/index.ts",
12
+ "./router": "./router/index.ts",
13
+ "./runtime": "./runtime/index.ts"
14
+ },
15
+ "keywords": [
16
+ "zenith",
17
+ "core",
18
+ "framework"
19
+ ],
20
+ "author": "Zenith Team",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git@github.com:zenithbuild/zenith-core.git"
24
+ },
25
+ "scripts": {
26
+ "dev": "bun dev",
27
+ "build": "bun build",
28
+ "start": "bun run build && bun run dev",
29
+ "format": "prettier --write \"**/*.ts\"",
30
+ "format:check": "prettier --check \"**/*.ts\""
31
+ },
32
+ "private": false,
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "prettier": "^3.7.4"
36
+ },
37
+ "peerDependencies": {
38
+ "typescript": "^5"
39
+ },
40
+ "dependencies": {
41
+ "@types/parse5": "^7.0.0",
42
+ "parse5": "^8.0.0"
43
+ }
44
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Zenith Router
3
+ *
4
+ * File-based SPA router for Zenith framework.
5
+ * Includes routing, navigation, and ZenLink components.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { navigate, isActive, prefetch } from 'zenith/router'
10
+ *
11
+ * // Navigate programmatically
12
+ * navigate('/about')
13
+ *
14
+ * // Check active state
15
+ * if (isActive('/blog')) {
16
+ * console.log('On blog section')
17
+ * }
18
+ * ```
19
+ */
20
+
21
+ // Core router types and utilities
22
+ export * from "./types"
23
+ export * from "./manifest"
24
+
25
+ // Router runtime (core router implementation)
26
+ // These are the primary exports for router functionality
27
+ export {
28
+ initRouter,
29
+ resolveRoute,
30
+ navigate,
31
+ getRoute,
32
+ onRouteChange,
33
+ beforeEach,
34
+ afterEach,
35
+ isActive,
36
+ prefetch,
37
+ isPrefetched
38
+ } from "./runtime"
39
+
40
+ // Navigation utilities (additional helpers and zen* prefixed exports)
41
+ // Note: Some functions like navigate, isActive, prefetch are also in runtime
42
+ // We export runtime's versions above, and navigation's unique functions here
43
+ export {
44
+ // Navigation API (zen* prefixed names)
45
+ zenNavigate,
46
+ zenBack,
47
+ zenForward,
48
+ zenGo,
49
+ zenIsActive,
50
+ zenPrefetch,
51
+ zenIsPrefetched,
52
+ zenGetRoute,
53
+ zenGetParam,
54
+ zenGetQuery,
55
+ createZenLink,
56
+ zenLink,
57
+ // Additional navigation utilities (not in runtime)
58
+ back,
59
+ forward,
60
+ go,
61
+ getParam,
62
+ getQuery,
63
+ isExternalUrl,
64
+ shouldUseSPANavigation,
65
+ normalizePath,
66
+ setGlobalTransition,
67
+ getGlobalTransition,
68
+ createTransitionContext
69
+ } from "./navigation/index"
70
+
71
+ // Navigation-specific types
72
+ export type {
73
+ ZenLinkProps,
74
+ TransitionContext,
75
+ TransitionHandler
76
+ } from "./navigation/index"
@@ -0,0 +1,314 @@
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
+