@fiyuu/core 0.1.0 → 0.2.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/package.json +5 -5
- package/src/artifacts.ts +328 -0
- package/src/config.ts +260 -0
- package/src/contracts.ts +247 -0
- package/src/{generator.js → generator.ts} +68 -41
- package/src/media.ts +143 -0
- package/src/reactive.ts +229 -0
- package/src/{responsive-wrapper.js → responsive-wrapper.ts} +118 -74
- package/src/responsive.ts +54 -0
- package/src/scanner.ts +289 -0
- package/src/template.ts +110 -0
- package/src/{virtual.js → virtual.tsx} +16 -7
- package/LICENSE +0 -674
- package/README.md +0 -194
- package/src/artifacts.d.ts +0 -20
- package/src/artifacts.js +0 -274
- package/src/client.js +0 -8
- package/src/config.d.ts +0 -179
- package/src/config.js +0 -58
- package/src/contracts.d.ts +0 -176
- package/src/contracts.js +0 -45
- package/src/generator.d.ts +0 -2
- package/src/index.js +0 -11
- package/src/media.d.ts +0 -44
- package/src/media.js +0 -87
- package/src/reactive.d.ts +0 -53
- package/src/reactive.js +0 -160
- package/src/responsive-wrapper.d.ts +0 -18
- package/src/responsive.d.ts +0 -15
- package/src/responsive.js +0 -48
- package/src/scanner.d.ts +0 -65
- package/src/scanner.js +0 -200
- package/src/state.js +0 -1
- package/src/template.d.ts +0 -48
- package/src/template.js +0 -98
- package/src/virtual.d.ts +0 -14
- /package/src/{client.d.ts → client.ts} +0 -0
- /package/src/{index.d.ts → index.ts} +0 -0
- /package/src/{state.d.ts → state.ts} +0 -0
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export type AnyZodSchema = z.ZodTypeAny;
|
|
4
|
+
|
|
5
|
+
export interface SchemaContract<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema> {
|
|
6
|
+
input: TInput;
|
|
7
|
+
output: TOutput;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ActionDefinition<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema>
|
|
12
|
+
extends SchemaContract<TInput, TOutput> {
|
|
13
|
+
kind: "action";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface QueryDefinition<TInput extends AnyZodSchema = AnyZodSchema, TOutput extends AnyZodSchema = AnyZodSchema>
|
|
17
|
+
extends SchemaContract<TInput, TOutput> {
|
|
18
|
+
kind: "query";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface QueryCacheConfig {
|
|
22
|
+
/** Cache TTL in seconds. Set to 0 to disable. */
|
|
23
|
+
ttl: number;
|
|
24
|
+
/** Cache key varies by these URL query string parameters. */
|
|
25
|
+
vary?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MetaDefinition {
|
|
29
|
+
intent: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
render?: RenderMode;
|
|
32
|
+
/**
|
|
33
|
+
* Mark this page as zero-JS.
|
|
34
|
+
* `fiyuu doctor` will warn if <script> tags are detected in page.tsx.
|
|
35
|
+
*/
|
|
36
|
+
noJs?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Revalidate interval in seconds for `render: "ssg"` pages.
|
|
39
|
+
* Works like ISR: stale HTML is served immediately and refreshed in background.
|
|
40
|
+
*/
|
|
41
|
+
revalidate?: number;
|
|
42
|
+
seo?: {
|
|
43
|
+
title?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PageDefinition {
|
|
49
|
+
kind: "page";
|
|
50
|
+
intent: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LayoutDefinition {
|
|
54
|
+
kind: "layout";
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type RenderMode = "ssr" | "csr" | "ssg";
|
|
59
|
+
|
|
60
|
+
export interface PageProps<TData = unknown> {
|
|
61
|
+
data: TData | null;
|
|
62
|
+
route: string;
|
|
63
|
+
intent: string;
|
|
64
|
+
render: RenderMode;
|
|
65
|
+
/** Dynamic route parameters extracted from the URL, e.g. { id: "42" } for /blog/[id] */
|
|
66
|
+
params: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LayoutProps {
|
|
70
|
+
children: string;
|
|
71
|
+
route: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function defineAction<TInput extends AnyZodSchema, TOutput extends AnyZodSchema>(config: SchemaContract<TInput, TOutput>): ActionDefinition<TInput, TOutput> {
|
|
75
|
+
return {
|
|
76
|
+
kind: "action",
|
|
77
|
+
...config,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function defineQuery<TInput extends AnyZodSchema, TOutput extends AnyZodSchema>(config: SchemaContract<TInput, TOutput>): QueryDefinition<TInput, TOutput> {
|
|
82
|
+
return {
|
|
83
|
+
kind: "query",
|
|
84
|
+
...config,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function definePage(config: { intent: string }): PageDefinition {
|
|
89
|
+
return {
|
|
90
|
+
kind: "page",
|
|
91
|
+
...config,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function defineLayout(config: { name?: string } = {}): LayoutDefinition {
|
|
96
|
+
return {
|
|
97
|
+
kind: "layout",
|
|
98
|
+
...config,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defineMeta(config: MetaDefinition): MetaDefinition {
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Type inference helpers ─────────────────────────────────────────────────
|
|
107
|
+
//
|
|
108
|
+
// Eliminate type duplication between Zod schemas and TypeScript code.
|
|
109
|
+
// Define your schema once in query.ts / action.ts — infer everywhere else.
|
|
110
|
+
//
|
|
111
|
+
// @example page.tsx
|
|
112
|
+
// import { query } from "./query.js";
|
|
113
|
+
// type PageData = InferQueryOutput<typeof query>; // no manual type needed
|
|
114
|
+
//
|
|
115
|
+
// @example action.ts execute()
|
|
116
|
+
// export async function execute({ input, request }: ActionContext<typeof action>) { ... }
|
|
117
|
+
// // input is fully typed from the action's Zod input schema
|
|
118
|
+
|
|
119
|
+
/** TypeScript output type inferred from a QueryDefinition's output Zod schema. */
|
|
120
|
+
export type InferQueryOutput<T extends QueryDefinition> = z.infer<T["output"]>;
|
|
121
|
+
|
|
122
|
+
/** TypeScript input type inferred from a QueryDefinition's input Zod schema. */
|
|
123
|
+
export type InferQueryInput<T extends QueryDefinition> = z.infer<T["input"]>;
|
|
124
|
+
|
|
125
|
+
/** TypeScript input type inferred from an ActionDefinition's input Zod schema. */
|
|
126
|
+
export type InferActionInput<T extends ActionDefinition> = z.infer<T["input"]>;
|
|
127
|
+
|
|
128
|
+
/** TypeScript output type inferred from an ActionDefinition's output Zod schema. */
|
|
129
|
+
export type InferActionOutput<T extends ActionDefinition> = z.infer<T["output"]>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Context passed to query `execute()` functions.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* export async function execute({ request, params }: QueryContext) {
|
|
136
|
+
* const sessionId = getSessionIdFromRequest(request as any);
|
|
137
|
+
* ...
|
|
138
|
+
* }
|
|
139
|
+
*/
|
|
140
|
+
export interface QueryContext {
|
|
141
|
+
/** Incoming HTTP request (Node.js IncomingMessage). Cast to `any` if you need raw access. */
|
|
142
|
+
request: unknown;
|
|
143
|
+
/** Dynamic URL parameters, e.g. { id: "42" } for route /blog/[id] */
|
|
144
|
+
params: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Context passed to action `execute()` functions.
|
|
149
|
+
* Pass `typeof action` as the generic to get fully typed `input`.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* export const action = defineAction({ input: z.object({ title: z.string() }), ... });
|
|
153
|
+
*
|
|
154
|
+
* export async function execute({ input, request }: ActionContext<typeof action>) {
|
|
155
|
+
* // input.title is typed as string — no manual type annotation needed
|
|
156
|
+
* }
|
|
157
|
+
*/
|
|
158
|
+
export interface ActionContext<T extends ActionDefinition = ActionDefinition> {
|
|
159
|
+
/** Action input, typed from the action's Zod input schema. */
|
|
160
|
+
input: z.infer<T["input"]>;
|
|
161
|
+
/** Incoming HTTP request (Node.js IncomingMessage). Cast to `any` if you need raw access. */
|
|
162
|
+
request: unknown;
|
|
163
|
+
/** Dynamic URL parameters, e.g. { id: "42" } for route /blog/[id] */
|
|
164
|
+
params: Record<string, string>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Middleware ─────────────────────────────────────────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// Use `defineMiddleware` to get full type inference without importing from
|
|
170
|
+
// @fiyuu/runtime. All types use `unknown` for Node.js primitives so that
|
|
171
|
+
// @fiyuu/core stays free of @types/node as a required peer dependency.
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Context object passed to every middleware handler.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* export const middleware = defineMiddleware(async ({ url, request }, next) => {
|
|
178
|
+
* if (url.pathname === "/secret") { ... }
|
|
179
|
+
* await next();
|
|
180
|
+
* });
|
|
181
|
+
*/
|
|
182
|
+
export interface FiyuuMiddlewareContext {
|
|
183
|
+
/** Incoming HTTP request — Node.js `IncomingMessage`. */
|
|
184
|
+
request: unknown;
|
|
185
|
+
/** Parsed request URL. */
|
|
186
|
+
url: URL;
|
|
187
|
+
/** Mutable response headers that will be sent with the final response. */
|
|
188
|
+
responseHeaders: Record<string, string>;
|
|
189
|
+
/** Unique ID for this request (useful for logging). */
|
|
190
|
+
requestId: string;
|
|
191
|
+
/** Non-fatal warnings accumulated during request handling. */
|
|
192
|
+
warnings: string[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return this from a middleware to short-circuit the request.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* // Redirect to login
|
|
200
|
+
* return {
|
|
201
|
+
* headers: { Location: "/auth" },
|
|
202
|
+
* response: { status: 302, body: "" },
|
|
203
|
+
* };
|
|
204
|
+
*/
|
|
205
|
+
export interface FiyuuMiddlewareResult {
|
|
206
|
+
/** Extra response headers to send (e.g. `Location` for redirects). */
|
|
207
|
+
headers?: Record<string, string>;
|
|
208
|
+
response?: {
|
|
209
|
+
/** HTTP status code. */
|
|
210
|
+
status?: number;
|
|
211
|
+
/** JSON body — serialised automatically. */
|
|
212
|
+
json?: unknown;
|
|
213
|
+
/** Raw string body. */
|
|
214
|
+
body?: string;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Call this to continue to the next middleware or the route handler. */
|
|
219
|
+
export type FiyuuMiddlewareNext = () => Promise<void>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A single middleware function.
|
|
223
|
+
* Return `FiyuuMiddlewareResult` to short-circuit, or call `next()` to continue.
|
|
224
|
+
*/
|
|
225
|
+
export type FiyuuMiddlewareHandler = (
|
|
226
|
+
context: FiyuuMiddlewareContext,
|
|
227
|
+
next: FiyuuMiddlewareNext,
|
|
228
|
+
) => Promise<FiyuuMiddlewareResult | void> | FiyuuMiddlewareResult | void;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Wraps your middleware function and gives it full type inference.
|
|
232
|
+
* Import from `@fiyuu/core` — no need to touch `@fiyuu/runtime`.
|
|
233
|
+
*
|
|
234
|
+
* @example app/middleware.ts
|
|
235
|
+
* import { defineMiddleware } from "@fiyuu/core";
|
|
236
|
+
*
|
|
237
|
+
* export const middleware = defineMiddleware(async ({ url, request }, next) => {
|
|
238
|
+
* if (url.pathname.startsWith("/dashboard")) {
|
|
239
|
+
* const user = await getSessionUser(request);
|
|
240
|
+
* if (!user) return { headers: { Location: "/auth" }, response: { status: 302, body: "" } };
|
|
241
|
+
* }
|
|
242
|
+
* await next();
|
|
243
|
+
* });
|
|
244
|
+
*/
|
|
245
|
+
export function defineMiddleware(handler: FiyuuMiddlewareHandler): FiyuuMiddlewareHandler {
|
|
246
|
+
return handler;
|
|
247
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
|
|
3
4
|
const PAGE_TEMPLATE = `import { Component } from "@geajs/core";
|
|
4
5
|
import { definePage, html, optimizedImage, responsiveStyle, fluid, type PageProps } from "fiyuu/client";
|
|
5
6
|
|
|
@@ -28,6 +29,7 @@ export default class {{Title}}Page extends Component<PageProps<{{Title}}Data>> {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
`;
|
|
32
|
+
|
|
31
33
|
const ACTION_TEMPLATE = `import { z } from "zod";
|
|
32
34
|
import { defineAction } from "fiyuu/client";
|
|
33
35
|
|
|
@@ -41,6 +43,7 @@ export async function execute(input: Record<string, unknown>) {
|
|
|
41
43
|
return { success: true };
|
|
42
44
|
}
|
|
43
45
|
`;
|
|
46
|
+
|
|
44
47
|
const QUERY_TEMPLATE = `import { z } from "zod";
|
|
45
48
|
import { defineQuery } from "fiyuu/client";
|
|
46
49
|
|
|
@@ -54,6 +57,7 @@ export async function execute() {
|
|
|
54
57
|
return {};
|
|
55
58
|
}
|
|
56
59
|
`;
|
|
60
|
+
|
|
57
61
|
const SCHEMA_TEMPLATE = `import { z } from "zod";
|
|
58
62
|
|
|
59
63
|
export const input = z.object({});
|
|
@@ -64,6 +68,7 @@ export const output = z.object({
|
|
|
64
68
|
|
|
65
69
|
export const description = "{{description}}";
|
|
66
70
|
`;
|
|
71
|
+
|
|
67
72
|
const META_TEMPLATE = `import { defineMeta } from "fiyuu/client";
|
|
68
73
|
|
|
69
74
|
export default defineMeta({
|
|
@@ -76,49 +81,71 @@ export default defineMeta({
|
|
|
76
81
|
},
|
|
77
82
|
});
|
|
78
83
|
`;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
|
|
85
|
+
export async function generatePageFeature(appDirectory: string, featureName: string): Promise<string[]> {
|
|
86
|
+
const featureDirectory = path.join(appDirectory, featureName);
|
|
87
|
+
await fs.mkdir(featureDirectory, { recursive: true });
|
|
88
|
+
|
|
89
|
+
const title = titleCase(featureName);
|
|
90
|
+
const intent = `${title} page for user-facing interactions`;
|
|
91
|
+
const description = `Fetches data for ${title}`;
|
|
92
|
+
const files = [
|
|
93
|
+
["page.tsx", PAGE_TEMPLATE],
|
|
94
|
+
["query.ts", QUERY_TEMPLATE],
|
|
95
|
+
["schema.ts", SCHEMA_TEMPLATE],
|
|
96
|
+
["meta.ts", META_TEMPLATE],
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
await Promise.all(
|
|
100
|
+
files.map(([fileName, template]) =>
|
|
101
|
+
fs.writeFile(path.join(featureDirectory, fileName), hydrate(template, { title, intent, description, Title: pascalCase(featureName) })),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return files.map(([fileName]) => path.join(featureDirectory, fileName));
|
|
93
106
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
|
|
108
|
+
export async function generateActionFeature(appDirectory: string, featureName: string): Promise<string[]> {
|
|
109
|
+
const featureDirectory = path.join(appDirectory, featureName);
|
|
110
|
+
await fs.mkdir(featureDirectory, { recursive: true });
|
|
111
|
+
|
|
112
|
+
const title = titleCase(featureName);
|
|
113
|
+
const intent = `${title} workflow for server-side mutations`;
|
|
114
|
+
const description = `Performs ${title} mutation`;
|
|
115
|
+
const files = [
|
|
116
|
+
["action.ts", ACTION_TEMPLATE],
|
|
117
|
+
["schema.ts", SCHEMA_TEMPLATE],
|
|
118
|
+
["meta.ts", META_TEMPLATE],
|
|
119
|
+
] as const;
|
|
120
|
+
|
|
121
|
+
await Promise.all(
|
|
122
|
+
files.map(([fileName, template]) =>
|
|
123
|
+
fs.writeFile(path.join(featureDirectory, fileName), hydrate(template, { title, intent, description, Title: pascalCase(featureName) })),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return files.map(([fileName]) => path.join(featureDirectory, fileName));
|
|
107
128
|
}
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
|
|
130
|
+
function hydrate(template: string, replacements: Record<string, string>): string {
|
|
131
|
+
return Object.entries(replacements).reduce(
|
|
132
|
+
(content, [key, value]) => content.replaceAll(`{{${key}}}`, value),
|
|
133
|
+
template,
|
|
134
|
+
);
|
|
110
135
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
|
|
137
|
+
function titleCase(value: string): string {
|
|
138
|
+
return value
|
|
139
|
+
.split(/[\/-]/)
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
142
|
+
.join(" ");
|
|
117
143
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
|
|
145
|
+
function pascalCase(value: string): string {
|
|
146
|
+
return value
|
|
147
|
+
.split(/[\/-_]/)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
150
|
+
.join("");
|
|
124
151
|
}
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { escapeHtml } from "./template.js";
|
|
2
|
+
|
|
3
|
+
export interface OptimizedImageSource {
|
|
4
|
+
srcSet: string;
|
|
5
|
+
media?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
sizes?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OptimizedImageProps {
|
|
11
|
+
src: string;
|
|
12
|
+
alt: string;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
sizes?: string;
|
|
16
|
+
srcSet?: string;
|
|
17
|
+
sources?: OptimizedImageSource[];
|
|
18
|
+
loading?: "lazy" | "eager";
|
|
19
|
+
decoding?: "async" | "sync" | "auto";
|
|
20
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
21
|
+
class?: string;
|
|
22
|
+
style?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OptimizedVideoSource {
|
|
27
|
+
src: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
media?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OptimizedVideoProps {
|
|
33
|
+
src?: string;
|
|
34
|
+
sources?: OptimizedVideoSource[];
|
|
35
|
+
poster?: string;
|
|
36
|
+
preload?: "none" | "metadata" | "auto";
|
|
37
|
+
controls?: boolean;
|
|
38
|
+
muted?: boolean;
|
|
39
|
+
loop?: boolean;
|
|
40
|
+
autoPlay?: boolean;
|
|
41
|
+
playsInline?: boolean;
|
|
42
|
+
width?: number;
|
|
43
|
+
height?: number;
|
|
44
|
+
class?: string;
|
|
45
|
+
style?: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function optimizedImage(props: OptimizedImageProps): string {
|
|
50
|
+
const loading = props.loading ?? "lazy";
|
|
51
|
+
const decoding = props.decoding ?? "async";
|
|
52
|
+
const fetchPriority = props.fetchPriority ?? "auto";
|
|
53
|
+
const imageAttributes = [
|
|
54
|
+
createAttribute("src", props.src),
|
|
55
|
+
createAttribute("alt", props.alt),
|
|
56
|
+
createAttribute("loading", loading),
|
|
57
|
+
createAttribute("decoding", decoding),
|
|
58
|
+
createAttribute("fetchpriority", fetchPriority),
|
|
59
|
+
createAttribute("sizes", props.sizes),
|
|
60
|
+
createAttribute("srcset", props.srcSet),
|
|
61
|
+
createAttribute("width", props.width),
|
|
62
|
+
createAttribute("height", props.height),
|
|
63
|
+
createAttribute("class", props.class),
|
|
64
|
+
createAttribute("style", props.style),
|
|
65
|
+
createAttribute("id", props.id),
|
|
66
|
+
]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(" ");
|
|
69
|
+
|
|
70
|
+
const sourceTags = (props.sources ?? [])
|
|
71
|
+
.map((source) => {
|
|
72
|
+
const attributes = [
|
|
73
|
+
createAttribute("srcset", source.srcSet),
|
|
74
|
+
createAttribute("media", source.media),
|
|
75
|
+
createAttribute("type", source.type),
|
|
76
|
+
createAttribute("sizes", source.sizes),
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(" ");
|
|
80
|
+
return `<source ${attributes}>`;
|
|
81
|
+
})
|
|
82
|
+
.join("");
|
|
83
|
+
|
|
84
|
+
if (sourceTags.length > 0) {
|
|
85
|
+
return `<picture>${sourceTags}<img ${imageAttributes}></picture>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `<img ${imageAttributes}>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function optimizedVideo(props: OptimizedVideoProps): string {
|
|
92
|
+
const preload = props.preload ?? "metadata";
|
|
93
|
+
const controls = props.controls ?? true;
|
|
94
|
+
const playsInline = props.playsInline ?? true;
|
|
95
|
+
const sources = props.sources && props.sources.length > 0
|
|
96
|
+
? props.sources
|
|
97
|
+
: props.src
|
|
98
|
+
? [{ src: props.src }]
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
if (sources.length === 0) {
|
|
102
|
+
throw new Error("optimizedVideo requires either `src` or `sources`.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const videoAttributes = [
|
|
106
|
+
createAttribute("poster", props.poster),
|
|
107
|
+
createAttribute("preload", preload),
|
|
108
|
+
createAttribute("width", props.width),
|
|
109
|
+
createAttribute("height", props.height),
|
|
110
|
+
createAttribute("class", props.class),
|
|
111
|
+
createAttribute("style", props.style),
|
|
112
|
+
createAttribute("id", props.id),
|
|
113
|
+
controls ? "controls" : "",
|
|
114
|
+
props.muted ? "muted" : "",
|
|
115
|
+
props.loop ? "loop" : "",
|
|
116
|
+
props.autoPlay ? "autoplay" : "",
|
|
117
|
+
playsInline ? "playsinline" : "",
|
|
118
|
+
]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join(" ");
|
|
121
|
+
|
|
122
|
+
const sourceTags = sources
|
|
123
|
+
.map((source) => {
|
|
124
|
+
const attributes = [
|
|
125
|
+
createAttribute("src", source.src),
|
|
126
|
+
createAttribute("type", source.type),
|
|
127
|
+
createAttribute("media", source.media),
|
|
128
|
+
]
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.join(" ");
|
|
131
|
+
return `<source ${attributes}>`;
|
|
132
|
+
})
|
|
133
|
+
.join("");
|
|
134
|
+
|
|
135
|
+
return `<video ${videoAttributes}>${sourceTags}</video>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createAttribute(name: string, value: string | number | undefined): string {
|
|
139
|
+
if (value === undefined || value === null || value === "") {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
return `${name}="${escapeHtml(value)}"`;
|
|
143
|
+
}
|