@chr33s/solarflare 0.0.2
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/package.json +52 -0
- package/readme.md +183 -0
- package/src/ast.ts +316 -0
- package/src/build.bundle-client.ts +404 -0
- package/src/build.bundle-server.ts +131 -0
- package/src/build.bundle.ts +48 -0
- package/src/build.emit-manifests.ts +25 -0
- package/src/build.hmr-entry.ts +88 -0
- package/src/build.scan.ts +182 -0
- package/src/build.ts +227 -0
- package/src/build.validate.ts +63 -0
- package/src/client.hmr.ts +78 -0
- package/src/client.styles.ts +68 -0
- package/src/client.ts +190 -0
- package/src/codemod.ts +688 -0
- package/src/console-forward.ts +254 -0
- package/src/critical-css.ts +103 -0
- package/src/devtools-json.ts +52 -0
- package/src/diff-dom-streaming.ts +406 -0
- package/src/early-flush.ts +125 -0
- package/src/early-hints.ts +83 -0
- package/src/fetch.ts +44 -0
- package/src/fs.ts +11 -0
- package/src/head.ts +876 -0
- package/src/hmr.ts +647 -0
- package/src/hydration.ts +238 -0
- package/src/manifest.runtime.ts +25 -0
- package/src/manifest.ts +23 -0
- package/src/paths.ts +96 -0
- package/src/render-priority.ts +69 -0
- package/src/route-cache.ts +163 -0
- package/src/router-deferred.ts +85 -0
- package/src/router-stream.ts +65 -0
- package/src/router.ts +535 -0
- package/src/runtime.ts +32 -0
- package/src/serialize.ts +38 -0
- package/src/server.hmr.ts +67 -0
- package/src/server.styles.ts +42 -0
- package/src/server.ts +480 -0
- package/src/solarflare.d.ts +101 -0
- package/src/speculation-rules.ts +171 -0
- package/src/store.ts +78 -0
- package/src/stream-assets.ts +135 -0
- package/src/stylesheets.ts +222 -0
- package/src/worker.config.ts +243 -0
- package/src/worker.ts +542 -0
- package/tsconfig.json +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chr33s/solarflare",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"bin": "./src/build.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"tsconfig.json",
|
|
9
|
+
"!src/*.test.ts"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/solarflare.d.ts",
|
|
15
|
+
"import": "./src/worker.ts"
|
|
16
|
+
},
|
|
17
|
+
"./client": "./src/client.ts",
|
|
18
|
+
"./server": "./src/server.ts",
|
|
19
|
+
"./tsconfig.json": "./tsconfig.json"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"check": "oxfmt --check && oxlint --type-aware --type-check",
|
|
23
|
+
"fix": "oxlint --type-aware --type-check --fix && oxfmt --write",
|
|
24
|
+
"test": "WRANGLER_LOG=error; node --test src/*.test.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@preact/signals": "2.8.0",
|
|
28
|
+
"lightningcss": "1.31.1",
|
|
29
|
+
"picomatch": "4.0.3",
|
|
30
|
+
"preact": "11.0.0-beta.1",
|
|
31
|
+
"preact-custom-element": "4.6.0",
|
|
32
|
+
"preact-render-to-string": "6.6.5",
|
|
33
|
+
"rolldown": "1.0.0-rc.4",
|
|
34
|
+
"turbo-stream": "3.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@preact/signals-debug": "1.4.1",
|
|
38
|
+
"@types/node": "25.2.3",
|
|
39
|
+
"oxfmt": "0.32.0",
|
|
40
|
+
"oxlint": "1.47.0",
|
|
41
|
+
"oxlint-tsgolint": "0.13.0",
|
|
42
|
+
"playwright": "1.58.2",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"wrangler": "4.65.0"
|
|
45
|
+
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"ts-morph": "27.0.2"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.12.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
> [!WARNING]
|
|
2
|
+
> Experimental: API is unstable and not production-ready.
|
|
3
|
+
|
|
4
|
+
# Solarflare
|
|
5
|
+
|
|
6
|
+
Cloudflare-optimized streaming SSR/CSR meta-framework built on web platform APIs, whilst retaining the DX of JSX / React|Preact.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Streaming SSR** — File-based routing with deferred promise streaming
|
|
11
|
+
- **Web Components** — Hydration via `preact-custom-element`
|
|
12
|
+
- **SPA Navigation** — Navigation API + View Transitions
|
|
13
|
+
- **HMR** — Hot module replacement with scroll restoration
|
|
14
|
+
- **Styles** — Constructable Stylesheets, critical CSS extraction
|
|
15
|
+
- **Performance** — Early hints, route caching, preconnect hints, speculation rules
|
|
16
|
+
- **Cloudflare** — Workers-optimized with edge caching
|
|
17
|
+
- **TypeScript** — Full type safety
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- [Node.js](https://nodejs.org) ≥v24.12.0
|
|
22
|
+
- Modern browser (Chrome 102+, Edge 102+, Safari 15.4+)
|
|
23
|
+
|
|
24
|
+
## CLI
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
solarflare [options]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Option | Description |
|
|
31
|
+
| -------------------- | ------------------------- |
|
|
32
|
+
| `--clean`, `-c` | Clean output before build |
|
|
33
|
+
| `--debug`, `-d` | Enable debugging |
|
|
34
|
+
| `--production`, `-p` | Optimize for production |
|
|
35
|
+
| `--serve`, `-s` | Start dev server with HMR |
|
|
36
|
+
| `--sourcemap` | Generate source maps |
|
|
37
|
+
| `--watch`, `-w` | Watch and rebuild |
|
|
38
|
+
|
|
39
|
+
## Conventions
|
|
40
|
+
|
|
41
|
+
| Directory | Purpose |
|
|
42
|
+
| ---------- | ------------------------------------------------------- |
|
|
43
|
+
| `./src` | Original (source) human readable code |
|
|
44
|
+
| `./dist` | Compiled (distribution) [client, server] output code |
|
|
45
|
+
| `./public` | Static assets, copied verbatim to dist/client directory |
|
|
46
|
+
|
|
47
|
+
| File | Purpose |
|
|
48
|
+
| -------------- | -------------------------------- |
|
|
49
|
+
| `*.client.tsx` | Client component (web component) |
|
|
50
|
+
| `*.server.tsx` | Server handler (Workers runtime) |
|
|
51
|
+
| `_layout.tsx` | Layout wrapper |
|
|
52
|
+
| `_*` | Private (not routed) |
|
|
53
|
+
| `$param` | Dynamic segment → `:param` |
|
|
54
|
+
| `index.*` | Directory root |
|
|
55
|
+
|
|
56
|
+
| Path | Purpose |
|
|
57
|
+
| ----- | ---------------------------------------- |
|
|
58
|
+
| `/_*` | reserved internal use (e.g. `/_console`) |
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### Server Handler
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
export default async function server(request: Request, params: Record<string, string>) {
|
|
66
|
+
return {
|
|
67
|
+
_status: 200,
|
|
68
|
+
_headers: { "Cache-Control": "max-age=3600" },
|
|
69
|
+
title: "Hello",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Promise-valued props are streamed independently (deferred):
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
export default async function server() {
|
|
78
|
+
const user = await fetchUser(); // blocking
|
|
79
|
+
const analytics = fetchAnalytics(); // deferred
|
|
80
|
+
const recommendations = fetchRecommendations(); // deferred
|
|
81
|
+
return { user, analytics, recommendations };
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Client Component
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
export default function Client({ title }: { title: string }) {
|
|
89
|
+
return <h1>{title}</h1>;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Layout
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import type { VNode } from "preact";
|
|
97
|
+
import { Body, Head } from "@chr33s/solarflare/server";
|
|
98
|
+
|
|
99
|
+
export default function Layout({ children }: { children: VNode }) {
|
|
100
|
+
return (
|
|
101
|
+
<html>
|
|
102
|
+
<head>
|
|
103
|
+
<Head />
|
|
104
|
+
</html>
|
|
105
|
+
<body>
|
|
106
|
+
{children}
|
|
107
|
+
<Body />
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Deferred (Suspense like deferred renderer)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { Deferred } from "@chr33s/solarflare/client";
|
|
118
|
+
|
|
119
|
+
<Deferred priority="high" fallback={<div>Loading additional content...</div>}>
|
|
120
|
+
...
|
|
121
|
+
</Deferred>;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Configuration using meta tags
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<!-- router -->
|
|
128
|
+
<meta name="sf:base" content="/" />
|
|
129
|
+
<meta name="sf:scroll-behavior" content="auto" />
|
|
130
|
+
<meta name="sf:view-transitions" content="false" />
|
|
131
|
+
|
|
132
|
+
<!-- performance -->
|
|
133
|
+
<meta name="sf:preconnect" content="https://cdn.example.com" />
|
|
134
|
+
<meta name="sf:early-flush" content="true" />
|
|
135
|
+
<meta name="sf:critical-css" content="true" />
|
|
136
|
+
<meta name="sf:cache-max-age" content="300" />
|
|
137
|
+
<meta name="sf:cache-swr" content="3600" />
|
|
138
|
+
<meta name="sf:prefetch" content="/about, /faq, /blog/*" />
|
|
139
|
+
<meta name="sf:prerender" content="/, /landing" />
|
|
140
|
+
<meta name="sf:prefetch-selector" content="a.nav-link" />
|
|
141
|
+
<meta name="sf:speculation-eagerness" content="moderate" />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom Web Component
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { define } from "@chr33s/solarflare/client";
|
|
148
|
+
export default define(MyComponent, { shadow: true });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Environment
|
|
152
|
+
|
|
153
|
+
| File | Purpose |
|
|
154
|
+
| ----------------------- | ------------------------------------------------------------ |
|
|
155
|
+
| `WRANGLER_LOG` | Set logging verbosity for both wrangler & console forwarding |
|
|
156
|
+
| `WRANGLER_SEND_METRICS` | Disable sending anonymous usage data to Cloudflare |
|
|
157
|
+
|
|
158
|
+
## Examples
|
|
159
|
+
|
|
160
|
+
- [Basic](examples/basic/readme.md) — Layouts, dynamic routes, API, components
|
|
161
|
+
- [Bun](examples/bun/readme.md) — Bun runtime example
|
|
162
|
+
- [Deno](examples/deno/readme.md) — Deno runtime example
|
|
163
|
+
- [Minimal](examples/minimal/readme.md) — Single route
|
|
164
|
+
- [Node](examples/node/readme.md) — Using `srvx` instead of Workers
|
|
165
|
+
- [Shopify App](examples/shopify-app/readme.md) — Shopify app starter
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
npm install
|
|
171
|
+
npm run dev
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Codemod
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
npm install --save-optional
|
|
178
|
+
npx solarflare --codemod ./app
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { parsePath, type ParsedPath, type ModuleKind } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function readCompilerOptions(
|
|
9
|
+
configPath = join(__dirname, "..", "tsconfig.json"),
|
|
10
|
+
sys: ts.ParseConfigHost = ts.sys,
|
|
11
|
+
) {
|
|
12
|
+
const configFile = ts.readConfigFile(configPath, (path) => sys.readFile(path));
|
|
13
|
+
|
|
14
|
+
if (configFile.error) {
|
|
15
|
+
console.warn("Failed to read tsconfig.json, using defaults");
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, sys, dirname(configPath));
|
|
20
|
+
|
|
21
|
+
if (parsed.errors.length > 0) {
|
|
22
|
+
console.warn("Errors parsing tsconfig.json, using defaults");
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return parsed.options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COMPILER_OPTIONS: ts.CompilerOptions = readCompilerOptions();
|
|
30
|
+
|
|
31
|
+
export function createProgram(files: string[]) {
|
|
32
|
+
return ts.createProgram(files, COMPILER_OPTIONS);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ExportInfo {
|
|
36
|
+
type: ts.Type;
|
|
37
|
+
signatures: readonly ts.Signature[];
|
|
38
|
+
typeString: string;
|
|
39
|
+
isFunction: boolean;
|
|
40
|
+
parameters: ParameterInfo[];
|
|
41
|
+
returnType: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ParameterInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
optional: boolean;
|
|
48
|
+
properties: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getFirstCallSignature(type: ts.Type) {
|
|
52
|
+
const signatures = type.getCallSignatures();
|
|
53
|
+
return signatures.length > 0 ? signatures[0] : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSignatureParameterInfo(
|
|
57
|
+
checker: ts.TypeChecker,
|
|
58
|
+
signature: ts.Signature,
|
|
59
|
+
sourceFile: ts.SourceFile,
|
|
60
|
+
) {
|
|
61
|
+
const parameters: ParameterInfo[] = [];
|
|
62
|
+
|
|
63
|
+
for (const param of signature.getParameters()) {
|
|
64
|
+
const paramType = checker.getTypeOfSymbolAtLocation(param, sourceFile);
|
|
65
|
+
const properties = paramType.getProperties().map((p) => p.getName());
|
|
66
|
+
|
|
67
|
+
parameters.push({
|
|
68
|
+
name: param.getName(),
|
|
69
|
+
type: checker.typeToString(paramType),
|
|
70
|
+
optional: !!(param.flags & ts.SymbolFlags.Optional),
|
|
71
|
+
properties,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
parameters,
|
|
77
|
+
returnType: checker.typeToString(signature.getReturnType()),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Gets detailed information about a module's default export. */
|
|
82
|
+
export function getDefaultExportInfo(checker: ts.TypeChecker, sourceFile: ts.SourceFile) {
|
|
83
|
+
const symbol = checker.getSymbolAtLocation(sourceFile);
|
|
84
|
+
if (!symbol) return null;
|
|
85
|
+
|
|
86
|
+
const exports = checker.getExportsOfModule(symbol);
|
|
87
|
+
const defaultExport = exports.find((e) => e.escapedName === "default");
|
|
88
|
+
if (!defaultExport) return null;
|
|
89
|
+
|
|
90
|
+
const type = checker.getTypeOfSymbolAtLocation(defaultExport, sourceFile);
|
|
91
|
+
const signatures = type.getCallSignatures();
|
|
92
|
+
const typeString = checker.typeToString(type);
|
|
93
|
+
const signature = getFirstCallSignature(type);
|
|
94
|
+
const isFunction = !!signature;
|
|
95
|
+
|
|
96
|
+
const parameters: ParameterInfo[] = [];
|
|
97
|
+
let returnType: string | null = null;
|
|
98
|
+
|
|
99
|
+
if (isFunction && signature) {
|
|
100
|
+
const info = getSignatureParameterInfo(checker, signature, sourceFile);
|
|
101
|
+
parameters.push(...info.parameters);
|
|
102
|
+
returnType = info.returnType;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type,
|
|
107
|
+
signatures,
|
|
108
|
+
typeString,
|
|
109
|
+
isFunction,
|
|
110
|
+
parameters,
|
|
111
|
+
returnType,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ValidationResult {
|
|
116
|
+
file: string;
|
|
117
|
+
kind: ModuleKind;
|
|
118
|
+
valid: boolean;
|
|
119
|
+
errors: string[];
|
|
120
|
+
warnings: string[];
|
|
121
|
+
exportInfo: ExportInfo | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function validateModule(program: ts.Program, filePath: string, baseDir: string = "./src") {
|
|
125
|
+
const fullPath = join(baseDir, filePath);
|
|
126
|
+
const sourceFile = program.getSourceFile(fullPath);
|
|
127
|
+
const checker = program.getTypeChecker();
|
|
128
|
+
const parsed = parsePath(filePath);
|
|
129
|
+
|
|
130
|
+
const result: ValidationResult = {
|
|
131
|
+
file: filePath,
|
|
132
|
+
kind: parsed.kind,
|
|
133
|
+
valid: true,
|
|
134
|
+
errors: [],
|
|
135
|
+
warnings: [],
|
|
136
|
+
exportInfo: null,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (!sourceFile) {
|
|
140
|
+
result.valid = false;
|
|
141
|
+
result.errors.push(`Source file not found: ${fullPath}`);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const exportInfo = getDefaultExportInfo(checker, sourceFile);
|
|
146
|
+
result.exportInfo = exportInfo;
|
|
147
|
+
|
|
148
|
+
if (!exportInfo) {
|
|
149
|
+
result.valid = false;
|
|
150
|
+
result.errors.push("Missing default export");
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate based on module kind
|
|
155
|
+
switch (parsed.kind) {
|
|
156
|
+
case "server":
|
|
157
|
+
validateServerModule(result, exportInfo);
|
|
158
|
+
break;
|
|
159
|
+
case "client":
|
|
160
|
+
validateClientModule(result, exportInfo);
|
|
161
|
+
break;
|
|
162
|
+
case "layout":
|
|
163
|
+
validateLayoutModule(result, exportInfo);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateServerModule(result: ValidationResult, exportInfo: ExportInfo) {
|
|
171
|
+
if (!exportInfo.isFunction) {
|
|
172
|
+
result.valid = false;
|
|
173
|
+
result.errors.push("Default export must be a function");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (exportInfo.parameters.length < 1) {
|
|
178
|
+
result.warnings.push("Server loader should accept (request, params?, env?) parameters");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check first param is Request-like
|
|
182
|
+
const firstParam = exportInfo.parameters[0];
|
|
183
|
+
if (firstParam && !firstParam.type.includes("Request") && firstParam.type !== "any") {
|
|
184
|
+
result.warnings.push(`First parameter should be Request, got ${firstParam.type}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateClientModule(result: ValidationResult, exportInfo: ExportInfo) {
|
|
189
|
+
if (!exportInfo.isFunction) {
|
|
190
|
+
result.valid = false;
|
|
191
|
+
result.errors.push("Default export must be a function component");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check return type is JSX-like
|
|
196
|
+
if (
|
|
197
|
+
exportInfo.returnType &&
|
|
198
|
+
!exportInfo.returnType.includes("VNode") &&
|
|
199
|
+
!exportInfo.returnType.includes("Element") &&
|
|
200
|
+
!exportInfo.returnType.includes("JSX") &&
|
|
201
|
+
exportInfo.returnType !== "null" &&
|
|
202
|
+
exportInfo.returnType !== "any"
|
|
203
|
+
) {
|
|
204
|
+
result.warnings.push(`Component should return JSX, got ${exportInfo.returnType}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateLayoutModule(result: ValidationResult, exportInfo: ExportInfo) {
|
|
209
|
+
if (!exportInfo.isFunction) {
|
|
210
|
+
result.valid = false;
|
|
211
|
+
result.errors.push("Default export must be a function component");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (exportInfo.parameters.length === 0) {
|
|
216
|
+
result.warnings.push("Layout should accept { children } prop");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check first param has 'children' property
|
|
221
|
+
const firstParam = exportInfo.parameters[0];
|
|
222
|
+
if (!firstParam.properties.includes("children")) {
|
|
223
|
+
result.warnings.push('Layout props should include "children"');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ModuleEntry {
|
|
228
|
+
path: string;
|
|
229
|
+
parsed: ParsedPath;
|
|
230
|
+
validation: ValidationResult | null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getTypeDeclaration(kind: ModuleKind) {
|
|
234
|
+
switch (kind) {
|
|
235
|
+
case "server":
|
|
236
|
+
return "(request: Request, params: Record<string, string>, env: Env) => Response | Promise<Response> | Record<string, unknown> | Promise<Record<string, unknown>>";
|
|
237
|
+
case "client":
|
|
238
|
+
return '(props: any) => import("preact").VNode';
|
|
239
|
+
case "layout":
|
|
240
|
+
return '(props: { children: import("preact").VNode }) => import("preact").VNode';
|
|
241
|
+
case "error":
|
|
242
|
+
return '(props: { error: Error; url?: URL; statusCode?: number; reset?: () => void }) => import("preact").VNode';
|
|
243
|
+
default:
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function generateTypedModulesFile(entries: ModuleEntry[]) {
|
|
249
|
+
const errors: string[] = [];
|
|
250
|
+
|
|
251
|
+
// Group by kind
|
|
252
|
+
const serverModules = entries.filter((e) => e.parsed.kind === "server");
|
|
253
|
+
const clientModules = entries.filter((e) => e.parsed.kind === "client");
|
|
254
|
+
const layoutModules = entries.filter((e) => e.parsed.kind === "layout");
|
|
255
|
+
const errorModule = entries.find((e) => e.parsed.kind === "error");
|
|
256
|
+
|
|
257
|
+
// Check for validation errors
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
if (entry.validation && !entry.validation.valid) {
|
|
260
|
+
for (const error of entry.validation.errors) {
|
|
261
|
+
errors.push(`${entry.path}: ${error}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Import paths are relative from dist/ to src/
|
|
267
|
+
const generateEntries = (modules: ModuleEntry[]) =>
|
|
268
|
+
modules
|
|
269
|
+
.map((m) => ` './${m.parsed.normalized}': () => import('../src/${m.parsed.normalized}')`)
|
|
270
|
+
.join(",\n");
|
|
271
|
+
|
|
272
|
+
const errorEntry = errorModule
|
|
273
|
+
? `() => import('../src/${errorModule.parsed.normalized}')`
|
|
274
|
+
: "undefined";
|
|
275
|
+
|
|
276
|
+
const content = `/**
|
|
277
|
+
* Auto-generated route modules
|
|
278
|
+
* Pre-resolved imports for Cloudflare Workers compatibility
|
|
279
|
+
*
|
|
280
|
+
* Module types validated via AST analysis:
|
|
281
|
+
* - Server modules: ${serverModules.length}
|
|
282
|
+
* - Client modules: ${clientModules.length}
|
|
283
|
+
* - Layout modules: ${layoutModules.length}
|
|
284
|
+
* - Error module: ${errorModule ? "yes" : "no"}
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
type ServerLoader = ${getTypeDeclaration("server")}
|
|
288
|
+
type ClientComponent = ${getTypeDeclaration("client")}
|
|
289
|
+
type LayoutComponent = ${getTypeDeclaration("layout")}
|
|
290
|
+
type ErrorComponent = ${getTypeDeclaration("error")}
|
|
291
|
+
|
|
292
|
+
interface ModuleMap {
|
|
293
|
+
server: Record<string, () => Promise<{ default: ServerLoader }>>
|
|
294
|
+
client: Record<string, () => Promise<{ default: ClientComponent }>>
|
|
295
|
+
layout: Record<string, () => Promise<{ default: LayoutComponent }>>
|
|
296
|
+
error?: () => Promise<{ default: ErrorComponent }>
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const modules: ModuleMap = {
|
|
300
|
+
server: {
|
|
301
|
+
${generateEntries(serverModules)}
|
|
302
|
+
},
|
|
303
|
+
client: {
|
|
304
|
+
${generateEntries(clientModules)}
|
|
305
|
+
},
|
|
306
|
+
layout: {
|
|
307
|
+
${generateEntries(layoutModules)}
|
|
308
|
+
},
|
|
309
|
+
error: ${errorEntry},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default modules
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
return { content, errors };
|
|
316
|
+
}
|