@fiyuu/runtime 0.2.0 → 0.4.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/README.md +62 -0
- package/package.json +23 -4
- package/src/bundler.ts +0 -151
- package/src/cli.ts +0 -32
- package/src/client-runtime.ts +0 -528
- package/src/index.ts +0 -4
- package/src/inspector.ts +0 -329
- package/src/server-devtools.ts +0 -133
- package/src/server-loader.ts +0 -213
- package/src/server-middleware.ts +0 -71
- package/src/server-renderer.ts +0 -260
- package/src/server-router.ts +0 -77
- package/src/server-types.ts +0 -198
- package/src/server-utils.ts +0 -137
- package/src/server-websocket.ts +0 -71
- package/src/server.ts +0 -1089
- package/src/service.ts +0 -97
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @fiyuu/runtime
|
|
2
|
+
|
|
3
|
+
Server runtime for the Fiyuu framework. Handles HTTP server, routing, rendering, middleware, WebSocket, and background services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fiyuu/runtime
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- HTTP server with file-based routing
|
|
14
|
+
- SSR, CSR, and SSG rendering modes
|
|
15
|
+
- Dynamic route matching (`[id]`, `[...slug]`, `[[...optional]]`)
|
|
16
|
+
- Middleware pipeline
|
|
17
|
+
- WebSocket integration
|
|
18
|
+
- Background service management
|
|
19
|
+
- esbuild-based client bundling
|
|
20
|
+
- Live reload in development
|
|
21
|
+
- Developer tools (unified inspector)
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
server.ts - Main orchestrator
|
|
27
|
+
server-router.ts - Route matching engine
|
|
28
|
+
server-loader.ts - Dynamic module loading & caching
|
|
29
|
+
server-renderer.ts - HTML document & status page rendering
|
|
30
|
+
server-middleware.ts - Middleware chain
|
|
31
|
+
server-websocket.ts - WebSocket setup
|
|
32
|
+
service.ts - Background service lifecycle
|
|
33
|
+
bundler.ts - esbuild client bundling
|
|
34
|
+
client-runtime.ts - Client-side runtime script
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Route Matching
|
|
38
|
+
|
|
39
|
+
Routes are matched in order of specificity:
|
|
40
|
+
|
|
41
|
+
1. Exact matches first (O(1) lookup)
|
|
42
|
+
2. Dynamic segments (`/blog/[id]`)
|
|
43
|
+
3. Catch-all routes (`/docs/[...slug]`)
|
|
44
|
+
4. Optional catch-all (`/files/[[...path]]`)
|
|
45
|
+
|
|
46
|
+
## Rendering Modes
|
|
47
|
+
|
|
48
|
+
Configure per-route in `meta.ts`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
export default defineMeta({
|
|
52
|
+
intent: "User List",
|
|
53
|
+
render: "ssr", // Server-side rendering (default)
|
|
54
|
+
// render: "csr", // Client-side rendering
|
|
55
|
+
// render: "ssg", // Static site generation
|
|
56
|
+
// revalidate: 300, // ISR: revalidate every 5 minutes
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiyuu/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts",
|
|
8
8
|
"./cli": "./src/cli.ts"
|
|
9
9
|
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./cli": {
|
|
19
|
+
"import": "./dist/cli.js",
|
|
20
|
+
"types": "./dist/cli.d.ts"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
10
29
|
"dependencies": {
|
|
11
30
|
"@geajs/core": "^1.0.12",
|
|
12
|
-
"@fiyuu/core": "
|
|
13
|
-
"@fiyuu/db": "
|
|
14
|
-
"@fiyuu/realtime": "
|
|
31
|
+
"@fiyuu/core": "workspace:*",
|
|
32
|
+
"@fiyuu/db": "workspace:*",
|
|
33
|
+
"@fiyuu/realtime": "workspace:*",
|
|
15
34
|
"chokidar": "^4.0.3",
|
|
16
35
|
"esbuild": "^0.25.2",
|
|
17
36
|
"ws": "^8.18.1"
|
package/src/bundler.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { promises as fs, existsSync } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { build } from "esbuild";
|
|
5
|
-
import type { FeatureRecord, RenderMode } from "@fiyuu/core";
|
|
6
|
-
|
|
7
|
-
export interface ClientAsset {
|
|
8
|
-
route: string;
|
|
9
|
-
feature: string;
|
|
10
|
-
render: RenderMode;
|
|
11
|
-
bundleFile: string;
|
|
12
|
-
publicPath: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const buildCache = new Map<string, { signature: string; asset: ClientAsset }>();
|
|
16
|
-
|
|
17
|
-
export async function bundleClient(features: FeatureRecord[], outputDirectory: string): Promise<ClientAsset[]> {
|
|
18
|
-
await fs.mkdir(outputDirectory, { recursive: true });
|
|
19
|
-
|
|
20
|
-
const pageFeatures = features.filter((feature) => feature.files["page.tsx"] && feature.render === "csr");
|
|
21
|
-
const assets = await Promise.all(
|
|
22
|
-
pageFeatures.map(async (feature) => {
|
|
23
|
-
const safeFeatureName = feature.feature.length > 0 ? feature.feature.replaceAll("/", "_") : "home";
|
|
24
|
-
const pageFile = feature.files["page.tsx"]!;
|
|
25
|
-
const layoutFiles = resolveLayoutFiles(feature, pageFile);
|
|
26
|
-
const signature = await createBuildSignature([pageFile, ...layoutFiles]);
|
|
27
|
-
const signatureHash = createHash("sha1").update(signature).digest("hex").slice(0, 10);
|
|
28
|
-
const bundleName = `${safeFeatureName}.${signatureHash}.js`;
|
|
29
|
-
const bundleFile = path.join(outputDirectory, bundleName);
|
|
30
|
-
const cacheKey = feature.route;
|
|
31
|
-
const publicPath = `/__fiyuu/client/${bundleName}`;
|
|
32
|
-
const cached = buildCache.get(cacheKey);
|
|
33
|
-
|
|
34
|
-
if (cached && cached.signature === signature && existsSync(cached.asset.bundleFile)) {
|
|
35
|
-
return {
|
|
36
|
-
...cached.asset,
|
|
37
|
-
render: feature.render,
|
|
38
|
-
} satisfies ClientAsset;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await build({
|
|
42
|
-
stdin: {
|
|
43
|
-
contents: createClientEntry(pageFile, layoutFiles),
|
|
44
|
-
resolveDir: path.dirname(pageFile),
|
|
45
|
-
sourcefile: `${feature.feature}-client.tsx`,
|
|
46
|
-
loader: "tsx",
|
|
47
|
-
},
|
|
48
|
-
bundle: true,
|
|
49
|
-
format: "esm",
|
|
50
|
-
jsx: "automatic",
|
|
51
|
-
jsxImportSource: "@geajs/core",
|
|
52
|
-
minify: true,
|
|
53
|
-
outfile: bundleFile,
|
|
54
|
-
platform: "browser",
|
|
55
|
-
sourcemap: false,
|
|
56
|
-
target: ["es2022"],
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const asset = {
|
|
60
|
-
route: feature.route,
|
|
61
|
-
feature: feature.feature,
|
|
62
|
-
render: feature.render,
|
|
63
|
-
bundleFile,
|
|
64
|
-
publicPath,
|
|
65
|
-
} satisfies ClientAsset;
|
|
66
|
-
|
|
67
|
-
if (cached && cached.asset.bundleFile !== asset.bundleFile && existsSync(cached.asset.bundleFile)) {
|
|
68
|
-
try {
|
|
69
|
-
await fs.unlink(cached.asset.bundleFile);
|
|
70
|
-
} catch {
|
|
71
|
-
// ignore stale artifact cleanup failures
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
buildCache.set(cacheKey, { signature, asset });
|
|
76
|
-
return asset;
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return assets;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function createBuildSignature(filePaths: string[]): Promise<string> {
|
|
84
|
-
const signatures = await Promise.all(
|
|
85
|
-
filePaths.map(async (filePath) => {
|
|
86
|
-
const stats = await fs.stat(filePath);
|
|
87
|
-
return `${filePath}:${stats.size}:${Math.floor(stats.mtimeMs)}`;
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
return signatures.join("|");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function createClientEntry(pageFile: string, layoutFiles: string[]): string {
|
|
95
|
-
const layoutImports = layoutFiles
|
|
96
|
-
.map((layoutFile, index) => `import * as LayoutModule${index} from ${JSON.stringify(layoutFile)};`)
|
|
97
|
-
.join("\n");
|
|
98
|
-
const layoutWrappers = layoutFiles
|
|
99
|
-
.map((_, index) => `const Layout${index} = LayoutModule${index}.default; if (Layout${index}) { const wrapped = new Layout${index}({ route, children: String(component) }); component = wrapped; }`)
|
|
100
|
-
.reverse()
|
|
101
|
-
.join("\n ");
|
|
102
|
-
|
|
103
|
-
return `
|
|
104
|
-
import { Component } from "@geajs/core";
|
|
105
|
-
import Page from ${JSON.stringify(pageFile)};
|
|
106
|
-
${layoutImports}
|
|
107
|
-
|
|
108
|
-
const data = window.__FIYUU_DATA__ ?? null;
|
|
109
|
-
const route = window.__FIYUU_ROUTE__ ?? "/";
|
|
110
|
-
const intent = window.__FIYUU_INTENT__ ?? "";
|
|
111
|
-
const render = window.__FIYUU_RENDER__ ?? "csr";
|
|
112
|
-
const rootElement = document.getElementById("app");
|
|
113
|
-
const pageProps = { data, route, intent, render };
|
|
114
|
-
if (!(Page && Page.prototype instanceof Component)) {
|
|
115
|
-
throw new Error("Fiyuu Gea mode expects page default export to extend @geajs/core Component.");
|
|
116
|
-
}
|
|
117
|
-
let component = new Page(pageProps);
|
|
118
|
-
${layoutWrappers}
|
|
119
|
-
|
|
120
|
-
if (rootElement) {
|
|
121
|
-
rootElement.innerHTML = "";
|
|
122
|
-
component.render(rootElement);
|
|
123
|
-
|
|
124
|
-
// Re-execute any <script> tags injected by the component.
|
|
125
|
-
// innerHTML assignment does not execute scripts — this is a browser security rule.
|
|
126
|
-
// We collect all script tags and recreate them so they run normally.
|
|
127
|
-
const injectedScripts = rootElement.querySelectorAll("script");
|
|
128
|
-
for (const oldScript of injectedScripts) {
|
|
129
|
-
const newScript = document.createElement("script");
|
|
130
|
-
for (const attr of oldScript.attributes) {
|
|
131
|
-
newScript.setAttribute(attr.name, attr.value);
|
|
132
|
-
}
|
|
133
|
-
newScript.textContent = oldScript.textContent;
|
|
134
|
-
oldScript.parentNode?.replaceChild(newScript, oldScript);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function resolveLayoutFiles(feature: FeatureRecord, pageFile: string): string[] {
|
|
141
|
-
const featureParts = feature.feature ? feature.feature.split("/") : [];
|
|
142
|
-
const featureDirectory = path.dirname(pageFile);
|
|
143
|
-
const appDirectory = featureParts.length > 0
|
|
144
|
-
? path.resolve(featureDirectory, ...Array(featureParts.length).fill(".."))
|
|
145
|
-
: featureDirectory;
|
|
146
|
-
const directories = [appDirectory, ...featureParts.map((_, index) => path.join(appDirectory, ...featureParts.slice(0, index + 1)))];
|
|
147
|
-
|
|
148
|
-
return directories
|
|
149
|
-
.map((directory) => path.join(directory, "layout.tsx"))
|
|
150
|
-
.filter((layoutPath) => existsSync(layoutPath));
|
|
151
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { scanApp } from "@fiyuu/core";
|
|
6
|
-
import { bundleClient } from "./bundler.js";
|
|
7
|
-
|
|
8
|
-
async function main(): Promise<void> {
|
|
9
|
-
const [, , command, rootDirectory] = process.argv;
|
|
10
|
-
|
|
11
|
-
if (command !== "bundle" || !rootDirectory) {
|
|
12
|
-
throw new Error("Usage: runtime bundle <rootDirectory>");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const appDirectory = resolveAppDirectory(rootDirectory);
|
|
16
|
-
const features = await scanApp(appDirectory);
|
|
17
|
-
const outputDirectory = path.join(rootDirectory, ".fiyuu", "client");
|
|
18
|
-
await bundleClient(features, outputDirectory);
|
|
19
|
-
console.log(`Bundled client assets to ${outputDirectory}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function resolveAppDirectory(rootDirectory: string): string {
|
|
23
|
-
const rootAppDirectory = path.join(rootDirectory, "app");
|
|
24
|
-
const exampleAppDirectory = path.join(rootDirectory, "examples", "basic-app", "app");
|
|
25
|
-
|
|
26
|
-
return existsSync(rootAppDirectory) ? rootAppDirectory : exampleAppDirectory;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
main().catch((error: unknown) => {
|
|
30
|
-
console.error(error instanceof Error ? error.message : "Unknown bundle error");
|
|
31
|
-
process.exitCode = 1;
|
|
32
|
-
});
|