@fiyuu/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/LICENSE +674 -0
- package/README.md +194 -0
- package/package.json +17 -0
- package/src/artifacts.d.ts +20 -0
- package/src/artifacts.js +274 -0
- package/src/client.d.ts +8 -0
- package/src/client.js +8 -0
- package/src/config.d.ts +179 -0
- package/src/config.js +58 -0
- package/src/contracts.d.ts +176 -0
- package/src/contracts.js +45 -0
- package/src/generator.d.ts +2 -0
- package/src/generator.js +124 -0
- package/src/index.d.ts +11 -0
- package/src/index.js +11 -0
- package/src/media.d.ts +44 -0
- package/src/media.js +87 -0
- package/src/reactive.d.ts +53 -0
- package/src/reactive.js +160 -0
- package/src/responsive-wrapper.d.ts +18 -0
- package/src/responsive-wrapper.js +285 -0
- package/src/responsive.d.ts +15 -0
- package/src/responsive.js +48 -0
- package/src/scanner.d.ts +65 -0
- package/src/scanner.js +200 -0
- package/src/state.d.ts +1 -0
- package/src/state.js +1 -0
- package/src/template.d.ts +48 -0
- package/src/template.js +98 -0
- package/src/virtual.d.ts +14 -0
- package/src/virtual.js +21 -0
package/src/config.d.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export interface FiyuuConfig {
|
|
2
|
+
app?: {
|
|
3
|
+
name?: string;
|
|
4
|
+
runtime?: "node" | "bun";
|
|
5
|
+
port?: number;
|
|
6
|
+
};
|
|
7
|
+
ai?: {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
skillsDirectory?: string;
|
|
10
|
+
defaultSkills?: string[];
|
|
11
|
+
graphContext?: boolean;
|
|
12
|
+
inspector?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
localModelCommand?: string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
autoSetupPrompt?: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
fullstack?: {
|
|
20
|
+
client?: boolean;
|
|
21
|
+
serverActions?: boolean;
|
|
22
|
+
serverQueries?: boolean;
|
|
23
|
+
sockets?: boolean;
|
|
24
|
+
};
|
|
25
|
+
data?: {
|
|
26
|
+
driver?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
autosave?: boolean;
|
|
29
|
+
autosaveIntervalMs?: number;
|
|
30
|
+
tables?: string[];
|
|
31
|
+
};
|
|
32
|
+
security?: {
|
|
33
|
+
requestEncryption?: boolean;
|
|
34
|
+
serverSecretFile?: string;
|
|
35
|
+
};
|
|
36
|
+
websocket?: {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
path?: string;
|
|
39
|
+
heartbeatMs?: number;
|
|
40
|
+
maxPayloadBytes?: number;
|
|
41
|
+
};
|
|
42
|
+
realtime?: {
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
transports?: ("websocket" | "nats")[];
|
|
45
|
+
websocket?: {
|
|
46
|
+
path?: string;
|
|
47
|
+
heartbeatMs?: number;
|
|
48
|
+
maxPayloadBytes?: number;
|
|
49
|
+
};
|
|
50
|
+
nats?: {
|
|
51
|
+
url?: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
services?: {
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
directory?: string;
|
|
58
|
+
failFast?: boolean;
|
|
59
|
+
};
|
|
60
|
+
middleware?: {
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
};
|
|
63
|
+
developerTools?: {
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
renderTiming?: boolean;
|
|
66
|
+
};
|
|
67
|
+
observability?: {
|
|
68
|
+
requestId?: boolean;
|
|
69
|
+
warningsAsOverlay?: boolean;
|
|
70
|
+
};
|
|
71
|
+
auth?: {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
sessionStrategy?: "cookie" | "token";
|
|
74
|
+
};
|
|
75
|
+
analytics?: {
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
provider?: string;
|
|
78
|
+
};
|
|
79
|
+
featureFlags?: {
|
|
80
|
+
enabled?: boolean;
|
|
81
|
+
defaults?: Record<string, boolean>;
|
|
82
|
+
};
|
|
83
|
+
errors?: {
|
|
84
|
+
/**
|
|
85
|
+
* Called for every unhandled server error.
|
|
86
|
+
* Use this to send errors to Sentry, Datadog, etc.
|
|
87
|
+
*/
|
|
88
|
+
handler?: (error: Error, context: {
|
|
89
|
+
route: string;
|
|
90
|
+
method: string;
|
|
91
|
+
requestId: string;
|
|
92
|
+
}) => void | Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Whether to expose error details (stack trace, message) in responses.
|
|
95
|
+
* Defaults to true in dev, false in production.
|
|
96
|
+
*/
|
|
97
|
+
expose?: boolean;
|
|
98
|
+
};
|
|
99
|
+
deploy?: {
|
|
100
|
+
/**
|
|
101
|
+
* Enables `fiyuu deploy` workflow.
|
|
102
|
+
* If undefined, deploy command still works when `deploy.ssh` is configured.
|
|
103
|
+
*/
|
|
104
|
+
enabled?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Build locally before upload. Defaults to true.
|
|
107
|
+
*/
|
|
108
|
+
localBuild?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Extra archive exclude patterns for `tar`.
|
|
111
|
+
*/
|
|
112
|
+
excludes?: string[];
|
|
113
|
+
ssh?: {
|
|
114
|
+
host: string;
|
|
115
|
+
user: string;
|
|
116
|
+
port?: number;
|
|
117
|
+
privateKeyPath?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Absolute path on remote server (for example: /var/www/my-app)
|
|
120
|
+
*/
|
|
121
|
+
destinationPath: string;
|
|
122
|
+
/**
|
|
123
|
+
* Number of old releases to keep on the server.
|
|
124
|
+
*/
|
|
125
|
+
keepReleases?: number;
|
|
126
|
+
};
|
|
127
|
+
remote?: {
|
|
128
|
+
/**
|
|
129
|
+
* Command executed on remote server in release directory.
|
|
130
|
+
*/
|
|
131
|
+
installCommand?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Command executed on remote server after install.
|
|
134
|
+
*/
|
|
135
|
+
buildCommand?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Command executed on remote server to start/reload the app.
|
|
138
|
+
*/
|
|
139
|
+
startCommand?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Optional validation command after start.
|
|
142
|
+
*/
|
|
143
|
+
healthcheckCommand?: string;
|
|
144
|
+
};
|
|
145
|
+
pm2?: {
|
|
146
|
+
enabled?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Auto-generate ecosystem file in project root if missing.
|
|
149
|
+
*/
|
|
150
|
+
autoCreate?: boolean;
|
|
151
|
+
ecosystemFile?: string;
|
|
152
|
+
appName?: string;
|
|
153
|
+
instances?: number | "max";
|
|
154
|
+
execMode?: "fork" | "cluster";
|
|
155
|
+
maxMemoryRestart?: string;
|
|
156
|
+
env?: Record<string, string>;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
cloud?: {
|
|
160
|
+
/**
|
|
161
|
+
* Control-plane API endpoint for `fiyuu cloud`.
|
|
162
|
+
* Example: https://api.fiyuu.work
|
|
163
|
+
*/
|
|
164
|
+
endpoint?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Default project slug used by `fiyuu cloud deploy`.
|
|
167
|
+
*/
|
|
168
|
+
project?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Extra archive exclude patterns used by cloud deploy packaging.
|
|
171
|
+
*/
|
|
172
|
+
excludes?: string[];
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export interface LoadedFiyuuConfig {
|
|
176
|
+
config: FiyuuConfig;
|
|
177
|
+
env: Record<string, string>;
|
|
178
|
+
}
|
|
179
|
+
export declare function loadFiyuuConfig(rootDirectory: string, mode?: "dev" | "start"): Promise<LoadedFiyuuConfig>;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
export async function loadFiyuuConfig(rootDirectory, mode = "dev") {
|
|
6
|
+
const env = await loadFiyuuEnvironment(rootDirectory, mode);
|
|
7
|
+
const configPath = path.join(rootDirectory, "fiyuu.config.ts");
|
|
8
|
+
if (!existsSync(configPath)) {
|
|
9
|
+
applyEnv(env);
|
|
10
|
+
return { config: {}, env };
|
|
11
|
+
}
|
|
12
|
+
const moduleUrl = pathToFileURL(configPath).href;
|
|
13
|
+
const loaded = await import(`${moduleUrl}?t=${Date.now()}`);
|
|
14
|
+
const config = (loaded.default ?? loaded.config ?? {}) ?? {};
|
|
15
|
+
applyEnv(env);
|
|
16
|
+
return { config, env };
|
|
17
|
+
}
|
|
18
|
+
async function loadFiyuuEnvironment(rootDirectory, mode) {
|
|
19
|
+
const directory = path.join(rootDirectory, ".fiyuu");
|
|
20
|
+
const files = [
|
|
21
|
+
path.join(directory, "env"),
|
|
22
|
+
path.join(directory, `${mode}.env`),
|
|
23
|
+
path.join(directory, "SECRET"),
|
|
24
|
+
];
|
|
25
|
+
const env = {};
|
|
26
|
+
for (const filePath of files) {
|
|
27
|
+
if (!existsSync(filePath)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const name = path.basename(filePath);
|
|
31
|
+
if (name === "SECRET") {
|
|
32
|
+
env.FIYUU_SECRET = (await fs.readFile(filePath, "utf8")).trim();
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
36
|
+
for (const line of content.split(/\r?\n/)) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const separator = trimmed.indexOf("=");
|
|
42
|
+
if (separator <= 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const key = trimmed.slice(0, separator).trim();
|
|
46
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
47
|
+
env[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
52
|
+
function applyEnv(env) {
|
|
53
|
+
for (const [key, value] of Object.entries(env)) {
|
|
54
|
+
if (process.env[key] === undefined) {
|
|
55
|
+
process.env[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export type AnyZodSchema = z.ZodTypeAny;
|
|
3
|
+
export interface SchemaContract<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema> {
|
|
4
|
+
input: TInput;
|
|
5
|
+
output: TOutput;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ActionDefinition<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema> extends SchemaContract<TInput, TOutput> {
|
|
9
|
+
kind: "action";
|
|
10
|
+
}
|
|
11
|
+
export interface QueryDefinition<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema> extends SchemaContract<TInput, TOutput> {
|
|
12
|
+
kind: "query";
|
|
13
|
+
}
|
|
14
|
+
export interface QueryCacheConfig {
|
|
15
|
+
/** Cache TTL in seconds. Set to 0 to disable. */
|
|
16
|
+
ttl: number;
|
|
17
|
+
/** Cache key varies by these URL query string parameters. */
|
|
18
|
+
vary?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface MetaDefinition {
|
|
21
|
+
intent: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
render?: RenderMode;
|
|
24
|
+
/**
|
|
25
|
+
* Mark this page as zero-JS.
|
|
26
|
+
* `fiyuu doctor` will warn if <script> tags are detected in page.tsx.
|
|
27
|
+
*/
|
|
28
|
+
noJs?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Revalidate interval in seconds for `render: "ssg"` pages.
|
|
31
|
+
* Works like ISR: stale HTML is served immediately and refreshed in background.
|
|
32
|
+
*/
|
|
33
|
+
revalidate?: number;
|
|
34
|
+
seo?: {
|
|
35
|
+
title?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface PageDefinition {
|
|
40
|
+
kind: "page";
|
|
41
|
+
intent: string;
|
|
42
|
+
}
|
|
43
|
+
export interface LayoutDefinition {
|
|
44
|
+
kind: "layout";
|
|
45
|
+
name?: string;
|
|
46
|
+
}
|
|
47
|
+
export type RenderMode = "ssr" | "csr" | "ssg";
|
|
48
|
+
export interface PageProps<TData = unknown> {
|
|
49
|
+
data: TData | null;
|
|
50
|
+
route: string;
|
|
51
|
+
intent: string;
|
|
52
|
+
render: RenderMode;
|
|
53
|
+
/** Dynamic route parameters extracted from the URL, e.g. { id: "42" } for /blog/[id] */
|
|
54
|
+
params: Record<string, string>;
|
|
55
|
+
}
|
|
56
|
+
export interface LayoutProps {
|
|
57
|
+
children: string;
|
|
58
|
+
route: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function defineAction<TInput extends AnyZodSchema, TOutput extends AnyZodSchema>(config: SchemaContract<TInput, TOutput>): ActionDefinition<TInput, TOutput>;
|
|
61
|
+
export declare function defineQuery<TInput extends AnyZodSchema, TOutput extends AnyZodSchema>(config: SchemaContract<TInput, TOutput>): QueryDefinition<TInput, TOutput>;
|
|
62
|
+
export declare function definePage(config: {
|
|
63
|
+
intent: string;
|
|
64
|
+
}): PageDefinition;
|
|
65
|
+
export declare function defineLayout(config?: {
|
|
66
|
+
name?: string;
|
|
67
|
+
}): LayoutDefinition;
|
|
68
|
+
export declare function defineMeta(config: MetaDefinition): MetaDefinition;
|
|
69
|
+
/** TypeScript output type inferred from a QueryDefinition's output Zod schema. */
|
|
70
|
+
export type InferQueryOutput<T extends QueryDefinition> = z.infer<T["output"]>;
|
|
71
|
+
/** TypeScript input type inferred from a QueryDefinition's input Zod schema. */
|
|
72
|
+
export type InferQueryInput<T extends QueryDefinition> = z.infer<T["input"]>;
|
|
73
|
+
/** TypeScript input type inferred from an ActionDefinition's input Zod schema. */
|
|
74
|
+
export type InferActionInput<T extends ActionDefinition> = z.infer<T["input"]>;
|
|
75
|
+
/** TypeScript output type inferred from an ActionDefinition's output Zod schema. */
|
|
76
|
+
export type InferActionOutput<T extends ActionDefinition> = z.infer<T["output"]>;
|
|
77
|
+
/**
|
|
78
|
+
* Context passed to query `execute()` functions.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* export async function execute({ request, params }: QueryContext) {
|
|
82
|
+
* const sessionId = getSessionIdFromRequest(request as any);
|
|
83
|
+
* ...
|
|
84
|
+
* }
|
|
85
|
+
*/
|
|
86
|
+
export interface QueryContext {
|
|
87
|
+
/** Incoming HTTP request (Node.js IncomingMessage). Cast to `any` if you need raw access. */
|
|
88
|
+
request: unknown;
|
|
89
|
+
/** Dynamic URL parameters, e.g. { id: "42" } for route /blog/[id] */
|
|
90
|
+
params: Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Context passed to action `execute()` functions.
|
|
94
|
+
* Pass `typeof action` as the generic to get fully typed `input`.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* export const action = defineAction({ input: z.object({ title: z.string() }), ... });
|
|
98
|
+
*
|
|
99
|
+
* export async function execute({ input, request }: ActionContext<typeof action>) {
|
|
100
|
+
* // input.title is typed as string — no manual type annotation needed
|
|
101
|
+
* }
|
|
102
|
+
*/
|
|
103
|
+
export interface ActionContext<T extends ActionDefinition = ActionDefinition> {
|
|
104
|
+
/** Action input, typed from the action's Zod input schema. */
|
|
105
|
+
input: z.infer<T["input"]>;
|
|
106
|
+
/** Incoming HTTP request (Node.js IncomingMessage). Cast to `any` if you need raw access. */
|
|
107
|
+
request: unknown;
|
|
108
|
+
/** Dynamic URL parameters, e.g. { id: "42" } for route /blog/[id] */
|
|
109
|
+
params: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Context object passed to every middleware handler.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* export const middleware = defineMiddleware(async ({ url, request }, next) => {
|
|
116
|
+
* if (url.pathname === "/secret") { ... }
|
|
117
|
+
* await next();
|
|
118
|
+
* });
|
|
119
|
+
*/
|
|
120
|
+
export interface FiyuuMiddlewareContext {
|
|
121
|
+
/** Incoming HTTP request — Node.js `IncomingMessage`. */
|
|
122
|
+
request: unknown;
|
|
123
|
+
/** Parsed request URL. */
|
|
124
|
+
url: URL;
|
|
125
|
+
/** Mutable response headers that will be sent with the final response. */
|
|
126
|
+
responseHeaders: Record<string, string>;
|
|
127
|
+
/** Unique ID for this request (useful for logging). */
|
|
128
|
+
requestId: string;
|
|
129
|
+
/** Non-fatal warnings accumulated during request handling. */
|
|
130
|
+
warnings: string[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Return this from a middleware to short-circuit the request.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // Redirect to login
|
|
137
|
+
* return {
|
|
138
|
+
* headers: { Location: "/auth" },
|
|
139
|
+
* response: { status: 302, body: "" },
|
|
140
|
+
* };
|
|
141
|
+
*/
|
|
142
|
+
export interface FiyuuMiddlewareResult {
|
|
143
|
+
/** Extra response headers to send (e.g. `Location` for redirects). */
|
|
144
|
+
headers?: Record<string, string>;
|
|
145
|
+
response?: {
|
|
146
|
+
/** HTTP status code. */
|
|
147
|
+
status?: number;
|
|
148
|
+
/** JSON body — serialised automatically. */
|
|
149
|
+
json?: unknown;
|
|
150
|
+
/** Raw string body. */
|
|
151
|
+
body?: string;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Call this to continue to the next middleware or the route handler. */
|
|
155
|
+
export type FiyuuMiddlewareNext = () => Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* A single middleware function.
|
|
158
|
+
* Return `FiyuuMiddlewareResult` to short-circuit, or call `next()` to continue.
|
|
159
|
+
*/
|
|
160
|
+
export type FiyuuMiddlewareHandler = (context: FiyuuMiddlewareContext, next: FiyuuMiddlewareNext) => Promise<FiyuuMiddlewareResult | void> | FiyuuMiddlewareResult | void;
|
|
161
|
+
/**
|
|
162
|
+
* Wraps your middleware function and gives it full type inference.
|
|
163
|
+
* Import from `@fiyuu/core` — no need to touch `@fiyuu/runtime`.
|
|
164
|
+
*
|
|
165
|
+
* @example app/middleware.ts
|
|
166
|
+
* import { defineMiddleware } from "@fiyuu/core";
|
|
167
|
+
*
|
|
168
|
+
* export const middleware = defineMiddleware(async ({ url, request }, next) => {
|
|
169
|
+
* if (url.pathname.startsWith("/dashboard")) {
|
|
170
|
+
* const user = await getSessionUser(request);
|
|
171
|
+
* if (!user) return { headers: { Location: "/auth" }, response: { status: 302, body: "" } };
|
|
172
|
+
* }
|
|
173
|
+
* await next();
|
|
174
|
+
* });
|
|
175
|
+
*/
|
|
176
|
+
export declare function defineMiddleware(handler: FiyuuMiddlewareHandler): FiyuuMiddlewareHandler;
|
package/src/contracts.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function defineAction(config) {
|
|
2
|
+
return {
|
|
3
|
+
kind: "action",
|
|
4
|
+
...config,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function defineQuery(config) {
|
|
8
|
+
return {
|
|
9
|
+
kind: "query",
|
|
10
|
+
...config,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function definePage(config) {
|
|
14
|
+
return {
|
|
15
|
+
kind: "page",
|
|
16
|
+
...config,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function defineLayout(config = {}) {
|
|
20
|
+
return {
|
|
21
|
+
kind: "layout",
|
|
22
|
+
...config,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function defineMeta(config) {
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps your middleware function and gives it full type inference.
|
|
30
|
+
* Import from `@fiyuu/core` — no need to touch `@fiyuu/runtime`.
|
|
31
|
+
*
|
|
32
|
+
* @example app/middleware.ts
|
|
33
|
+
* import { defineMiddleware } from "@fiyuu/core";
|
|
34
|
+
*
|
|
35
|
+
* export const middleware = defineMiddleware(async ({ url, request }, next) => {
|
|
36
|
+
* if (url.pathname.startsWith("/dashboard")) {
|
|
37
|
+
* const user = await getSessionUser(request);
|
|
38
|
+
* if (!user) return { headers: { Location: "/auth" }, response: { status: 302, body: "" } };
|
|
39
|
+
* }
|
|
40
|
+
* await next();
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
export function defineMiddleware(handler) {
|
|
44
|
+
return handler;
|
|
45
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const PAGE_TEMPLATE = `import { Component } from "@geajs/core";
|
|
4
|
+
import { definePage, html, optimizedImage, responsiveStyle, fluid, type PageProps } from "fiyuu/client";
|
|
5
|
+
|
|
6
|
+
type {{Title}}Data = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export const page = definePage({
|
|
9
|
+
intent: "{{intent}}",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default class {{Title}}Page extends Component<PageProps<{{Title}}Data>> {
|
|
13
|
+
template({ data, intent }: PageProps<{{Title}}Data> = this.props) {
|
|
14
|
+
// Strings interpolated into html\`\` are auto-escaped — no need for escapeHtml().
|
|
15
|
+
// For pre-rendered HTML fragments, use raw() to prevent double-escaping.
|
|
16
|
+
const responsive = responsiveStyle(".feature-shell", "padding:16px;max-width:100%;", {
|
|
17
|
+
md: "padding:24px;",
|
|
18
|
+
lg: "padding:32px;max-width:960px;margin-inline:auto;",
|
|
19
|
+
});
|
|
20
|
+
return html\`<main style="padding:48px;font-family:ui-sans-serif,system-ui,sans-serif">
|
|
21
|
+
\${responsive}
|
|
22
|
+
<section class="feature-shell">
|
|
23
|
+
<h1 style="margin:0;font-size:2.5rem">{{title}}</h1>
|
|
24
|
+
<p style="margin-top:12px;color:#4b5b4c;font-size:\${fluid(16, 20)}">\${intent}</p>
|
|
25
|
+
\${optimizedImage({ src: "/assets/hero.jpg", alt: "{{title}}", width: 1280, height: 720, sizes: "(min-width: 1024px) 960px, 100vw" })}
|
|
26
|
+
</section>
|
|
27
|
+
</main>\`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
const ACTION_TEMPLATE = `import { z } from "zod";
|
|
32
|
+
import { defineAction } from "fiyuu/client";
|
|
33
|
+
|
|
34
|
+
export const action = defineAction({
|
|
35
|
+
input: z.object({}),
|
|
36
|
+
output: z.object({ success: z.boolean() }),
|
|
37
|
+
description: "{{description}}",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export async function execute(input: Record<string, unknown>) {
|
|
41
|
+
return { success: true };
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
const QUERY_TEMPLATE = `import { z } from "zod";
|
|
45
|
+
import { defineQuery } from "fiyuu/client";
|
|
46
|
+
|
|
47
|
+
export const query = defineQuery({
|
|
48
|
+
input: z.object({}),
|
|
49
|
+
output: z.object({}),
|
|
50
|
+
description: "{{description}}",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export async function execute() {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
const SCHEMA_TEMPLATE = `import { z } from "zod";
|
|
58
|
+
|
|
59
|
+
export const input = z.object({});
|
|
60
|
+
|
|
61
|
+
export const output = z.object({
|
|
62
|
+
success: z.boolean(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const description = "{{description}}";
|
|
66
|
+
`;
|
|
67
|
+
const META_TEMPLATE = `import { defineMeta } from "fiyuu/client";
|
|
68
|
+
|
|
69
|
+
export default defineMeta({
|
|
70
|
+
intent: "{{intent}}",
|
|
71
|
+
title: "{{title}}",
|
|
72
|
+
render: "ssr",
|
|
73
|
+
seo: {
|
|
74
|
+
title: "{{title}}",
|
|
75
|
+
description: "{{description}}",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
`;
|
|
79
|
+
export async function generatePageFeature(appDirectory, featureName) {
|
|
80
|
+
const featureDirectory = path.join(appDirectory, featureName);
|
|
81
|
+
await fs.mkdir(featureDirectory, { recursive: true });
|
|
82
|
+
const title = titleCase(featureName);
|
|
83
|
+
const intent = `${title} page for user-facing interactions`;
|
|
84
|
+
const description = `Fetches data for ${title}`;
|
|
85
|
+
const files = [
|
|
86
|
+
["page.tsx", PAGE_TEMPLATE],
|
|
87
|
+
["query.ts", QUERY_TEMPLATE],
|
|
88
|
+
["schema.ts", SCHEMA_TEMPLATE],
|
|
89
|
+
["meta.ts", META_TEMPLATE],
|
|
90
|
+
];
|
|
91
|
+
await Promise.all(files.map(([fileName, template]) => fs.writeFile(path.join(featureDirectory, fileName), hydrate(template, { title, intent, description, Title: pascalCase(featureName) }))));
|
|
92
|
+
return files.map(([fileName]) => path.join(featureDirectory, fileName));
|
|
93
|
+
}
|
|
94
|
+
export async function generateActionFeature(appDirectory, featureName) {
|
|
95
|
+
const featureDirectory = path.join(appDirectory, featureName);
|
|
96
|
+
await fs.mkdir(featureDirectory, { recursive: true });
|
|
97
|
+
const title = titleCase(featureName);
|
|
98
|
+
const intent = `${title} workflow for server-side mutations`;
|
|
99
|
+
const description = `Performs ${title} mutation`;
|
|
100
|
+
const files = [
|
|
101
|
+
["action.ts", ACTION_TEMPLATE],
|
|
102
|
+
["schema.ts", SCHEMA_TEMPLATE],
|
|
103
|
+
["meta.ts", META_TEMPLATE],
|
|
104
|
+
];
|
|
105
|
+
await Promise.all(files.map(([fileName, template]) => fs.writeFile(path.join(featureDirectory, fileName), hydrate(template, { title, intent, description, Title: pascalCase(featureName) }))));
|
|
106
|
+
return files.map(([fileName]) => path.join(featureDirectory, fileName));
|
|
107
|
+
}
|
|
108
|
+
function hydrate(template, replacements) {
|
|
109
|
+
return Object.entries(replacements).reduce((content, [key, value]) => content.replaceAll(`{{${key}}}`, value), template);
|
|
110
|
+
}
|
|
111
|
+
function titleCase(value) {
|
|
112
|
+
return value
|
|
113
|
+
.split(/[\/-]/)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
116
|
+
.join(" ");
|
|
117
|
+
}
|
|
118
|
+
function pascalCase(value) {
|
|
119
|
+
return value
|
|
120
|
+
.split(/[\/-_]/)
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
123
|
+
.join("");
|
|
124
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./artifacts.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./generator.js";
|
|
5
|
+
export * from "./media.js";
|
|
6
|
+
export * from "./responsive.js";
|
|
7
|
+
export * from "./responsive-wrapper.js";
|
|
8
|
+
export * from "./scanner.js";
|
|
9
|
+
export * from "./state.js";
|
|
10
|
+
export * from "./virtual.js";
|
|
11
|
+
export * from "./reactive.js";
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./artifacts.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./generator.js";
|
|
5
|
+
export * from "./media.js";
|
|
6
|
+
export * from "./responsive.js";
|
|
7
|
+
export * from "./responsive-wrapper.js";
|
|
8
|
+
export * from "./scanner.js";
|
|
9
|
+
export * from "./state.js";
|
|
10
|
+
export * from "./virtual.js";
|
|
11
|
+
export * from "./reactive.js";
|
package/src/media.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface OptimizedImageSource {
|
|
2
|
+
srcSet: string;
|
|
3
|
+
media?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
sizes?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OptimizedImageProps {
|
|
8
|
+
src: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
sizes?: string;
|
|
13
|
+
srcSet?: string;
|
|
14
|
+
sources?: OptimizedImageSource[];
|
|
15
|
+
loading?: "lazy" | "eager";
|
|
16
|
+
decoding?: "async" | "sync" | "auto";
|
|
17
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
18
|
+
class?: string;
|
|
19
|
+
style?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface OptimizedVideoSource {
|
|
23
|
+
src: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
media?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface OptimizedVideoProps {
|
|
28
|
+
src?: string;
|
|
29
|
+
sources?: OptimizedVideoSource[];
|
|
30
|
+
poster?: string;
|
|
31
|
+
preload?: "none" | "metadata" | "auto";
|
|
32
|
+
controls?: boolean;
|
|
33
|
+
muted?: boolean;
|
|
34
|
+
loop?: boolean;
|
|
35
|
+
autoPlay?: boolean;
|
|
36
|
+
playsInline?: boolean;
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
class?: string;
|
|
40
|
+
style?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function optimizedImage(props: OptimizedImageProps): string;
|
|
44
|
+
export declare function optimizedVideo(props: OptimizedVideoProps): string;
|