@pyreon/zero 0.12.1 → 0.12.3
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/lib/actions.js +97 -0
- package/lib/actions.js.map +1 -0
- package/lib/ai.js +503 -0
- package/lib/ai.js.map +1 -0
- package/lib/api-routes.js +137 -0
- package/lib/api-routes.js.map +1 -0
- package/lib/compression.js +80 -0
- package/lib/compression.js.map +1 -0
- package/lib/cors.js +57 -0
- package/lib/cors.js.map +1 -0
- package/lib/csp.js +119 -0
- package/lib/csp.js.map +1 -0
- package/lib/env.js +217 -0
- package/lib/env.js.map +1 -0
- package/lib/favicon.js +424 -0
- package/lib/favicon.js.map +1 -0
- package/lib/i18n-routing.js +167 -0
- package/lib/i18n-routing.js.map +1 -0
- package/lib/index.js +1631 -179
- package/lib/index.js.map +1 -1
- package/lib/link.js +5 -0
- package/lib/link.js.map +1 -1
- package/lib/logger.js +78 -0
- package/lib/logger.js.map +1 -0
- package/lib/meta.js +336 -0
- package/lib/meta.js.map +1 -0
- package/lib/middleware.js +53 -0
- package/lib/middleware.js.map +1 -0
- package/lib/og-image.js +233 -0
- package/lib/og-image.js.map +1 -0
- package/lib/rate-limit.js +76 -0
- package/lib/rate-limit.js.map +1 -0
- package/lib/testing.js +179 -0
- package/lib/testing.js.map +1 -0
- package/lib/theme.js +11 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/actions.d.ts +27 -24
- package/lib/types/actions.d.ts.map +1 -1
- package/lib/types/ai.d.ts +163 -0
- package/lib/types/ai.d.ts.map +1 -0
- package/lib/types/api-routes.d.ts +37 -33
- package/lib/types/api-routes.d.ts.map +1 -1
- package/lib/types/cache.d.ts +26 -22
- package/lib/types/cache.d.ts.map +1 -1
- package/lib/types/client.d.ts +13 -9
- package/lib/types/client.d.ts.map +1 -1
- package/lib/types/compression.d.ts +14 -10
- package/lib/types/compression.d.ts.map +1 -1
- package/lib/types/config.d.ts +39 -4
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/cors.d.ts +20 -16
- package/lib/types/cors.d.ts.map +1 -1
- package/lib/types/csp.d.ts +88 -0
- package/lib/types/csp.d.ts.map +1 -0
- package/lib/types/env.d.ts +118 -0
- package/lib/types/env.d.ts.map +1 -0
- package/lib/types/favicon.d.ts +70 -24
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/font.d.ts +68 -65
- package/lib/types/font.d.ts.map +1 -1
- package/lib/types/i18n-routing.d.ts +43 -37
- package/lib/types/i18n-routing.d.ts.map +1 -1
- package/lib/types/image-plugin.d.ts +49 -45
- package/lib/types/image-plugin.d.ts.map +1 -1
- package/lib/types/image.d.ts +47 -36
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts +1961 -46
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/link.d.ts +61 -56
- package/lib/types/link.d.ts.map +1 -1
- package/lib/types/logger.d.ts +57 -0
- package/lib/types/logger.d.ts.map +1 -0
- package/lib/types/meta.d.ts +180 -69
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/middleware.d.ts +8 -4
- package/lib/types/middleware.d.ts.map +1 -1
- package/lib/types/og-image.d.ts +111 -0
- package/lib/types/og-image.d.ts.map +1 -0
- package/lib/types/rate-limit.d.ts +20 -16
- package/lib/types/rate-limit.d.ts.map +1 -1
- package/lib/types/script.d.ts +23 -19
- package/lib/types/script.d.ts.map +1 -1
- package/lib/types/seo.d.ts +47 -43
- package/lib/types/seo.d.ts.map +1 -1
- package/lib/types/testing.d.ts +64 -27
- package/lib/types/testing.d.ts.map +1 -1
- package/lib/types/theme.d.ts +22 -12
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +37 -12
- package/src/actions.ts +1 -3
- package/src/adapters/bun.ts +2 -0
- package/src/adapters/cloudflare.ts +84 -0
- package/src/adapters/index.ts +13 -1
- package/src/adapters/netlify.ts +86 -0
- package/src/adapters/node.ts +2 -0
- package/src/adapters/validate.ts +16 -0
- package/src/adapters/vercel.ts +86 -0
- package/src/ai.ts +623 -0
- package/src/compression.ts +19 -3
- package/src/csp.ts +207 -0
- package/src/entry-server.ts +28 -5
- package/src/env.ts +344 -0
- package/src/favicon.ts +221 -80
- package/src/index.ts +42 -2
- package/src/link.tsx +6 -0
- package/src/logger.ts +144 -0
- package/src/meta.tsx +124 -14
- package/src/og-image.ts +378 -0
- package/src/rate-limit.ts +11 -9
- package/src/theme.tsx +12 -1
- package/src/types.ts +1 -1
- package/src/vite-plugin.ts +5 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -10
- package/lib/types/adapters/index.d.ts.map +0 -1
- package/lib/types/adapters/node.d.ts +0 -6
- package/lib/types/adapters/node.d.ts.map +0 -1
- package/lib/types/adapters/static.d.ts +0 -7
- package/lib/types/adapters/static.d.ts.map +0 -1
- package/lib/types/app.d.ts +0 -24
- package/lib/types/app.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -37
- package/lib/types/entry-server.d.ts.map +0 -1
- package/lib/types/error-overlay.d.ts +0 -6
- package/lib/types/error-overlay.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -47
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/isr.d.ts +0 -9
- package/lib/types/isr.d.ts.map +0 -1
- package/lib/types/not-found.d.ts +0 -7
- package/lib/types/not-found.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -111
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils/use-intersection-observer.d.ts +0 -10
- package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
- package/lib/types/utils/with-headers.d.ts +0 -6
- package/lib/types/utils/with-headers.d.ts.map +0 -1
- package/lib/types/vite-plugin.d.ts +0 -17
- package/lib/types/vite-plugin.d.ts.map +0 -1
package/lib/testing.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
//#region src/api-routes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Match a URL path against an API route pattern.
|
|
4
|
+
* Returns extracted params or null if no match.
|
|
5
|
+
*/
|
|
6
|
+
function matchApiRoute(pattern, path) {
|
|
7
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
8
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
9
|
+
const params = {};
|
|
10
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
11
|
+
const pp = patternParts[i];
|
|
12
|
+
if (!pp) continue;
|
|
13
|
+
if (pp.endsWith("*")) {
|
|
14
|
+
const paramName = pp.slice(1, -1);
|
|
15
|
+
params[paramName] = pathParts.slice(i).join("/");
|
|
16
|
+
return params;
|
|
17
|
+
}
|
|
18
|
+
if (i >= pathParts.length) return null;
|
|
19
|
+
if (pp.startsWith(":")) {
|
|
20
|
+
params[pp.slice(1)] = pathParts[i];
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (pp !== pathParts[i]) return null;
|
|
24
|
+
}
|
|
25
|
+
return patternParts.length === pathParts.length ? params : null;
|
|
26
|
+
}
|
|
27
|
+
const HTTP_METHODS = [
|
|
28
|
+
"GET",
|
|
29
|
+
"POST",
|
|
30
|
+
"PUT",
|
|
31
|
+
"PATCH",
|
|
32
|
+
"DELETE",
|
|
33
|
+
"HEAD",
|
|
34
|
+
"OPTIONS"
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Create a middleware that dispatches API route requests.
|
|
38
|
+
* API routes are matched by URL pattern and HTTP method.
|
|
39
|
+
*/
|
|
40
|
+
function createApiMiddleware(routes) {
|
|
41
|
+
return async (ctx) => {
|
|
42
|
+
for (const route of routes) {
|
|
43
|
+
const params = matchApiRoute(route.pattern, ctx.path);
|
|
44
|
+
if (!params) continue;
|
|
45
|
+
const method = ctx.req.method.toUpperCase();
|
|
46
|
+
const handler = route.module[method];
|
|
47
|
+
if (!handler) {
|
|
48
|
+
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(", ");
|
|
49
|
+
return new Response(null, {
|
|
50
|
+
status: 405,
|
|
51
|
+
headers: {
|
|
52
|
+
Allow: allowed,
|
|
53
|
+
"Content-Type": "application/json"
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return handler({
|
|
58
|
+
request: ctx.req,
|
|
59
|
+
url: ctx.url,
|
|
60
|
+
path: ctx.path,
|
|
61
|
+
params,
|
|
62
|
+
headers: ctx.req.headers
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/testing.ts
|
|
70
|
+
/**
|
|
71
|
+
* Create a mock MiddlewareContext for testing middleware.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* import { createTestContext } from "@pyreon/zero/testing"
|
|
75
|
+
*
|
|
76
|
+
* const ctx = createTestContext("/api/posts", { method: "POST", body: { title: "Hello" } })
|
|
77
|
+
* const result = await myMiddleware(ctx)
|
|
78
|
+
*/
|
|
79
|
+
function createTestContext(path, options = {}) {
|
|
80
|
+
const { method = "GET", headers = {}, body } = options;
|
|
81
|
+
const url = new URL(`http://localhost${path}`);
|
|
82
|
+
const requestHeaders = { ...headers };
|
|
83
|
+
let requestBody;
|
|
84
|
+
if (body !== void 0) {
|
|
85
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
86
|
+
requestBody = JSON.stringify(body);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
req: new Request(url.toString(), {
|
|
90
|
+
method,
|
|
91
|
+
headers: requestHeaders,
|
|
92
|
+
...requestBody != null ? { body: requestBody } : {}
|
|
93
|
+
}),
|
|
94
|
+
url,
|
|
95
|
+
path,
|
|
96
|
+
headers: new Headers(),
|
|
97
|
+
locals: {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Test a middleware by running it with a mock context and returning
|
|
102
|
+
* the result along with the response headers it set.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* import { testMiddleware } from "@pyreon/zero/testing"
|
|
106
|
+
*
|
|
107
|
+
* const { response, headers } = await testMiddleware(
|
|
108
|
+
* corsMiddleware({ origin: "*" }),
|
|
109
|
+
* "/api/posts"
|
|
110
|
+
* )
|
|
111
|
+
* expect(headers.get("Access-Control-Allow-Origin")).toBe("*")
|
|
112
|
+
*/
|
|
113
|
+
async function testMiddleware(middleware, path, options = {}) {
|
|
114
|
+
const ctx = createTestContext(path, options);
|
|
115
|
+
return {
|
|
116
|
+
response: await middleware(ctx),
|
|
117
|
+
headers: ctx.headers
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a test server for API routes. Returns a function that
|
|
122
|
+
* accepts Request objects and dispatches to the correct handler.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* import { createTestApiServer } from "@pyreon/zero/testing"
|
|
126
|
+
*
|
|
127
|
+
* const server = createTestApiServer([
|
|
128
|
+
* { pattern: "/api/posts", module: postsApi },
|
|
129
|
+
* { pattern: "/api/posts/:id", module: postByIdApi },
|
|
130
|
+
* ])
|
|
131
|
+
*
|
|
132
|
+
* const response = await server.request("/api/posts")
|
|
133
|
+
* expect(response.status).toBe(200)
|
|
134
|
+
*
|
|
135
|
+
* const data = await server.request("/api/posts", { method: "POST", body: { title: "Hi" } })
|
|
136
|
+
* expect(data.status).toBe(201)
|
|
137
|
+
*/
|
|
138
|
+
function createTestApiServer(routes) {
|
|
139
|
+
const middleware = createApiMiddleware(routes);
|
|
140
|
+
return { async request(path, options = {}) {
|
|
141
|
+
const result = await middleware(createTestContext(path, options));
|
|
142
|
+
if (!result) return new Response("Not Found", { status: 404 });
|
|
143
|
+
return result;
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a mock API handler for testing.
|
|
148
|
+
* Records all calls and returns a configurable response.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* import { createMockHandler } from "@pyreon/zero/testing"
|
|
152
|
+
*
|
|
153
|
+
* const handler = createMockHandler({ status: 200, body: { ok: true } })
|
|
154
|
+
* // ... use handler in your API route module
|
|
155
|
+
* expect(handler.calls).toHaveLength(1)
|
|
156
|
+
* expect(handler.calls[0].params).toEqual({ id: "123" })
|
|
157
|
+
*/
|
|
158
|
+
function createMockHandler(responseConfig = {}) {
|
|
159
|
+
const { status = 200, body = null, headers = {} } = responseConfig;
|
|
160
|
+
const calls = [];
|
|
161
|
+
const handler = (ctx) => {
|
|
162
|
+
calls.push({
|
|
163
|
+
path: ctx.path,
|
|
164
|
+
params: ctx.params
|
|
165
|
+
});
|
|
166
|
+
return new Response(JSON.stringify(body), {
|
|
167
|
+
status,
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
...headers
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
return Object.assign(handler, { calls });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
export { createMockHandler, createTestApiServer, createTestContext, testMiddleware };
|
|
179
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/api-routes.ts","../src/testing.ts"],"sourcesContent":["import type { Middleware, MiddlewareContext } from '@pyreon/server'\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** HTTP methods supported by API routes. */\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'\n\n/** Context passed to API route handlers. */\nexport interface ApiContext {\n /** The incoming request. */\n request: Request\n /** Parsed URL. */\n url: URL\n /** URL path. */\n path: string\n /** Dynamic route parameters (e.g., { id: \"123\" }). */\n params: Record<string, string>\n /** Request headers. */\n headers: Headers\n}\n\n/** An API route handler function. */\nexport type ApiHandler = (ctx: ApiContext) => Response | Promise<Response>\n\n/** An API route module — exports named HTTP method handlers. */\nexport interface ApiRouteModule {\n GET?: ApiHandler\n POST?: ApiHandler\n PUT?: ApiHandler\n PATCH?: ApiHandler\n DELETE?: ApiHandler\n HEAD?: ApiHandler\n OPTIONS?: ApiHandler\n}\n\n/** A registered API route entry. */\nexport interface ApiRouteEntry {\n /** URL pattern (e.g., \"/api/posts/:id\"). */\n pattern: string\n /** The route module with method handlers. */\n module: ApiRouteModule\n}\n\n// ─── Pattern matching ────────────────────────────────────────────────────────\n\n/**\n * Match a URL path against an API route pattern.\n * Returns extracted params or null if no match.\n */\nexport function matchApiRoute(pattern: string, path: string): Record<string, string> | null {\n const patternParts = pattern.split('/').filter(Boolean)\n const pathParts = path.split('/').filter(Boolean)\n const params: Record<string, string> = {}\n\n for (let i = 0; i < patternParts.length; i++) {\n const pp = patternParts[i]\n if (!pp) continue\n\n // Catch-all: :param*\n if (pp.endsWith('*')) {\n const paramName = pp.slice(1, -1)\n params[paramName] = pathParts.slice(i).join('/')\n return params\n }\n\n // No more path segments\n if (i >= pathParts.length) return null\n\n // Dynamic segment: :param\n if (pp.startsWith(':')) {\n params[pp.slice(1)] = pathParts[i]!\n continue\n }\n\n // Static segment\n if (pp !== pathParts[i]) return null\n }\n\n return patternParts.length === pathParts.length ? params : null\n}\n\n// ─── Middleware ───────────────────────────────────────────────────────────────\n\nconst HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']\n\n/**\n * Create a middleware that dispatches API route requests.\n * API routes are matched by URL pattern and HTTP method.\n */\nexport function createApiMiddleware(routes: ApiRouteEntry[]): Middleware {\n return async (ctx: MiddlewareContext) => {\n for (const route of routes) {\n const params = matchApiRoute(route.pattern, ctx.path)\n if (!params) continue\n\n const method = ctx.req.method.toUpperCase() as HttpMethod\n const handler = route.module[method]\n\n if (!handler) {\n // Route matched but method not supported\n const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(', ')\n return new Response(null, {\n status: 405,\n headers: {\n Allow: allowed,\n 'Content-Type': 'application/json',\n },\n })\n }\n\n return handler({\n request: ctx.req,\n url: ctx.url,\n path: ctx.path,\n params,\n headers: ctx.req.headers,\n })\n }\n }\n}\n\n// ─── Virtual module generation ───────────────────────────────────────────────\n\n/**\n * Detect whether a route file is an API route.\n * API routes are `.ts` or `.js` files inside an `api/` directory.\n */\nexport function isApiRoute(filePath: string): boolean {\n const normalized = filePath.replace(/\\\\/g, '/')\n return (\n normalized.startsWith('api/') &&\n (normalized.endsWith('.ts') || normalized.endsWith('.js')) &&\n !normalized.endsWith('.tsx') &&\n !normalized.endsWith('.jsx')\n )\n}\n\n/**\n * Convert an API route file path to a URL pattern.\n *\n * Examples:\n * \"api/posts.ts\" → \"/api/posts\"\n * \"api/posts/index.ts\" → \"/api/posts\"\n * \"api/posts/[id].ts\" → \"/api/posts/:id\"\n * \"api/[...path].ts\" → \"/api/:path*\"\n */\nexport function apiFilePathToPattern(filePath: string): string {\n let route = filePath\n // Remove extension\n for (const ext of ['.ts', '.js']) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const segments = route.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n if (seg === 'index') continue\n\n // Catch-all: [...param]\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param]\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n return `/${urlSegments.join('/')}`\n}\n\n/**\n * Generate a virtual module that exports API route entries.\n * Each entry maps a URL pattern to a module with HTTP method handlers.\n */\nexport function generateApiRouteModule(files: string[], routesDir: string): string {\n const apiFiles = files.filter(isApiRoute)\n\n if (apiFiles.length === 0) {\n return 'export const apiRoutes = []\\n'\n }\n\n const imports: string[] = []\n const entries: string[] = []\n\n for (let i = 0; i < apiFiles.length; i++) {\n const name = `_api${i}`\n const file = apiFiles[i]\n if (!file) continue\n const fullPath = `${routesDir}/${file}`\n const pattern = apiFilePathToPattern(file)\n\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(pattern)}, module: ${name} }`)\n }\n\n return [...imports, '', 'export const apiRoutes = [', entries.join(',\\n'), ']'].join('\\n')\n}\n","import type { Middleware, MiddlewareContext } from '@pyreon/server'\nimport type { ApiHandler, ApiRouteEntry } from './api-routes'\nimport { createApiMiddleware } from './api-routes'\n\n// ─── Test helpers for Zero applications ─────────────────────────────────────\n\n/**\n * Create a mock MiddlewareContext for testing middleware.\n *\n * @example\n * import { createTestContext } from \"@pyreon/zero/testing\"\n *\n * const ctx = createTestContext(\"/api/posts\", { method: \"POST\", body: { title: \"Hello\" } })\n * const result = await myMiddleware(ctx)\n */\nexport function createTestContext(\n path: string,\n options: {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n } = {},\n): MiddlewareContext {\n const { method = 'GET', headers = {}, body } = options\n const url = new URL(`http://localhost${path}`)\n\n const requestHeaders: Record<string, string> = { ...headers }\n let requestBody: string | undefined\n\n if (body !== undefined) {\n requestHeaders['Content-Type'] = 'application/json'\n requestBody = JSON.stringify(body)\n }\n\n const req = new Request(url.toString(), {\n method,\n headers: requestHeaders,\n ...(requestBody != null ? { body: requestBody } : {}),\n })\n\n return {\n req,\n url,\n path,\n headers: new Headers(),\n locals: {},\n }\n}\n\n/**\n * Test a middleware by running it with a mock context and returning\n * the result along with the response headers it set.\n *\n * @example\n * import { testMiddleware } from \"@pyreon/zero/testing\"\n *\n * const { response, headers } = await testMiddleware(\n * corsMiddleware({ origin: \"*\" }),\n * \"/api/posts\"\n * )\n * expect(headers.get(\"Access-Control-Allow-Origin\")).toBe(\"*\")\n */\nexport async function testMiddleware(\n middleware: Middleware,\n path: string,\n options: {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n } = {},\n): Promise<{ response: Response | undefined; headers: Headers }> {\n const ctx = createTestContext(path, options)\n const response = (await middleware(ctx)) as Response | undefined\n return { response, headers: ctx.headers }\n}\n\n/**\n * Create a test server for API routes. Returns a function that\n * accepts Request objects and dispatches to the correct handler.\n *\n * @example\n * import { createTestApiServer } from \"@pyreon/zero/testing\"\n *\n * const server = createTestApiServer([\n * { pattern: \"/api/posts\", module: postsApi },\n * { pattern: \"/api/posts/:id\", module: postByIdApi },\n * ])\n *\n * const response = await server.request(\"/api/posts\")\n * expect(response.status).toBe(200)\n *\n * const data = await server.request(\"/api/posts\", { method: \"POST\", body: { title: \"Hi\" } })\n * expect(data.status).toBe(201)\n */\nexport function createTestApiServer(routes: ApiRouteEntry[]) {\n const middleware = createApiMiddleware(routes)\n\n return {\n async request(\n path: string,\n options: {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n } = {},\n ): Promise<Response> {\n const ctx = createTestContext(path, options)\n const result = await middleware(ctx)\n if (!result) {\n return new Response('Not Found', { status: 404 })\n }\n return result\n },\n }\n}\n\n/**\n * Create a mock API handler for testing.\n * Records all calls and returns a configurable response.\n *\n * @example\n * import { createMockHandler } from \"@pyreon/zero/testing\"\n *\n * const handler = createMockHandler({ status: 200, body: { ok: true } })\n * // ... use handler in your API route module\n * expect(handler.calls).toHaveLength(1)\n * expect(handler.calls[0].params).toEqual({ id: \"123\" })\n */\nexport function createMockHandler(\n responseConfig: { status?: number; body?: unknown; headers?: Record<string, string> } = {},\n): ApiHandler & {\n calls: Array<{ path: string; params: Record<string, string> }>\n} {\n const { status = 200, body = null, headers = {} } = responseConfig\n const calls: Array<{ path: string; params: Record<string, string> }> = []\n\n const handler: ApiHandler = (ctx) => {\n calls.push({ path: ctx.path, params: ctx.params })\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json', ...headers },\n })\n }\n\n return Object.assign(handler, { calls })\n}\n"],"mappings":"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"}
|
package/lib/theme.js
CHANGED
|
@@ -55,11 +55,20 @@ const jsxs = jsx;
|
|
|
55
55
|
const STORAGE_KEY = "zero-theme";
|
|
56
56
|
/** Reactive theme signal. */
|
|
57
57
|
const theme = signal("system");
|
|
58
|
+
/** SSR fallback when system preference can't be detected. Default: 'light'. */
|
|
59
|
+
let _ssrDefault = "light";
|
|
60
|
+
/**
|
|
61
|
+
* Set the default theme for SSR (when `matchMedia` is unavailable).
|
|
62
|
+
* Call once at server startup before rendering.
|
|
63
|
+
*/
|
|
64
|
+
function setSSRThemeDefault(value) {
|
|
65
|
+
_ssrDefault = value;
|
|
66
|
+
}
|
|
58
67
|
/** Computed resolved theme (what's actually applied). */
|
|
59
68
|
function resolvedTheme() {
|
|
60
69
|
const t = theme();
|
|
61
70
|
if (t === "system") {
|
|
62
|
-
if (typeof window === "undefined") return
|
|
71
|
+
if (typeof window === "undefined") return _ssrDefault;
|
|
63
72
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
64
73
|
}
|
|
65
74
|
return t;
|
|
@@ -210,5 +219,5 @@ function ThemeToggle(props) {
|
|
|
210
219
|
const themeScript = `(function(){try{var t=localStorage.getItem("${STORAGE_KEY}");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.dataset.theme=r}catch(e){}})()`;
|
|
211
220
|
|
|
212
221
|
//#endregion
|
|
213
|
-
export { ThemeToggle, initTheme, resolvedTheme, setTheme, theme, themeScript, toggleTheme };
|
|
222
|
+
export { ThemeToggle, initTheme, resolvedTheme, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme };
|
|
214
223
|
//# sourceMappingURL=theme.js.map
|
package/lib/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","names":[],"sources":["../../../core/core/lib/jsx-runtime.js","../src/theme.tsx"],"sourcesContent":["//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { onMount, onUnmount } from '@pyreon/core'\nimport { effect, signal } from '@pyreon/reactivity'\n\n// ─── Theme system ───────────────────────────────────────────────────────────\n//\n// Provides dark/light/system theme support with:\n// - System preference detection via matchMedia\n// - Persistent preference via localStorage\n// - No flash of wrong theme (inline script in HTML)\n// - Reactive theme signal for components\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nconst STORAGE_KEY = 'zero-theme'\n\n/** Reactive theme signal. */\nexport const theme = signal<Theme>('system')\n\n/** Computed resolved theme (what's actually applied). */\nexport function resolvedTheme(): 'light' | 'dark' {\n const t = theme()\n if (t === 'system') {\n if (typeof window === 'undefined') return 'dark'\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n }\n return t\n}\n\n/** Toggle between light and dark. */\nexport function toggleTheme() {\n const current = resolvedTheme()\n setTheme(current === 'dark' ? 'light' : 'dark')\n}\n\n/** Set theme explicitly. */\nexport function setTheme(t: Theme) {\n theme.set(t)\n if (typeof document !== 'undefined') {\n document.documentElement.dataset.theme = resolvedTheme()\n try {\n localStorage.setItem(STORAGE_KEY, t)\n } catch {\n // localStorage may not be available (SSR, private browsing)\n }\n }\n}\n\n/**\n * Initialize the theme system. Call once in your app entry or layout.\n * Reads from localStorage, listens for system preference changes.\n */\nexport function initTheme() {\n onMount(() => {\n // Read persisted preference\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n theme.set(stored)\n }\n } catch {\n // localStorage may not be available\n }\n\n // Apply to document\n document.documentElement.dataset.theme = resolvedTheme()\n\n // Watch for system preference changes\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n function onChange() {\n if (theme() === 'system') {\n document.documentElement.dataset.theme = resolvedTheme()\n }\n }\n mq.addEventListener('change', onChange)\n onUnmount(() => mq.removeEventListener('change', onChange))\n\n // Re-apply when theme signal changes\n const dispose = effect(() => {\n document.documentElement.dataset.theme = resolvedTheme()\n })\n if (dispose) onUnmount(() => dispose.dispose())\n\n return undefined\n })\n}\n\n/**\n * Theme toggle button component.\n *\n * @example\n * import { ThemeToggle } from \"@pyreon/zero/theme\"\n * <ThemeToggle />\n */\nexport function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {\n initTheme()\n\n return (\n <button\n class={props.class}\n style={props.style}\n onClick={toggleTheme}\n aria-label=\"Toggle theme\"\n title=\"Toggle theme\"\n type=\"button\"\n >\n {() =>\n resolvedTheme() === 'dark' ? (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n </svg>\n ) : (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n )\n }\n </button>\n )\n}\n\n/**\n * Inline script to prevent flash of wrong theme.\n * Include this in your index.html <head> BEFORE any stylesheets.\n *\n * @example\n * // index.html\n * <head>\n * <script>{themeScript}</script>\n * ...\n * </head>\n */\nexport const themeScript = `(function(){try{var t=localStorage.getItem(\"${STORAGE_KEY}\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.documentElement.dataset.theme=r}catch(e){}})()`\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../../../core/core/lib/jsx-runtime.js","../src/theme.tsx"],"sourcesContent":["//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { onMount, onUnmount } from '@pyreon/core'\nimport { effect, signal } from '@pyreon/reactivity'\n\n// ─── Theme system ───────────────────────────────────────────────────────────\n//\n// Provides dark/light/system theme support with:\n// - System preference detection via matchMedia\n// - Persistent preference via localStorage\n// - No flash of wrong theme (inline script in HTML)\n// - Reactive theme signal for components\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nconst STORAGE_KEY = 'zero-theme'\n\n/** Reactive theme signal. */\nexport const theme = signal<Theme>('system')\n\n/** SSR fallback when system preference can't be detected. Default: 'light'. */\nlet _ssrDefault: 'light' | 'dark' = 'light'\n\n/**\n * Set the default theme for SSR (when `matchMedia` is unavailable).\n * Call once at server startup before rendering.\n */\nexport function setSSRThemeDefault(value: 'light' | 'dark'): void {\n _ssrDefault = value\n}\n\n/** Computed resolved theme (what's actually applied). */\nexport function resolvedTheme(): 'light' | 'dark' {\n const t = theme()\n if (t === 'system') {\n if (typeof window === 'undefined') return _ssrDefault\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n }\n return t\n}\n\n/** Toggle between light and dark. */\nexport function toggleTheme() {\n const current = resolvedTheme()\n setTheme(current === 'dark' ? 'light' : 'dark')\n}\n\n/** Set theme explicitly. */\nexport function setTheme(t: Theme) {\n theme.set(t)\n if (typeof document !== 'undefined') {\n document.documentElement.dataset.theme = resolvedTheme()\n try {\n localStorage.setItem(STORAGE_KEY, t)\n } catch {\n // localStorage may not be available (SSR, private browsing)\n }\n }\n}\n\n/**\n * Initialize the theme system. Call once in your app entry or layout.\n * Reads from localStorage, listens for system preference changes.\n */\nexport function initTheme() {\n onMount(() => {\n // Read persisted preference\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n theme.set(stored)\n }\n } catch {\n // localStorage may not be available\n }\n\n // Apply to document\n document.documentElement.dataset.theme = resolvedTheme()\n\n // Watch for system preference changes\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n function onChange() {\n if (theme() === 'system') {\n document.documentElement.dataset.theme = resolvedTheme()\n }\n }\n mq.addEventListener('change', onChange)\n onUnmount(() => mq.removeEventListener('change', onChange))\n\n // Re-apply when theme signal changes\n const dispose = effect(() => {\n document.documentElement.dataset.theme = resolvedTheme()\n })\n if (dispose) onUnmount(() => dispose.dispose())\n\n return undefined\n })\n}\n\n/**\n * Theme toggle button component.\n *\n * @example\n * import { ThemeToggle } from \"@pyreon/zero/theme\"\n * <ThemeToggle />\n */\nexport function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {\n initTheme()\n\n return (\n <button\n class={props.class}\n style={props.style}\n onClick={toggleTheme}\n aria-label=\"Toggle theme\"\n title=\"Toggle theme\"\n type=\"button\"\n >\n {() =>\n resolvedTheme() === 'dark' ? (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n </svg>\n ) : (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n )\n }\n </button>\n )\n}\n\n/**\n * Inline script to prevent flash of wrong theme.\n * Include this in your index.html <head> BEFORE any stylesheets.\n *\n * @example\n * // index.html\n * <head>\n * <script>{themeScript}</script>\n * ...\n * </head>\n */\nexport const themeScript = `(function(){try{var t=localStorage.getItem(\"${STORAGE_KEY}\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.documentElement.dataset.theme=r}catch(e){}})()`\n"],"mappings":"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"}
|
package/lib/types/actions.d.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MiddlewareContext } from "@pyreon/server";
|
|
2
|
+
|
|
3
|
+
//#region src/actions.d.ts
|
|
2
4
|
/** Context passed to server action handlers. */
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
interface ActionContext {
|
|
6
|
+
/** The original request. */
|
|
7
|
+
request: Request;
|
|
8
|
+
/** Parsed form data (for form submissions). */
|
|
9
|
+
formData: FormData | null;
|
|
10
|
+
/** Parsed JSON body (for JSON submissions). */
|
|
11
|
+
json: unknown;
|
|
12
|
+
/** Request headers. */
|
|
13
|
+
headers: Headers;
|
|
12
14
|
}
|
|
13
15
|
/** A server action handler function. */
|
|
14
|
-
|
|
16
|
+
type ActionHandler<T = unknown> = (ctx: ActionContext) => T | Promise<T>;
|
|
15
17
|
/** A registered action with its ID and handler. */
|
|
16
18
|
interface RegisteredAction {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
id: string;
|
|
20
|
+
handler: ActionHandler;
|
|
19
21
|
}
|
|
20
22
|
/** Client-side callable action returned by defineAction. */
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
interface Action<T = unknown> {
|
|
24
|
+
/** Call the action with JSON data. */
|
|
25
|
+
(data?: unknown): Promise<T>;
|
|
26
|
+
/** The action's unique ID. */
|
|
27
|
+
actionId: string;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Define a server action. Returns a callable function that:
|
|
@@ -40,18 +42,19 @@ export interface Action<T = unknown> {
|
|
|
40
42
|
* // In a component:
|
|
41
43
|
* const result = await createPost({ title: 'Hello', body: '...' })
|
|
42
44
|
*/
|
|
43
|
-
|
|
45
|
+
declare function defineAction<T = unknown>(handler: ActionHandler<T>): Action<T>;
|
|
44
46
|
/** Get all registered actions. Useful for testing. */
|
|
45
|
-
|
|
47
|
+
declare function getRegisteredActions(): Map<string, RegisteredAction>;
|
|
46
48
|
/**
|
|
47
49
|
* Reset the action registry. Useful for testing.
|
|
48
50
|
* @internal
|
|
49
51
|
*/
|
|
50
|
-
|
|
52
|
+
declare function _resetActions(): void;
|
|
51
53
|
/**
|
|
52
54
|
* Create a middleware that handles action requests at `/_zero/actions/*`.
|
|
53
55
|
* Mount this before the SSR handler in the server entry.
|
|
54
56
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
declare function createActionMiddleware(): (ctx: MiddlewareContext) => Response | undefined | Promise<Response | undefined>;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { Action, ActionContext, ActionHandler, _resetActions, createActionMiddleware, defineAction, getRegisteredActions };
|
|
60
|
+
//# sourceMappingURL=actions2.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"
|
|
1
|
+
{"version":3,"file":"actions2.d.ts","names":[],"sources":["../../../src/actions.ts"],"mappings":";;;;UAKiB,aAAA;EAAA;EAEf,OAAA,EAAS,OAAA;;EAET,QAAA,EAAU,QAAA;EAAA;EAEV,IAAA;EAEgB;EAAhB,OAAA,EAAS,OAAA;AAAA;;KAIC,aAAA,iBAA8B,GAAA,EAAK,aAAA,KAAkB,CAAA,GAAI,OAAA,CAAQ,CAAA;;UAGnE,gBAAA;EACR,EAAA;EACA,OAAA,EAAS,aAAA;AAAA;;UAIM,MAAA;EATQ;EAAA,CAWtB,IAAA,aAAiB,OAAA,CAAQ,CAAA;EAXmB;EAa7C,QAAA;AAAA;;;;;;;;;;;;AAb6E;;;;;iBAoC/D,YAAA,aAAA,CAA0B,OAAA,EAAS,aAAA,CAAc,CAAA,IAAK,MAAA,CAAO,CAAA;;iBAsC7D,oBAAA,CAAA,GAAwB,GAAA,SAAY,gBAAA;;AAjEpD;;;iBAyEgB,aAAA,CAAA;;;;;iBAUA,sBAAA,CAAA,IACd,GAAA,EAAK,iBAAA,KACF,QAAA,eAAuB,OAAA,CAAQ,QAAA"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/ai.d.ts
|
|
4
|
+
interface AiPluginConfig {
|
|
5
|
+
/** App/API name. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** App description for AI agents. */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Base URL. e.g. "https://example.com" */
|
|
10
|
+
origin: string;
|
|
11
|
+
/** Contact email (required by OpenAI plugin spec). */
|
|
12
|
+
contactEmail?: string;
|
|
13
|
+
/** Legal info URL. */
|
|
14
|
+
legalUrl?: string;
|
|
15
|
+
/** Logo URL for the plugin. */
|
|
16
|
+
logoUrl?: string;
|
|
17
|
+
/** Routes directory relative to project root. Default: "src/routes" */
|
|
18
|
+
routesDir?: string;
|
|
19
|
+
/** API routes directory relative to project root. Default: "src/api" */
|
|
20
|
+
apiDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* API route descriptions — map of pattern to description.
|
|
23
|
+
* Used for llms.txt and ai-plugin.json.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* apiDescriptions: {
|
|
28
|
+
* "GET /api/posts": "List all blog posts, supports ?page=N&limit=N",
|
|
29
|
+
* "GET /api/posts/:id": "Get a single post by ID",
|
|
30
|
+
* "POST /api/posts": "Create a new post (requires auth)",
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
apiDescriptions?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Page descriptions — map of URL path to description.
|
|
37
|
+
* Used for llms.txt. Falls back to route meta.title/description.
|
|
38
|
+
*/
|
|
39
|
+
pageDescriptions?: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Additional content to append to llms.txt.
|
|
42
|
+
* Useful for authentication instructions, rate limits, etc.
|
|
43
|
+
*/
|
|
44
|
+
llmsExtra?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate llms.txt content from route files and config.
|
|
48
|
+
*
|
|
49
|
+
* Format follows the llms.txt proposal:
|
|
50
|
+
* ```
|
|
51
|
+
* # {name}
|
|
52
|
+
* > {description}
|
|
53
|
+
*
|
|
54
|
+
* ## Pages
|
|
55
|
+
* - [/about](/about): About page
|
|
56
|
+
*
|
|
57
|
+
* ## API
|
|
58
|
+
* - GET /api/posts: List posts
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @internal Exported for testing.
|
|
62
|
+
*/
|
|
63
|
+
declare function generateLlmsTxt(routeFiles: string[], apiFiles: string[], config: AiPluginConfig): string;
|
|
64
|
+
/**
|
|
65
|
+
* Generate llms-full.txt — expanded version with more detail.
|
|
66
|
+
* Includes all route metadata and API descriptions.
|
|
67
|
+
*
|
|
68
|
+
* @internal Exported for testing.
|
|
69
|
+
*/
|
|
70
|
+
declare function generateLlmsFullTxt(routeFiles: string[], apiFiles: string[], config: AiPluginConfig): string;
|
|
71
|
+
interface InferJsonLdOptions {
|
|
72
|
+
/** Page URL. */
|
|
73
|
+
url: string;
|
|
74
|
+
/** Page title. */
|
|
75
|
+
title?: string;
|
|
76
|
+
/** Page description. */
|
|
77
|
+
description?: string;
|
|
78
|
+
/** Page image. */
|
|
79
|
+
image?: string;
|
|
80
|
+
/** Site name. */
|
|
81
|
+
siteName?: string;
|
|
82
|
+
/** Page type hint. */
|
|
83
|
+
type?: 'website' | 'article' | 'product' | 'profile';
|
|
84
|
+
/** Article metadata. */
|
|
85
|
+
publishedTime?: string;
|
|
86
|
+
/** Article author. */
|
|
87
|
+
author?: string;
|
|
88
|
+
/** Article tags. */
|
|
89
|
+
tags?: string[];
|
|
90
|
+
/** Breadcrumb path segments. */
|
|
91
|
+
breadcrumbs?: Array<{
|
|
92
|
+
name: string;
|
|
93
|
+
url: string;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Auto-infer JSON-LD structured data from page metadata.
|
|
98
|
+
*
|
|
99
|
+
* Returns an array of JSON-LD objects (multiple schemas can apply to one page).
|
|
100
|
+
* For example, an article page gets both `Article` and `BreadcrumbList`.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* const schemas = inferJsonLd({
|
|
105
|
+
* url: "https://example.com/blog/my-post",
|
|
106
|
+
* title: "My Post",
|
|
107
|
+
* description: "A great article",
|
|
108
|
+
* type: "article",
|
|
109
|
+
* author: "Vit Bokisch",
|
|
110
|
+
* publishedTime: "2026-03-31",
|
|
111
|
+
* })
|
|
112
|
+
* // → [Article schema, BreadcrumbList schema]
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare function inferJsonLd(options: InferJsonLdOptions): Record<string, unknown>[];
|
|
116
|
+
/**
|
|
117
|
+
* Generate an OpenAI-compatible AI plugin manifest.
|
|
118
|
+
*
|
|
119
|
+
* Follows the /.well-known/ai-plugin.json spec.
|
|
120
|
+
*
|
|
121
|
+
* @internal Exported for testing.
|
|
122
|
+
*/
|
|
123
|
+
declare function generateAiPluginManifest(config: AiPluginConfig): Record<string, unknown>;
|
|
124
|
+
/**
|
|
125
|
+
* Generate a minimal OpenAPI 3.0 spec from API route descriptions.
|
|
126
|
+
*
|
|
127
|
+
* @internal Exported for testing.
|
|
128
|
+
*/
|
|
129
|
+
declare function generateOpenApiSpec(apiFiles: string[], config: AiPluginConfig): Record<string, unknown>;
|
|
130
|
+
/**
|
|
131
|
+
* AI integration Vite plugin.
|
|
132
|
+
*
|
|
133
|
+
* Generates at build time:
|
|
134
|
+
* - `/llms.txt` — concise site summary for AI agents
|
|
135
|
+
* - `/llms-full.txt` — detailed reference for AI agents
|
|
136
|
+
* - `/.well-known/ai-plugin.json` — OpenAI plugin manifest
|
|
137
|
+
* - `/.well-known/openapi.yaml` — minimal OpenAPI spec from API routes
|
|
138
|
+
*
|
|
139
|
+
* In dev, serves these files via middleware.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { aiPlugin } from "@pyreon/zero/ai"
|
|
144
|
+
*
|
|
145
|
+
* export default {
|
|
146
|
+
* plugins: [
|
|
147
|
+
* aiPlugin({
|
|
148
|
+
* name: "My App",
|
|
149
|
+
* origin: "https://example.com",
|
|
150
|
+
* description: "A modern web application",
|
|
151
|
+
* apiDescriptions: {
|
|
152
|
+
* "GET /api/posts": "List blog posts",
|
|
153
|
+
* "GET /api/posts/:id": "Get post by ID",
|
|
154
|
+
* },
|
|
155
|
+
* }),
|
|
156
|
+
* ],
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare function aiPlugin(config: AiPluginConfig): Plugin;
|
|
161
|
+
//#endregion
|
|
162
|
+
export { AiPluginConfig, InferJsonLdOptions, aiPlugin, generateAiPluginManifest, generateLlmsFullTxt, generateLlmsTxt, generateOpenApiSpec, inferJsonLd };
|
|
163
|
+
//# sourceMappingURL=ai2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai2.d.ts","names":[],"sources":["../../../src/ai.ts"],"mappings":";;;UA4BiB,cAAA;EAiEO;EA/DtB,IAAA;EA8DA;EA5DA,WAAA;EA6DA;EA3DA,MAAA;EA2DsB;EAzDtB,YAAA;EAmJiC;EAjJjC,QAAA;EAoJsB;EAlJtB,OAAA;EAiJA;EA/IA,SAAA;EAgJA;EA9IA,MAAA;EA8IsB;AAsDxB;;;;;;;;;;;;EAtLE,eAAA,GAAkB,MAAA;EA0MlB;;;;EArMA,gBAAA,GAAmB,MAAA;EAqMoB;AAsBzC;;;EAtNE,SAAA;AAAA;;;;;AAwTF;;;;;;;;;AAuBA;;;;iBAzTgB,eAAA,CACd,UAAA,YACA,QAAA,YACA,MAAA,EAAQ,cAAA;;;;;;AAuYV;iBA7SgB,mBAAA,CACd,UAAA,YACA,QAAA,YACA,MAAA,EAAQ,cAAA;AAAA,UAsDO,kBAAA;EAoPuC;EAlPtD,GAAA;EAkPuB;EAhPvB,KAAA;EAgPsD;EA9OtD,WAAA;;EAEA,KAAA;;EAEA,QAAA;;EAEA,IAAA;;EAEA,aAAA;;EAEA,MAAA;;EAEA,IAAA;;EAEA,WAAA,GAAc,KAAA;IAAQ,IAAA;IAAc,GAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;;iBAsBtB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,MAAA;;;;;;;;iBAkG1C,wBAAA,CAAyB,MAAA,EAAQ,cAAA,GAAiB,MAAA;;;;;;iBAuBlD,mBAAA,CACd,QAAA,YACA,MAAA,EAAQ,cAAA,GACP,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8Ea,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,MAAA"}
|
|
@@ -1,53 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Middleware } from "@pyreon/server";
|
|
2
|
+
|
|
3
|
+
//#region src/api-routes.d.ts
|
|
2
4
|
/** HTTP methods supported by API routes. */
|
|
3
|
-
|
|
5
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
4
6
|
/** Context passed to API route handlers. */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
interface ApiContext {
|
|
8
|
+
/** The incoming request. */
|
|
9
|
+
request: Request;
|
|
10
|
+
/** Parsed URL. */
|
|
11
|
+
url: URL;
|
|
12
|
+
/** URL path. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** Dynamic route parameters (e.g., { id: "123" }). */
|
|
15
|
+
params: Record<string, string>;
|
|
16
|
+
/** Request headers. */
|
|
17
|
+
headers: Headers;
|
|
16
18
|
}
|
|
17
19
|
/** An API route handler function. */
|
|
18
|
-
|
|
20
|
+
type ApiHandler = (ctx: ApiContext) => Response | Promise<Response>;
|
|
19
21
|
/** An API route module — exports named HTTP method handlers. */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
interface ApiRouteModule {
|
|
23
|
+
GET?: ApiHandler;
|
|
24
|
+
POST?: ApiHandler;
|
|
25
|
+
PUT?: ApiHandler;
|
|
26
|
+
PATCH?: ApiHandler;
|
|
27
|
+
DELETE?: ApiHandler;
|
|
28
|
+
HEAD?: ApiHandler;
|
|
29
|
+
OPTIONS?: ApiHandler;
|
|
28
30
|
}
|
|
29
31
|
/** A registered API route entry. */
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
interface ApiRouteEntry {
|
|
33
|
+
/** URL pattern (e.g., "/api/posts/:id"). */
|
|
34
|
+
pattern: string;
|
|
35
|
+
/** The route module with method handlers. */
|
|
36
|
+
module: ApiRouteModule;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Match a URL path against an API route pattern.
|
|
38
40
|
* Returns extracted params or null if no match.
|
|
39
41
|
*/
|
|
40
|
-
|
|
42
|
+
declare function matchApiRoute(pattern: string, path: string): Record<string, string> | null;
|
|
41
43
|
/**
|
|
42
44
|
* Create a middleware that dispatches API route requests.
|
|
43
45
|
* API routes are matched by URL pattern and HTTP method.
|
|
44
46
|
*/
|
|
45
|
-
|
|
47
|
+
declare function createApiMiddleware(routes: ApiRouteEntry[]): Middleware;
|
|
46
48
|
/**
|
|
47
49
|
* Detect whether a route file is an API route.
|
|
48
50
|
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
49
51
|
*/
|
|
50
|
-
|
|
52
|
+
declare function isApiRoute(filePath: string): boolean;
|
|
51
53
|
/**
|
|
52
54
|
* Convert an API route file path to a URL pattern.
|
|
53
55
|
*
|
|
@@ -57,10 +59,12 @@ export declare function isApiRoute(filePath: string): boolean;
|
|
|
57
59
|
* "api/posts/[id].ts" → "/api/posts/:id"
|
|
58
60
|
* "api/[...path].ts" → "/api/:path*"
|
|
59
61
|
*/
|
|
60
|
-
|
|
62
|
+
declare function apiFilePathToPattern(filePath: string): string;
|
|
61
63
|
/**
|
|
62
64
|
* Generate a virtual module that exports API route entries.
|
|
63
65
|
* Each entry maps a URL pattern to a module with HTTP method handlers.
|
|
64
66
|
*/
|
|
65
|
-
|
|
66
|
-
//#
|
|
67
|
+
declare function generateApiRouteModule(files: string[], routesDir: string): string;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { ApiContext, ApiHandler, ApiRouteEntry, ApiRouteModule, HttpMethod, apiFilePathToPattern, createApiMiddleware, generateApiRouteModule, isApiRoute, matchApiRoute };
|
|
70
|
+
//# sourceMappingURL=api-routes2.d.ts.map
|