@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.
- package/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- 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
|
+
}
|
package/router/index.ts
ADDED
|
@@ -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
|
+
|