@fiyuu/runtime 0.1.0 → 0.1.1

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 (45) hide show
  1. package/package.json +5 -5
  2. package/src/bundler.ts +151 -0
  3. package/src/cli.ts +32 -0
  4. package/src/{client-runtime.js → client-runtime.ts} +3 -2
  5. package/src/inspector.ts +329 -0
  6. package/src/{server-devtools.js → server-devtools.ts} +22 -11
  7. package/src/server-loader.ts +213 -0
  8. package/src/server-middleware.ts +71 -0
  9. package/src/server-renderer.ts +260 -0
  10. package/src/server-router.ts +77 -0
  11. package/src/server-types.ts +198 -0
  12. package/src/server-utils.ts +137 -0
  13. package/src/server-websocket.ts +71 -0
  14. package/src/server.ts +1089 -0
  15. package/src/service.ts +97 -0
  16. package/LICENSE +0 -674
  17. package/README.md +0 -194
  18. package/src/bundler.d.ts +0 -9
  19. package/src/bundler.js +0 -124
  20. package/src/cli.d.ts +0 -2
  21. package/src/cli.js +0 -25
  22. package/src/client-runtime.d.ts +0 -15
  23. package/src/index.js +0 -4
  24. package/src/inspector.d.ts +0 -38
  25. package/src/inspector.js +0 -261
  26. package/src/server-devtools.d.ts +0 -13
  27. package/src/server-loader.d.ts +0 -26
  28. package/src/server-loader.js +0 -158
  29. package/src/server-middleware.d.ts +0 -7
  30. package/src/server-middleware.js +0 -49
  31. package/src/server-renderer.d.ts +0 -34
  32. package/src/server-renderer.js +0 -213
  33. package/src/server-router.d.ts +0 -14
  34. package/src/server-router.js +0 -67
  35. package/src/server-types.d.ts +0 -167
  36. package/src/server-types.js +0 -5
  37. package/src/server-utils.d.ts +0 -15
  38. package/src/server-utils.js +0 -97
  39. package/src/server-websocket.d.ts +0 -7
  40. package/src/server-websocket.js +0 -55
  41. package/src/server.d.ts +0 -68
  42. package/src/server.js +0 -779
  43. package/src/service.d.ts +0 -28
  44. package/src/service.js +0 -71
  45. /package/src/{index.d.ts → index.ts} +0 -0
package/README.md DELETED
@@ -1,194 +0,0 @@
1
- # Fiyuu
2
-
3
- Fiyuu is an **AI-native fullstack framework** built on GEA.
4
- It makes app structure deterministic and exports machine-readable artifacts so both developers and AI tools can work with the same reliable context.
5
-
6
- ## What problem does Fiyuu solve?
7
-
8
- Routing and rendering are already solved by strong frameworks.
9
- Fiyuu focuses on a different bottleneck: **AI and humans often misread intent in large, fast-changing codebases**.
10
-
11
- Fiyuu enforces fixed route contracts (`page.tsx`, `query.ts`, `action.ts`, `schema.ts`, `meta.ts`) and generates `.fiyuu/graph.json` plus AI docs (`PROJECT.md`, `PATHS.md`, `EXECUTION.md`, and more). This reduces guesswork in generation, refactors, and review flows.
12
-
13
- ## Why Fiyuu?
14
-
15
- - **AI-native project context** — `fiyuu sync` exports graph + AI docs from real app structure
16
- - **Deterministic fullstack contracts** — fixed file conventions reduce hidden behavior and drift
17
- - **GEA-first runtime** — app route code stays React-free at the framework layer
18
- - **Built-in diagnostics** — `fiyuu doctor` validates structure and common anti-patterns
19
- - **AI assistant bridge** — `fiyuu ai "<prompt>"` prints route-aware context for external LLM workflows
20
-
21
- ## Measurable differentiation
22
-
23
- Fiyuu tracks performance and DX scorecards by release.
24
-
25
- | Metric | How to measure | Current (v0.1.x) | Target (v0.2) |
26
- | --- | --- | --- | --- |
27
- | Cold build time | `time npm run build` | Baseline pending | >= 20% better on reference app profile |
28
- | SSR latency (avg/p95) | `npm run benchmark:gea` | Baseline pending | >= 15% lower p95 on reference profile |
29
- | Client JS bundle size | `npm run benchmark:gea` (bundle output) | Baseline pending | >= 20% smaller on reference profile |
30
- | AI context readiness time | `time fiyuu sync` | Baseline pending | <= 1s for 100-route reference app |
31
-
32
- Until public scorecards are published, treat Fiyuu as an early-stage framework.
33
-
34
- ## Performance and benchmark tooling
35
-
36
- Fiyuu uses Node.js native HTTP server (no Express). Client assets are bundled with esbuild. SSG routes are cached in memory with optional `meta.revalidate` (ISR-style TTL). Query results support TTL caching with in-flight de-duplication. Navigation responses and HTML support ETag/304, and client navigation prefetches links on hover/focus/viewport.
37
-
38
- For app-layer UI performance, `fiyuu/client` also provides `optimizedImage`, `optimizedVideo`, and responsive helpers (`responsiveStyle`, `mediaUp`, `fluid`, etc.) so teams can ship faster pages without adding heavy UI runtime dependencies.
39
-
40
- ## Built-in Database (FiyuuDB)
41
-
42
- Fiyuu includes a lightweight, always-in-memory database with SQL-like query support:
43
-
44
- ```typescript
45
- // In query.ts or action.ts
46
- import { db } from "@fiyuu/db";
47
-
48
- // SQL-like queries
49
- const users = await db.query("SELECT * FROM users WHERE age > ? AND status = ?", [18, "active"]);
50
- await db.query("INSERT INTO users (name, email) VALUES (?, ?)", ["Ali", "ali@test.com"]);
51
- await db.query("UPDATE users SET status = ? WHERE id = ?", ["inactive", "u_123"]);
52
-
53
- // Table API
54
- const table = db.table("users");
55
- const admins = table.find({ role: "admin" });
56
- const one = table.findOne({ email: "a@b.com" });
57
- table.insert({ name: "Ahmet", email: "ahmet@test.com" });
58
- ```
59
-
60
- ## Real-time Channels
61
-
62
- Fiyuu provides built-in real-time communication via WebSocket and NATS:
63
-
64
- ```typescript
65
- // Server-side (app/services/realtime-sync.ts)
66
- import { defineService } from "@fiyuu/runtime";
67
- import { realtime } from "@fiyuu/realtime";
68
-
69
- export default defineService({
70
- name: "realtime-sync",
71
- start({ realtime, db }) {
72
- const chat = realtime.channel("chat");
73
- chat.on("message", async (data, socket) => {
74
- chat.broadcast("new-message", { text: data.text, user: socket.userId });
75
- await db.query("INSERT INTO messages (text, user) VALUES (?, ?)", [data.text, socket.userId]);
76
- });
77
- },
78
- });
79
- ```
80
-
81
- ```html
82
- <!-- Client-side -->
83
- <script>
84
- const chat = fiyuu.channel("chat");
85
- chat.on("new-message", (data) => console.log(data));
86
- chat.emit("message", { text: "Hello!" });
87
- </script>
88
- ```
89
-
90
- ## Service-based Lifecycle (Always-Alive App)
91
-
92
- Unlike Next.js (request-driven), Fiyuu apps stay alive continuously with background services:
93
-
94
- ```typescript
95
- // app/services/data-sync.ts
96
- import { defineService } from "@fiyuu/runtime";
97
-
98
- export default defineService({
99
- name: "data-sync",
100
- async start({ db, realtime, config, log }) {
101
- // Runs on boot, continuously in background
102
- setInterval(async () => {
103
- const stats = await db.query("SELECT COUNT(*) as c FROM users WHERE active = 1");
104
- realtime.channel("stats").emit("update", stats[0]);
105
- }, 30000);
106
- },
107
- async stop({ log }) {
108
- // Cleanup on shutdown
109
- },
110
- });
111
- ```
112
-
113
- Run benchmark:
114
-
115
- ```bash
116
- npm run benchmark:gea
117
- npm run benchmark:scorecard
118
- ```
119
-
120
- This reports per-route latency (`avg`, `p50`, `p95`, `min`, `max`) and total client bundle size.
121
- The scorecard command also records build/sync/doctor outputs into `docs/benchmarks/latest-scorecard.md`.
122
-
123
- ## Current scope (v2 direction)
124
-
125
- - Primary: **AI-first routing framework** with deterministic contracts
126
- - Shipping priority: graph tooling, diagnostics, and SSR + cache primitives
127
- - Secondary: broader adapters, plugin ecosystem depth, CSR/SSG parity
128
-
129
- ## Competitive snapshot
130
-
131
- Fiyuu is not positioned as a full replacement for Next.js, Nuxt, or Astro today.
132
- It is positioned as an AI-native framework workflow where deterministic graph context is a first-class feature.
133
-
134
- - Ecosystem breadth: behind mature frameworks (current reality)
135
- - AI-readable architecture context: core investment area
136
- - Public benchmark scorecards: in progress (`docs/benchmark-matrix.md`)
137
-
138
- ## Use cases
139
-
140
- - **AI-assisted teams** using Copilot, Cursor, or local LLM pipelines
141
- - **React-free app layer** teams that prefer explicit route contracts
142
- - **Internal tools and dashboards** where deterministic structure matters more than maximal abstraction
143
-
144
- ## Quick Start
145
-
146
- ```bash
147
- npm create fiyuu-app@latest my-app
148
- cd my-app
149
- npm install
150
- npm run dev
151
- ```
152
-
153
- ## Useful commands
154
-
155
- ```bash
156
- fiyuu dev
157
- fiyuu build
158
- fiyuu start
159
- fiyuu deploy
160
- fiyuu cloud help
161
- fiyuu cloud login <token> --endpoint https://api.fiyuu.work
162
- fiyuu cloud project create mysite
163
- fiyuu cloud deploy mysite
164
- fiyuu sync
165
- fiyuu doctor
166
- fiyuu doctor --fix
167
- fiyuu graph stats
168
- fiyuu graph export --format markdown --out docs/graph.md
169
- fiyuu ai "explain route dependencies for /requests"
170
- fiyuu skill list
171
- fiyuu skill run seo-baseline
172
- fiyuu feat list
173
- fiyuu feat socket on
174
- fiyuu feat socket off
175
- ```
176
-
177
- ## Default starter
178
-
179
- - One-page home layout
180
- - Optional feature selection during setup (interactive multi-select)
181
- - Optional light/dark theme toggle with localStorage persistence
182
- - Built-in `app/not-found.tsx` and `app/error.tsx`
183
-
184
- ## Documentation
185
-
186
- - English: `docs/en.md`
187
- - Turkish: `docs/tr.md`
188
- - Skills (EN): `docs/skills.md`
189
- - Skills (TR): `docs/skills.tr.md`
190
- - v2 Product Spec (TR): `docs/v2-product-spec.tr.md`
191
- - Benchmark Matrix: `docs/benchmark-matrix.md`
192
- - Benchmarks Folder: `docs/benchmarks/README.md`
193
- - AI Demo Walkthrough: `docs/ai-demo.md`
194
- - AI-for-Framework Guide: `docs/ai-for-framework.md`
package/src/bundler.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { FeatureRecord, RenderMode } from "@fiyuu/core";
2
- export interface ClientAsset {
3
- route: string;
4
- feature: string;
5
- render: RenderMode;
6
- bundleFile: string;
7
- publicPath: string;
8
- }
9
- export declare function bundleClient(features: FeatureRecord[], outputDirectory: string): Promise<ClientAsset[]>;
package/src/bundler.js DELETED
@@ -1,124 +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
- const buildCache = new Map();
6
- export async function bundleClient(features, outputDirectory) {
7
- await fs.mkdir(outputDirectory, { recursive: true });
8
- const pageFeatures = features.filter((feature) => feature.files["page.tsx"] && feature.render === "csr");
9
- const assets = await Promise.all(pageFeatures.map(async (feature) => {
10
- const safeFeatureName = feature.feature.length > 0 ? feature.feature.replaceAll("/", "_") : "home";
11
- const pageFile = feature.files["page.tsx"];
12
- const layoutFiles = resolveLayoutFiles(feature, pageFile);
13
- const signature = await createBuildSignature([pageFile, ...layoutFiles]);
14
- const signatureHash = createHash("sha1").update(signature).digest("hex").slice(0, 10);
15
- const bundleName = `${safeFeatureName}.${signatureHash}.js`;
16
- const bundleFile = path.join(outputDirectory, bundleName);
17
- const cacheKey = feature.route;
18
- const publicPath = `/__fiyuu/client/${bundleName}`;
19
- const cached = buildCache.get(cacheKey);
20
- if (cached && cached.signature === signature && existsSync(cached.asset.bundleFile)) {
21
- return {
22
- ...cached.asset,
23
- render: feature.render,
24
- };
25
- }
26
- await build({
27
- stdin: {
28
- contents: createClientEntry(pageFile, layoutFiles),
29
- resolveDir: path.dirname(pageFile),
30
- sourcefile: `${feature.feature}-client.tsx`,
31
- loader: "tsx",
32
- },
33
- bundle: true,
34
- format: "esm",
35
- jsx: "automatic",
36
- jsxImportSource: "@geajs/core",
37
- minify: true,
38
- outfile: bundleFile,
39
- platform: "browser",
40
- sourcemap: false,
41
- target: ["es2022"],
42
- });
43
- const asset = {
44
- route: feature.route,
45
- feature: feature.feature,
46
- render: feature.render,
47
- bundleFile,
48
- publicPath,
49
- };
50
- if (cached && cached.asset.bundleFile !== asset.bundleFile && existsSync(cached.asset.bundleFile)) {
51
- try {
52
- await fs.unlink(cached.asset.bundleFile);
53
- }
54
- catch {
55
- // ignore stale artifact cleanup failures
56
- }
57
- }
58
- buildCache.set(cacheKey, { signature, asset });
59
- return asset;
60
- }));
61
- return assets;
62
- }
63
- async function createBuildSignature(filePaths) {
64
- const signatures = await Promise.all(filePaths.map(async (filePath) => {
65
- const stats = await fs.stat(filePath);
66
- return `${filePath}:${stats.size}:${Math.floor(stats.mtimeMs)}`;
67
- }));
68
- return signatures.join("|");
69
- }
70
- function createClientEntry(pageFile, layoutFiles) {
71
- const layoutImports = layoutFiles
72
- .map((layoutFile, index) => `import * as LayoutModule${index} from ${JSON.stringify(layoutFile)};`)
73
- .join("\n");
74
- const layoutWrappers = layoutFiles
75
- .map((_, index) => `const Layout${index} = LayoutModule${index}.default; if (Layout${index}) { const wrapped = new Layout${index}({ route, children: String(component) }); component = wrapped; }`)
76
- .reverse()
77
- .join("\n ");
78
- return `
79
- import { Component } from "@geajs/core";
80
- import Page from ${JSON.stringify(pageFile)};
81
- ${layoutImports}
82
-
83
- const data = window.__FIYUU_DATA__ ?? null;
84
- const route = window.__FIYUU_ROUTE__ ?? "/";
85
- const intent = window.__FIYUU_INTENT__ ?? "";
86
- const render = window.__FIYUU_RENDER__ ?? "csr";
87
- const rootElement = document.getElementById("app");
88
- const pageProps = { data, route, intent, render };
89
- if (!(Page && Page.prototype instanceof Component)) {
90
- throw new Error("Fiyuu Gea mode expects page default export to extend @geajs/core Component.");
91
- }
92
- let component = new Page(pageProps);
93
- ${layoutWrappers}
94
-
95
- if (rootElement) {
96
- rootElement.innerHTML = "";
97
- component.render(rootElement);
98
-
99
- // Re-execute any <script> tags injected by the component.
100
- // innerHTML assignment does not execute scripts — this is a browser security rule.
101
- // We collect all script tags and recreate them so they run normally.
102
- const injectedScripts = rootElement.querySelectorAll("script");
103
- for (const oldScript of injectedScripts) {
104
- const newScript = document.createElement("script");
105
- for (const attr of oldScript.attributes) {
106
- newScript.setAttribute(attr.name, attr.value);
107
- }
108
- newScript.textContent = oldScript.textContent;
109
- oldScript.parentNode?.replaceChild(newScript, oldScript);
110
- }
111
- }
112
- `;
113
- }
114
- function resolveLayoutFiles(feature, pageFile) {
115
- const featureParts = feature.feature ? feature.feature.split("/") : [];
116
- const featureDirectory = path.dirname(pageFile);
117
- const appDirectory = featureParts.length > 0
118
- ? path.resolve(featureDirectory, ...Array(featureParts.length).fill(".."))
119
- : featureDirectory;
120
- const directories = [appDirectory, ...featureParts.map((_, index) => path.join(appDirectory, ...featureParts.slice(0, index + 1)))];
121
- return directories
122
- .map((directory) => path.join(directory, "layout.tsx"))
123
- .filter((layoutPath) => existsSync(layoutPath));
124
- }
package/src/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/src/cli.js DELETED
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env node
2
- import path from "node:path";
3
- import { existsSync } from "node:fs";
4
- import { scanApp } from "@fiyuu/core";
5
- import { bundleClient } from "./bundler.js";
6
- async function main() {
7
- const [, , command, rootDirectory] = process.argv;
8
- if (command !== "bundle" || !rootDirectory) {
9
- throw new Error("Usage: runtime bundle <rootDirectory>");
10
- }
11
- const appDirectory = resolveAppDirectory(rootDirectory);
12
- const features = await scanApp(appDirectory);
13
- const outputDirectory = path.join(rootDirectory, ".fiyuu", "client");
14
- await bundleClient(features, outputDirectory);
15
- console.log(`Bundled client assets to ${outputDirectory}`);
16
- }
17
- function resolveAppDirectory(rootDirectory) {
18
- const rootAppDirectory = path.join(rootDirectory, "app");
19
- const exampleAppDirectory = path.join(rootDirectory, "examples", "basic-app", "app");
20
- return existsSync(rootAppDirectory) ? rootAppDirectory : exampleAppDirectory;
21
- }
22
- main().catch((error) => {
23
- console.error(error instanceof Error ? error.message : "Unknown bundle error");
24
- process.exitCode = 1;
25
- });
@@ -1,15 +0,0 @@
1
- /**
2
- * Fiyuu Client Runtime
3
- *
4
- * A small script injected into every page that provides:
5
- * - fiyuu.theme — dark/light mode management
6
- * - fiyuu.state — simple reactive state with DOM binding
7
- * - fiyuu.bind — shorthand for updating element text / html
8
- * - fiyuu.router — client-side navigation without page reload
9
- * - fiyuu.partial — replace a DOM element with a fetched route's content
10
- * - fiyuu.onError — global client-side error handler
11
- * - fiyuu.ws — WebSocket connection helper
12
- *
13
- * Everything is accessible via window.fiyuu in page scripts.
14
- */
15
- export declare function buildClientRuntime(websocketPath: string): string;
package/src/index.js DELETED
@@ -1,4 +0,0 @@
1
- export * from "./server.js";
2
- export * from "./bundler.js";
3
- export * from "./inspector.js";
4
- export * from "./service.js";
@@ -1,38 +0,0 @@
1
- import type { FeatureRecord, FiyuuConfig } from "@fiyuu/core";
2
- type InsightCategory = "security" | "performance" | "design" | "architecture";
3
- type InsightSeverity = "low" | "medium" | "high";
4
- export interface InsightItem {
5
- id: string;
6
- category: InsightCategory;
7
- severity: InsightSeverity;
8
- title: string;
9
- summary: string;
10
- recommendation: string;
11
- route?: string;
12
- file?: string;
13
- fixable: boolean;
14
- }
15
- export interface InsightsReport {
16
- generatedAt: string;
17
- summary: {
18
- total: number;
19
- high: number;
20
- medium: number;
21
- low: number;
22
- };
23
- items: InsightItem[];
24
- assistant: {
25
- mode: "rule-only";
26
- status: "ready";
27
- details: string;
28
- suggestions: string[];
29
- };
30
- }
31
- interface BuildInsightsOptions {
32
- rootDirectory: string;
33
- appDirectory: string;
34
- features: FeatureRecord[];
35
- config?: FiyuuConfig;
36
- }
37
- export declare function buildInsightsReport(options: BuildInsightsOptions): Promise<InsightsReport>;
38
- export {};
package/src/inspector.js DELETED
@@ -1,261 +0,0 @@
1
- import { promises as fs, existsSync } from "node:fs";
2
- import path from "node:path";
3
- export async function buildInsightsReport(options) {
4
- const items = await collectInsightItems(options);
5
- const assistant = await buildAssistantOutput(options, items);
6
- const summary = {
7
- total: items.length,
8
- high: items.filter((item) => item.severity === "high").length,
9
- medium: items.filter((item) => item.severity === "medium").length,
10
- low: items.filter((item) => item.severity === "low").length,
11
- };
12
- return {
13
- generatedAt: new Date().toISOString(),
14
- summary,
15
- items,
16
- assistant,
17
- };
18
- }
19
- async function collectInsightItems(options) {
20
- const items = [];
21
- for (const feature of options.features) {
22
- items.push(...toFeatureStructureInsights(feature));
23
- const fileEntries = Object.entries(feature.files).filter((entry) => Boolean(entry[1]));
24
- for (const [, filePath] of fileEntries) {
25
- const source = await readFileSafe(filePath);
26
- if (!source) {
27
- continue;
28
- }
29
- if (/dangerouslySetInnerHTML/.test(source)) {
30
- items.push({
31
- id: `security-dangerous-html-${filePath}`,
32
- category: "security",
33
- severity: "high",
34
- title: "Potential XSS surface detected",
35
- summary: "`dangerouslySetInnerHTML` is used in a route module.",
36
- recommendation: "Prefer escaped rendering or sanitize content before passing HTML strings.",
37
- route: feature.route,
38
- file: filePath,
39
- fixable: true,
40
- });
41
- }
42
- if (/\beval\s*\(|new\s+Function\s*\(/.test(source)) {
43
- items.push({
44
- id: `security-dynamic-eval-${filePath}`,
45
- category: "security",
46
- severity: "high",
47
- title: "Dynamic code execution detected",
48
- summary: "`eval` or `new Function` appears in application logic.",
49
- recommendation: "Replace dynamic execution with explicit, typed control flow.",
50
- route: feature.route,
51
- file: filePath,
52
- fixable: true,
53
- });
54
- }
55
- const lineCount = source.split(/\r?\n/).length;
56
- if (lineCount > 450) {
57
- items.push({
58
- id: `performance-large-module-${filePath}`,
59
- category: "performance",
60
- severity: "medium",
61
- title: "Large route module",
62
- summary: `Module has ${lineCount} lines and may increase parse and hydration cost.`,
63
- recommendation: "Split heavy route logic into smaller server/client helpers.",
64
- route: feature.route,
65
- file: filePath,
66
- fixable: true,
67
- });
68
- }
69
- }
70
- if (feature.files["meta.ts"]) {
71
- const metaSource = await readFileSafe(feature.files["meta.ts"]);
72
- if (metaSource) {
73
- const seoTitle = extractSeoField(metaSource, "title");
74
- const seoDescription = extractSeoField(metaSource, "description");
75
- if (!seoTitle) {
76
- items.push({
77
- id: `design-missing-seo-title-${feature.route}`,
78
- category: "design",
79
- severity: "low",
80
- title: "SEO title missing in meta",
81
- summary: "`meta.ts` exists but does not define `seo.title`.",
82
- recommendation: "Add route-specific `seo.title` to improve previews and search snippets.",
83
- route: feature.route,
84
- file: feature.files["meta.ts"],
85
- fixable: true,
86
- });
87
- }
88
- else if (seoTitle.length > 62) {
89
- items.push({
90
- id: `design-seo-title-length-${feature.route}`,
91
- category: "design",
92
- severity: "low",
93
- title: "SEO title is longer than recommended",
94
- summary: `Current title length is ${seoTitle.length} characters.`,
95
- recommendation: "Keep seo titles around 45-60 characters to avoid truncation in SERP previews.",
96
- route: feature.route,
97
- file: feature.files["meta.ts"],
98
- fixable: true,
99
- });
100
- }
101
- if (!seoDescription) {
102
- items.push({
103
- id: `design-missing-seo-description-${feature.route}`,
104
- category: "design",
105
- severity: "medium",
106
- title: "SEO description missing in meta",
107
- summary: "`meta.ts` does not define `seo.description`.",
108
- recommendation: "Add a concise description (90-160 chars) for better search and social previews.",
109
- route: feature.route,
110
- file: feature.files["meta.ts"],
111
- fixable: true,
112
- });
113
- }
114
- else if (seoDescription.length < 80 || seoDescription.length > 170) {
115
- items.push({
116
- id: `design-seo-description-length-${feature.route}`,
117
- category: "design",
118
- severity: "low",
119
- title: "SEO description length can be improved",
120
- summary: `Current description length is ${seoDescription.length} characters.`,
121
- recommendation: "Keep seo descriptions in the 90-160 character range for reliable previews.",
122
- route: feature.route,
123
- file: feature.files["meta.ts"],
124
- fixable: true,
125
- });
126
- }
127
- }
128
- }
129
- }
130
- items.push(...(await collectGlobalSecurityInsights(options.appDirectory)));
131
- items.push(...collectGlobalRenderInsights(options.features));
132
- return dedupeInsights(items);
133
- }
134
- function toFeatureStructureInsights(feature) {
135
- const items = [];
136
- for (const missing of feature.missingRequiredFiles) {
137
- items.push({
138
- id: `architecture-missing-${feature.route}-${missing}`,
139
- category: "architecture",
140
- severity: missing === "schema.ts" ? "high" : "medium",
141
- title: `Required file missing: ${missing}`,
142
- summary: `Feature ${feature.route} is missing ${missing}.`,
143
- recommendation: "Complete the feature contract so tooling and AI context stay deterministic.",
144
- route: feature.route,
145
- file: path.join(feature.directory, missing),
146
- fixable: true,
147
- });
148
- }
149
- return items;
150
- }
151
- async function collectGlobalSecurityInsights(appDirectory) {
152
- const items = [];
153
- const middlewarePath = path.join(appDirectory, "middleware.ts");
154
- if (existsSync(middlewarePath)) {
155
- const middlewareSource = await readFileSafe(middlewarePath);
156
- if (middlewareSource && /access-control-allow-origin["']?\s*[:,=]\s*["']\*/i.test(middlewareSource)) {
157
- items.push({
158
- id: "security-open-cors-middleware",
159
- category: "security",
160
- severity: "high",
161
- title: "Wildcard CORS header in middleware",
162
- summary: "Middleware appears to allow all origins globally.",
163
- recommendation: "Restrict allowed origins by environment and keep credentials disabled for public origins.",
164
- file: middlewarePath,
165
- fixable: true,
166
- });
167
- }
168
- }
169
- return items;
170
- }
171
- function collectGlobalRenderInsights(features) {
172
- const csrCount = features.filter((feature) => feature.render === "csr").length;
173
- if (features.length < 4) {
174
- return [];
175
- }
176
- const ratio = csrCount / features.length;
177
- if (ratio < 0.6) {
178
- return [];
179
- }
180
- return [
181
- {
182
- id: "performance-csr-heavy",
183
- category: "performance",
184
- severity: ratio > 0.8 ? "high" : "medium",
185
- title: "Project is CSR-heavy",
186
- summary: `${csrCount}/${features.length} routes render in CSR mode.`,
187
- recommendation: "Move content-driven pages to SSR where possible to reduce blank-first-paint risk.",
188
- fixable: true,
189
- },
190
- ];
191
- }
192
- async function buildAssistantOutput(options, items) {
193
- void options;
194
- return {
195
- mode: "rule-only",
196
- status: "ready",
197
- details: "Using deterministic project checks (no integrated model).",
198
- suggestions: createRuleBasedSuggestions(items),
199
- };
200
- }
201
- function createRuleBasedSuggestions(items) {
202
- if (items.length === 0) {
203
- return [
204
- "Run `fiyuu doctor --fix` after scaffolding new routes to keep contracts deterministic.",
205
- "Keep SEO descriptions in 12-28 words and route-specific titles for stable search snippets.",
206
- "Add focused tests around auth, middleware, and API routes before release builds.",
207
- ];
208
- }
209
- const topItems = items
210
- .slice()
211
- .sort((left, right) => severityScore(right.severity) - severityScore(left.severity))
212
- .slice(0, 4);
213
- return [
214
- ...topItems.map((item) => `${capitalize(item.category)}: ${item.recommendation}`),
215
- "Use `fiyuu doctor --fix` for safe auto-fixes (SEO fields, missing execute(), fallback pages, className->class).",
216
- ].slice(0, 6);
217
- }
218
- function dedupeInsights(items) {
219
- const seen = new Set();
220
- const output = [];
221
- for (const item of items) {
222
- if (seen.has(item.id)) {
223
- continue;
224
- }
225
- seen.add(item.id);
226
- output.push(item);
227
- }
228
- return output;
229
- }
230
- async function readFileSafe(filePath) {
231
- try {
232
- return await fs.readFile(filePath, "utf8");
233
- }
234
- catch {
235
- return null;
236
- }
237
- }
238
- function severityScore(severity) {
239
- if (severity === "high") {
240
- return 3;
241
- }
242
- if (severity === "medium") {
243
- return 2;
244
- }
245
- return 1;
246
- }
247
- function capitalize(value) {
248
- return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
249
- }
250
- function extractSeoField(source, field) {
251
- const seoBlock = source.match(/seo\s*:\s*\{([\s\S]*?)\}/m);
252
- if (!seoBlock) {
253
- return null;
254
- }
255
- const fieldRegex = new RegExp(`${field}\\s*:\\s*(["'\`])([\\s\\S]*?)\\1`, "m");
256
- const match = seoBlock[1].match(fieldRegex);
257
- if (!match) {
258
- return null;
259
- }
260
- return match[2].trim() || null;
261
- }