@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 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.2.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": "0.2.0",
13
- "@fiyuu/db": "0.3.0",
14
- "@fiyuu/realtime": "0.3.0",
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
- });