@pyreon/zero 0.12.6 → 0.12.8
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/fs-router-Dil4IKZR.js +290 -0
- package/lib/fs-router-Dil4IKZR.js.map +1 -0
- package/lib/image-plugin.js +68 -1
- package/lib/image-plugin.js.map +1 -1
- package/lib/server.js +1337 -265
- package/lib/server.js.map +1 -1
- package/lib/types/image-plugin.d.ts +49 -1
- package/lib/types/image-plugin.d.ts.map +1 -1
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/server.d.ts +425 -1
- package/lib/types/server.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/image-plugin.ts +143 -0
- package/src/image-types.d.ts +51 -0
- package/src/server.ts +9 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/fs-router.ts
|
|
19
|
+
var fs_router_exports = /* @__PURE__ */ __exportAll({
|
|
20
|
+
filePathToUrlPath: () => filePathToUrlPath,
|
|
21
|
+
generateMiddlewareModule: () => generateMiddlewareModule,
|
|
22
|
+
generateRouteModule: () => generateRouteModule,
|
|
23
|
+
parseFileRoutes: () => parseFileRoutes,
|
|
24
|
+
scanRouteFiles: () => scanRouteFiles
|
|
25
|
+
});
|
|
26
|
+
const ROUTE_EXTENSIONS = [
|
|
27
|
+
".tsx",
|
|
28
|
+
".jsx",
|
|
29
|
+
".ts",
|
|
30
|
+
".js"
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Parse a set of file paths (relative to routes dir) into FileRoute objects.
|
|
34
|
+
*
|
|
35
|
+
* @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
|
|
36
|
+
* @param defaultMode Default rendering mode from config
|
|
37
|
+
*/
|
|
38
|
+
function parseFileRoutes(files, defaultMode = "ssr") {
|
|
39
|
+
return files.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext))).map((filePath) => parseFilePath(filePath, defaultMode)).sort(sortRoutes);
|
|
40
|
+
}
|
|
41
|
+
function parseFilePath(filePath, defaultMode) {
|
|
42
|
+
let route = filePath;
|
|
43
|
+
for (const ext of ROUTE_EXTENSIONS) if (route.endsWith(ext)) {
|
|
44
|
+
route = route.slice(0, -ext.length);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const fileName = getFileName(route);
|
|
48
|
+
const isLayout = fileName === "_layout";
|
|
49
|
+
const isError = fileName === "_error";
|
|
50
|
+
const isLoading = fileName === "_loading";
|
|
51
|
+
const isNotFound = fileName === "_404" || fileName === "_not-found";
|
|
52
|
+
const isCatchAll = route.includes("[...");
|
|
53
|
+
const parts = route.split("/");
|
|
54
|
+
parts.pop();
|
|
55
|
+
const dirPath = parts.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/");
|
|
56
|
+
const urlPath = filePathToUrlPath(route);
|
|
57
|
+
return {
|
|
58
|
+
filePath,
|
|
59
|
+
urlPath,
|
|
60
|
+
dirPath,
|
|
61
|
+
depth: urlPath === "/" ? 0 : urlPath.split("/").filter(Boolean).length,
|
|
62
|
+
isLayout,
|
|
63
|
+
isError,
|
|
64
|
+
isLoading,
|
|
65
|
+
isNotFound,
|
|
66
|
+
isCatchAll,
|
|
67
|
+
renderMode: defaultMode
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert a file path (without extension) to a URL path pattern.
|
|
72
|
+
*
|
|
73
|
+
* Examples:
|
|
74
|
+
* "index" → "/"
|
|
75
|
+
* "about" → "/about"
|
|
76
|
+
* "users/index" → "/users"
|
|
77
|
+
* "users/[id]" → "/users/:id"
|
|
78
|
+
* "blog/[...slug]" → "/blog/:slug*"
|
|
79
|
+
* "(auth)/login" → "/login" (group stripped)
|
|
80
|
+
* "_layout" → "/" (layout marker)
|
|
81
|
+
*/
|
|
82
|
+
function filePathToUrlPath(filePath) {
|
|
83
|
+
const segments = filePath.split("/");
|
|
84
|
+
const urlSegments = [];
|
|
85
|
+
for (const seg of segments) {
|
|
86
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
87
|
+
if (seg === "_layout" || seg === "_error" || seg === "_loading" || seg === "_404" || seg === "_not-found") continue;
|
|
88
|
+
if (seg === "index") continue;
|
|
89
|
+
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
|
|
90
|
+
if (catchAll) {
|
|
91
|
+
urlSegments.push(`:${catchAll[1]}*`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const dynamic = seg.match(/^\[(\w+)\]$/);
|
|
95
|
+
if (dynamic) {
|
|
96
|
+
urlSegments.push(`:${dynamic[1]}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
urlSegments.push(seg);
|
|
100
|
+
}
|
|
101
|
+
return `/${urlSegments.join("/")}` || "/";
|
|
102
|
+
}
|
|
103
|
+
/** Sort routes: static before dynamic, catch-all last. */
|
|
104
|
+
function sortRoutes(a, b) {
|
|
105
|
+
if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1;
|
|
106
|
+
if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1;
|
|
107
|
+
const aDynamic = a.urlPath.includes(":");
|
|
108
|
+
if (aDynamic !== b.urlPath.includes(":")) return aDynamic ? 1 : -1;
|
|
109
|
+
return a.urlPath.localeCompare(b.urlPath);
|
|
110
|
+
}
|
|
111
|
+
function getFileName(filePath) {
|
|
112
|
+
const parts = filePath.split("/");
|
|
113
|
+
return parts[parts.length - 1] ?? "";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Group flat file routes into a directory tree.
|
|
117
|
+
*/
|
|
118
|
+
function getOrCreateChild(node, segment) {
|
|
119
|
+
let child = node.children.get(segment);
|
|
120
|
+
if (!child) {
|
|
121
|
+
child = {
|
|
122
|
+
pages: [],
|
|
123
|
+
children: /* @__PURE__ */ new Map()
|
|
124
|
+
};
|
|
125
|
+
node.children.set(segment, child);
|
|
126
|
+
}
|
|
127
|
+
return child;
|
|
128
|
+
}
|
|
129
|
+
function resolveNode(root, dirPath) {
|
|
130
|
+
let node = root;
|
|
131
|
+
if (dirPath) for (const segment of dirPath.split("/")) node = getOrCreateChild(node, segment);
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
function placeRoute(node, route) {
|
|
135
|
+
if (route.isLayout) node.layout = route;
|
|
136
|
+
else if (route.isError) node.error = route;
|
|
137
|
+
else if (route.isLoading) node.loading = route;
|
|
138
|
+
else if (route.isNotFound) node.notFound = route;
|
|
139
|
+
else node.pages.push(route);
|
|
140
|
+
}
|
|
141
|
+
function buildRouteTree(routes) {
|
|
142
|
+
const root = {
|
|
143
|
+
pages: [],
|
|
144
|
+
children: /* @__PURE__ */ new Map()
|
|
145
|
+
};
|
|
146
|
+
for (const route of routes) placeRoute(resolveNode(root, route.dirPath), route);
|
|
147
|
+
return root;
|
|
148
|
+
}
|
|
149
|
+
function generateRouteModule(files, routesDir, options) {
|
|
150
|
+
const tree = buildRouteTree(parseFileRoutes(files));
|
|
151
|
+
const imports = [];
|
|
152
|
+
let importCounter = 0;
|
|
153
|
+
const useStaticImports = options?.staticImports ?? false;
|
|
154
|
+
function nextImport(filePath, exportName = "default") {
|
|
155
|
+
const name = `_${importCounter++}`;
|
|
156
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
157
|
+
if (exportName === "default") imports.push(`import ${name} from "${fullPath}"`);
|
|
158
|
+
else imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`);
|
|
159
|
+
return name;
|
|
160
|
+
}
|
|
161
|
+
function nextLazy(filePath, loadingName, errorName) {
|
|
162
|
+
const name = `_${importCounter++}`;
|
|
163
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
164
|
+
if (useStaticImports) imports.push(`import ${name} from "${fullPath}"`);
|
|
165
|
+
else {
|
|
166
|
+
const opts = [];
|
|
167
|
+
if (loadingName) opts.push(`loading: ${loadingName}`);
|
|
168
|
+
if (errorName) opts.push(`error: ${errorName}`);
|
|
169
|
+
const optsStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
|
|
170
|
+
imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`);
|
|
171
|
+
}
|
|
172
|
+
return name;
|
|
173
|
+
}
|
|
174
|
+
function nextModuleImport(filePath) {
|
|
175
|
+
const name = `_m${importCounter++}`;
|
|
176
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
177
|
+
imports.push(`import * as ${name} from "${fullPath}"`);
|
|
178
|
+
return name;
|
|
179
|
+
}
|
|
180
|
+
function generatePageRoute(page, indent, loadingName, errorName, notFoundName) {
|
|
181
|
+
const mod = nextModuleImport(page.filePath);
|
|
182
|
+
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
183
|
+
const props = [
|
|
184
|
+
`${indent} path: ${JSON.stringify(page.urlPath)}`,
|
|
185
|
+
`${indent} component: ${comp}`,
|
|
186
|
+
`${indent} loader: ${mod}.loader`,
|
|
187
|
+
`${indent} beforeEnter: ${mod}.guard`,
|
|
188
|
+
`${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`
|
|
189
|
+
];
|
|
190
|
+
if (errorName) props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`);
|
|
191
|
+
if (notFoundName) props.push(`${indent} notFoundComponent: ${notFoundName}`);
|
|
192
|
+
return `${indent}{\n${props.join(",\n")}\n${indent}}`;
|
|
193
|
+
}
|
|
194
|
+
function wrapWithLayout(node, children, indent, errorName, notFoundName) {
|
|
195
|
+
const layout = node.layout;
|
|
196
|
+
const layoutMod = nextModuleImport(layout.filePath);
|
|
197
|
+
const layoutComp = nextImport(layout.filePath, "layout");
|
|
198
|
+
const props = [
|
|
199
|
+
`${indent}path: ${JSON.stringify(layout.urlPath)}`,
|
|
200
|
+
`${indent}component: ${layoutComp}`,
|
|
201
|
+
`${indent}loader: ${layoutMod}.loader`,
|
|
202
|
+
`${indent}beforeEnter: ${layoutMod}.guard`,
|
|
203
|
+
`${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`
|
|
204
|
+
];
|
|
205
|
+
if (errorName) props.push(`${indent}errorComponent: ${errorName}`);
|
|
206
|
+
if (notFoundName) props.push(`${indent}notFoundComponent: ${notFoundName}`);
|
|
207
|
+
if (children.length > 0) props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`);
|
|
208
|
+
return `${indent}{\n${props.map((p) => ` ${p}`).join(",\n")}\n${indent}}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate route definitions for a tree node.
|
|
212
|
+
*/
|
|
213
|
+
function generateNode(node, depth) {
|
|
214
|
+
const indent = " ".repeat(depth + 1);
|
|
215
|
+
const errorName = node.error ? nextImport(node.error.filePath) : void 0;
|
|
216
|
+
const loadingName = node.loading ? nextImport(node.loading.filePath) : void 0;
|
|
217
|
+
const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : void 0;
|
|
218
|
+
const childRouteDefs = [];
|
|
219
|
+
for (const [, childNode] of node.children) childRouteDefs.push(...generateNode(childNode, depth + 1));
|
|
220
|
+
const allChildren = [...node.pages.map((page) => generatePageRoute(page, indent, loadingName, errorName, notFoundName)), ...childRouteDefs];
|
|
221
|
+
if (node.layout) return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)];
|
|
222
|
+
return allChildren;
|
|
223
|
+
}
|
|
224
|
+
const routeDefs = generateNode(tree, 0);
|
|
225
|
+
return [
|
|
226
|
+
`import { lazy } from "@pyreon/router"`,
|
|
227
|
+
"",
|
|
228
|
+
...imports,
|
|
229
|
+
"",
|
|
230
|
+
`function clean(routes) {`,
|
|
231
|
+
` return routes.map(r => {`,
|
|
232
|
+
` const c = {}`,
|
|
233
|
+
` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,
|
|
234
|
+
` if (c.children) c.children = clean(c.children)`,
|
|
235
|
+
` return c`,
|
|
236
|
+
` })`,
|
|
237
|
+
`}`,
|
|
238
|
+
"",
|
|
239
|
+
`export const routes = clean([`,
|
|
240
|
+
routeDefs.join(",\n"),
|
|
241
|
+
`])`
|
|
242
|
+
].join("\n");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Generate a virtual module that maps URL patterns to their middleware exports.
|
|
246
|
+
* Used by the server entry to dispatch per-route middleware.
|
|
247
|
+
*/
|
|
248
|
+
function generateMiddlewareModule(files, routesDir) {
|
|
249
|
+
const routes = parseFileRoutes(files);
|
|
250
|
+
const imports = [];
|
|
251
|
+
const entries = [];
|
|
252
|
+
let counter = 0;
|
|
253
|
+
for (const route of routes) {
|
|
254
|
+
if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue;
|
|
255
|
+
const name = `_mw${counter++}`;
|
|
256
|
+
const fullPath = `${routesDir}/${route.filePath}`;
|
|
257
|
+
imports.push(`import { middleware as ${name} } from "${fullPath}"`);
|
|
258
|
+
entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`);
|
|
259
|
+
}
|
|
260
|
+
return [
|
|
261
|
+
...imports,
|
|
262
|
+
"",
|
|
263
|
+
`export const routeMiddleware = [`,
|
|
264
|
+
entries.join(",\n"),
|
|
265
|
+
`].filter(e => e.middleware)`
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Scan a directory for route files.
|
|
270
|
+
* Returns paths relative to the routes directory.
|
|
271
|
+
*/
|
|
272
|
+
async function scanRouteFiles(routesDir) {
|
|
273
|
+
const { readdir } = await import("node:fs/promises");
|
|
274
|
+
const { join, relative } = await import("node:path");
|
|
275
|
+
const files = [];
|
|
276
|
+
async function walk(dir) {
|
|
277
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const fullPath = join(dir, entry.name);
|
|
280
|
+
if (entry.isDirectory()) await walk(fullPath);
|
|
281
|
+
else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) files.push(relative(routesDir, fullPath));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
await walk(routesDir);
|
|
285
|
+
return files;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
export { parseFileRoutes as a, generateRouteModule as i, fs_router_exports as n, scanRouteFiles as o, generateMiddlewareModule as r, filePathToUrlPath as t };
|
|
290
|
+
//# sourceMappingURL=fs-router-Dil4IKZR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-router-Dil4IKZR.js","names":[],"sources":["../src/fs-router.ts"],"sourcesContent":["import type { FileRoute, RenderMode } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper — must use <RouterView /> to render child routes\n// (props.children is NOT passed — the router handles nesting)\n// _error → error component\n// _loading → loading component\n// _404 → not-found component (renders on 404)\n// _not-found → alias for _404\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n */\nexport function parseFileRoutes(files: string[], defaultMode: RenderMode = 'ssr'): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => parseFilePath(filePath, defaultMode))\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isNotFound = fileName === '_404' || fileName === '_not-found'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isNotFound,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading' || seg === '_404' || seg === '_not-found') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :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 const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Not-found (404) file (if any). */\n notFound?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else if (route.isNotFound) node.notFound = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport interface GenerateRouteModuleOptions {\n /**\n * When true, skip lazy() for route components and use static imports.\n * Use for SSG/prerender mode where all routes are rendered at build time\n * and code splitting provides no benefit. Avoids Rolldown warnings about\n * static + dynamic imports of the same module.\n */\n staticImports?: boolean\n}\n\nexport function generateRouteModule(\n files: string[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n const routes = parseFileRoutes(files)\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n const useStaticImports = options?.staticImports ?? false\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n\n if (useStaticImports) {\n // SSG mode: static import avoids Rolldown warnings about\n // static + dynamic imports of the same module\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n }\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const mod = nextModuleImport(page.filePath)\n const comp = nextLazy(page.filePath, loadingName, errorName)\n\n const props: string[] = [\n `${indent} path: ${JSON.stringify(page.urlPath)}`,\n `${indent} component: ${comp}`,\n `${indent} loader: ${mod}.loader`,\n `${indent} beforeEnter: ${mod}.guard`,\n `${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`,\n ]\n\n // Only emit errorComponent when there's an actual _error file in scope\n // or the route module exports an error component. Avoids referencing\n // undefined .error exports that produce noisy bundler warnings.\n if (errorName) {\n props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`)\n }\n\n if (notFoundName) {\n props.push(`${indent} notFoundComponent: ${notFoundName}`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const layoutMod = nextModuleImport(layout.filePath)\n const layoutComp = nextImport(layout.filePath, 'layout')\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n `${indent}loader: ${layoutMod}.loader`,\n `${indent}beforeEnter: ${layoutMod}.guard`,\n `${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`,\n ]\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (notFoundName) {\n props.push(`${indent}notFoundComponent: ${notFoundName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName, notFoundName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n return [\n `import { lazy } from \"@pyreon/router\"`,\n '',\n ...imports,\n '',\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n ].join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n"],"mappings":"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"}
|
package/lib/image-plugin.js
CHANGED
|
@@ -9,6 +9,13 @@ function warnSharpMissing() {
|
|
|
9
9
|
sharpWarned = true;
|
|
10
10
|
console.warn("\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n");
|
|
11
11
|
}
|
|
12
|
+
/** Built-in CDN providers. */
|
|
13
|
+
const cdnProviders = {
|
|
14
|
+
cloudinary: (cloudName) => (src, { width, quality, format }) => `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,
|
|
15
|
+
imgix: (domain) => (src, { width, quality, format }) => `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,
|
|
16
|
+
vercel: () => (src, { width, quality }) => `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
|
|
17
|
+
bunny: (pullZone) => (src, { width, quality }) => `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`
|
|
18
|
+
};
|
|
12
19
|
const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i;
|
|
13
20
|
/**
|
|
14
21
|
* Zero image processing Vite plugin.
|
|
@@ -43,8 +50,11 @@ function imagePlugin(config = {}) {
|
|
|
43
50
|
const defaultFormats = config.formats ?? ["webp"];
|
|
44
51
|
const quality = config.quality ?? 80;
|
|
45
52
|
const placeholderSize = config.placeholderSize ?? 16;
|
|
53
|
+
const placeholderStrategy = config.placeholder ?? "blur";
|
|
46
54
|
const outSubDir = config.outDir ?? "assets/img";
|
|
47
55
|
const include = config.include ?? IMAGE_EXT_RE;
|
|
56
|
+
const cdn = config.cdn;
|
|
57
|
+
const svgOpts = config.svg === true ? { currentColor: true } : config.svg === false || config.svg === void 0 ? false : config.svg;
|
|
48
58
|
let root = "";
|
|
49
59
|
let outDir = "";
|
|
50
60
|
let isBuild = false;
|
|
@@ -57,13 +67,70 @@ function imagePlugin(config = {}) {
|
|
|
57
67
|
isBuild = resolvedConfig.command === "build";
|
|
58
68
|
},
|
|
59
69
|
async resolveId(id) {
|
|
70
|
+
if (svgOpts && id.includes("?component") && id.split("?")[0].endsWith(".svg")) return `\0virtual:zero-svg:${id}`;
|
|
60
71
|
if (id.includes("?optimize") && include.test(id.split("?")[0])) return `\0virtual:zero-image:${id}`;
|
|
61
72
|
return null;
|
|
62
73
|
},
|
|
63
74
|
async load(id) {
|
|
75
|
+
if (id.startsWith("\0virtual:zero-svg:")) {
|
|
76
|
+
const rawPath = id.replace("\0virtual:zero-svg:", "").split("?")[0] ?? id;
|
|
77
|
+
const absPath = rawPath.startsWith("/") ? join(root, rawPath) : rawPath;
|
|
78
|
+
if (!existsSync(absPath)) return null;
|
|
79
|
+
let svg = await readFile(absPath, "utf-8");
|
|
80
|
+
if (svgOpts && svgOpts.currentColor !== false) svg = svg.replace(/fill="(?!none)[^"]*"/g, "fill=\"currentColor\"").replace(/stroke="(?!none)[^"]*"/g, "stroke=\"currentColor\"");
|
|
81
|
+
const defaultSize = svgOpts && svgOpts.defaultSize;
|
|
82
|
+
if (defaultSize && !svg.includes("width=")) svg = svg.replace("<svg", `<svg width="${defaultSize}" height="${defaultSize}"`);
|
|
83
|
+
return `
|
|
84
|
+
import { h } from '@pyreon/core'
|
|
85
|
+
const _svg = ${JSON.stringify(svg)}
|
|
86
|
+
export default function SvgComponent(props) {
|
|
87
|
+
const el = h('span', {
|
|
88
|
+
...props,
|
|
89
|
+
dangerouslySetInnerHTML: { __html: _svg },
|
|
90
|
+
style: [
|
|
91
|
+
'display:inline-flex;align-items:center;justify-content:center',
|
|
92
|
+
props.width ? 'width:' + props.width + 'px' : '',
|
|
93
|
+
props.height ? 'height:' + props.height + 'px' : '',
|
|
94
|
+
props.style || '',
|
|
95
|
+
].filter(Boolean).join(';'),
|
|
96
|
+
})
|
|
97
|
+
return el
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
64
101
|
if (!id.startsWith("\0virtual:zero-image:")) return null;
|
|
65
102
|
const rawPath = id.replace("\0virtual:zero-image:", "").split("?")[0] ?? id;
|
|
66
103
|
const absPath = rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath;
|
|
104
|
+
if (cdn) {
|
|
105
|
+
const metadata = await getImageMetadata(absPath);
|
|
106
|
+
const sources = defaultWidths.map((w) => ({
|
|
107
|
+
src: cdn(rawPath, {
|
|
108
|
+
width: w,
|
|
109
|
+
quality,
|
|
110
|
+
format: defaultFormats[0]
|
|
111
|
+
}) ?? rawPath,
|
|
112
|
+
width: w,
|
|
113
|
+
format: defaultFormats[0]
|
|
114
|
+
}));
|
|
115
|
+
const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(", ");
|
|
116
|
+
const result = {
|
|
117
|
+
src: sources[sources.length - 1]?.src ?? rawPath,
|
|
118
|
+
srcset,
|
|
119
|
+
width: metadata.width,
|
|
120
|
+
height: metadata.height,
|
|
121
|
+
placeholder: placeholderStrategy === "none" ? "" : await generateBlurPlaceholder(absPath, placeholderSize),
|
|
122
|
+
formats: defaultFormats.map((fmt) => ({
|
|
123
|
+
type: `image/${fmt}`,
|
|
124
|
+
srcset: defaultWidths.map((w) => `${cdn(rawPath, {
|
|
125
|
+
width: w,
|
|
126
|
+
quality,
|
|
127
|
+
format: fmt
|
|
128
|
+
}) ?? rawPath} ${w}w`).join(", ")
|
|
129
|
+
})),
|
|
130
|
+
sources
|
|
131
|
+
};
|
|
132
|
+
return `export default ${JSON.stringify(result)}`;
|
|
133
|
+
}
|
|
67
134
|
if (!isBuild) {
|
|
68
135
|
const result = await loadDevImage(absPath, rawPath, placeholderSize);
|
|
69
136
|
return `export default ${JSON.stringify(result)}`;
|
|
@@ -285,5 +352,5 @@ async function generateBlurPlaceholder(input, size) {
|
|
|
285
352
|
}
|
|
286
353
|
|
|
287
354
|
//#endregion
|
|
288
|
-
export { imagePlugin, parseJpegDimensions, parseWebPDimensions };
|
|
355
|
+
export { cdnProviders, imagePlugin, parseJpegDimensions, parseWebPDimensions };
|
|
289
356
|
//# sourceMappingURL=image-plugin.js.map
|
package/lib/image-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-plugin.js","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { basename, extname, join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Image processing Vite plugin ──────────────────────────────────────────\n//\n// Processes images at build time:\n// - Generates multiple sizes for responsive srcset\n// - Converts to modern formats (WebP, AVIF)\n// - Creates tiny blur placeholders (base64 inline)\n// - Outputs optimized images to the build directory\n//\n// Usage in code:\n// import heroImg from \"./hero.jpg?optimize\"\n// // → { src, srcset, width, height, placeholder }\n//\n// Or use the component helper:\n// import { Image } from \"@pyreon/zero/image\"\n// <Image src=\"/hero.jpg\" width={1920} height={1080} optimize />\n\nexport interface ImagePluginConfig {\n /** Output directory for processed images. Default: \"assets/img\" */\n outDir?: string\n /** Default widths for responsive images. Default: [640, 1024, 1920] */\n widths?: number[]\n /** Output formats. Default: [\"webp\"] */\n formats?: ImageFormat[]\n /** Quality for lossy formats (1-100). Default: 80 */\n quality?: number\n /** Blur placeholder size in px. Default: 16 */\n placeholderSize?: number\n /** File patterns to process. Default: /\\.(jpe?g|png|webp|avif)$/i */\n include?: RegExp\n}\n\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\n/** Per-format source set for <picture> <source> elements. */\nexport interface FormatSource {\n /** MIME type. e.g. \"image/webp\", \"image/avif\" */\n type: string\n /** srcset string for this format. e.g. \"/img-640.webp 640w, /img-1920.webp 1920w\" */\n srcset: string\n}\n\nexport interface ProcessedImage {\n /** Fallback source path (last format, largest width). */\n src: string\n /** Fallback srcset string (last format). */\n srcset: string\n /** Intrinsic width. */\n width: number\n /** Intrinsic height. */\n height: number\n /** Base64 blur placeholder data URI. */\n placeholder: string\n /** Per-format source sets for <picture> element. Ordered by priority (best format first). */\n formats: FormatSource[]\n /** Flat list of all sources. */\n sources: Array<{ src: string; width: number; format: string }>\n}\n\nconst IMAGE_EXT_RE = /\\.(jpe?g|png|webp|avif)$/i\n\n/**\n * Zero image processing Vite plugin.\n *\n * Transforms image imports with query params into optimized responsive images:\n *\n * @example\n * // vite.config.ts\n * import { imagePlugin } from \"@pyreon/zero/image-plugin\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),\n * ],\n * }\n *\n * @example\n * // In a component — import with ?optimize\n * import hero from \"./images/hero.jpg?optimize\"\n * // hero = { src, srcset, width, height, placeholder }\n *\n * <Image {...hero} alt=\"Hero\" priority />\n */\nexport function imagePlugin(config: ImagePluginConfig = {}): Plugin {\n const defaultWidths = config.widths ?? [640, 1024, 1920]\n const defaultFormats = config.formats ?? ['webp']\n const quality = config.quality ?? 80\n const placeholderSize = config.placeholderSize ?? 16\n const outSubDir = config.outDir ?? 'assets/img'\n const include = config.include ?? IMAGE_EXT_RE\n\n let root = ''\n let outDir = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-images',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n outDir = resolvedConfig.build.outDir\n isBuild = resolvedConfig.command === 'build'\n },\n\n async resolveId(id) {\n // Handle ?optimize query on image imports\n if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {\n return `\\0virtual:zero-image:${id}`\n }\n return null\n },\n\n async load(id) {\n if (!id.startsWith('\\0virtual:zero-image:')) return null\n\n const rawPath = id.replace('\\0virtual:zero-image:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath\n\n if (!isBuild) {\n const result = await loadDevImage(absPath, rawPath, placeholderSize)\n return `export default ${JSON.stringify(result)}`\n }\n\n const processed = await processImage(absPath, {\n widths: defaultWidths,\n formats: defaultFormats,\n quality,\n placeholderSize,\n outSubDir,\n outDir: join(root, outDir),\n })\n\n await emitProcessedSources(processed, outSubDir, this)\n rebuildFormatSrcsets(processed, absPath)\n\n return `export default ${JSON.stringify(processed)}`\n },\n }\n}\n\nasync function loadDevImage(\n absPath: string,\n rawPath: string,\n placeholderSize: number,\n): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`\n\n return {\n src: publicPath,\n srcset: '',\n width: metadata.width,\n height: metadata.height,\n placeholder: await generateBlurPlaceholder(absPath, placeholderSize),\n formats: [],\n sources: [{ src: publicPath, width: metadata.width, format: 'original' }],\n }\n}\n\nasync function emitProcessedSources(\n processed: ProcessedImage,\n outSubDir: string,\n ctx: {\n emitFile: (f: { type: 'asset'; fileName: string; source: Uint8Array }) => void\n },\n) {\n for (const source of processed.sources) {\n const fileName = join(outSubDir, basename(source.src))\n const content = await readFile(source.src)\n ctx.emitFile({ type: 'asset', fileName, source: content })\n source.src = `/${fileName}`\n }\n}\n\nfunction rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {\n const formatGroups = new Map<string, string[]>()\n for (const s of processed.sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push(`${s.src} ${s.width}w`)\n }\n processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({\n type: `image/${fmt}`,\n srcset: entries.join(', '),\n }))\n\n const lastFormat = processed.formats.at(-1)\n processed.srcset = lastFormat?.srcset ?? ''\n processed.src = processed.sources.at(-1)?.src ?? fallbackPath\n}\n\n// ─── Image processing utilities ─────────────────────────────────────────────\n\ninterface ProcessOptions {\n widths: number[]\n formats: ImageFormat[]\n quality: number\n placeholderSize: number\n outSubDir: string\n outDir: string\n}\n\nasync function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const ext = extname(absPath)\n const name = basename(absPath, ext)\n const sources: Array<{ src: string; width: number; format: string }> = []\n\n // Ensure output directory exists\n const processedDir = join(opts.outDir, opts.outSubDir)\n if (!existsSync(processedDir)) {\n await mkdir(processedDir, { recursive: true })\n }\n\n // Generate resized variants — iterate formats first so sources are grouped by format\n for (const format of opts.formats) {\n for (const targetWidth of opts.widths) {\n // Don't upscale\n const width = Math.min(targetWidth, metadata.width)\n const outName = `${name}-${width}.${format}`\n const outPath = join(processedDir, outName)\n\n await resizeImage(absPath, outPath, width, format, opts.quality)\n sources.push({ src: outPath, width, format })\n }\n }\n\n // Build per-format source sets for <picture>\n const formatGroups = new Map<string, Array<{ src: string; width: number }>>()\n for (const s of sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push({ src: s.src, width: s.width })\n }\n\n const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({\n type: `image/${fmt === 'jpeg' ? 'jpeg' : fmt}`,\n srcset: group.map((s) => `${s.src} ${s.width}w`).join(', '),\n }))\n\n // Fallback: last format's srcset\n const fallbackFormat = formats[formats.length - 1]\n const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!\n\n // Generate blur placeholder\n const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)\n\n return {\n src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,\n srcset: fallbackFormat?.srcset ?? '',\n width: metadata.width,\n height: metadata.height,\n placeholder,\n formats,\n sources,\n }\n}\n\ninterface ImageMetadata {\n width: number\n height: number\n format: string\n}\n\n/**\n * Read basic image metadata.\n * Uses minimal binary header parsing — no external dependencies.\n */\nasync function getImageMetadata(absPath: string): Promise<ImageMetadata> {\n const buffer = await readFile(absPath)\n const ext = extname(absPath).toLowerCase()\n\n if (ext === '.png') {\n // PNG: width at bytes 16-19, height at 20-23 (big-endian)\n const width = buffer.readUInt32BE(16)\n const height = buffer.readUInt32BE(20)\n return { width, height, format: 'png' }\n }\n\n if (ext === '.jpg' || ext === '.jpeg') {\n // JPEG: scan for SOF markers\n const dimensions = parseJpegDimensions(buffer)\n return { ...dimensions, format: 'jpeg' }\n }\n\n if (ext === '.webp') {\n // WebP: VP8 header\n const dimensions = parseWebPDimensions(buffer)\n return { ...dimensions, format: 'webp' }\n }\n\n // Fallback\n return { width: 0, height: 0, format: ext.slice(1) }\n}\n\n/** @internal Exported for testing */\nexport function parseJpegDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n let offset = 2 // Skip SOI marker\n while (offset < buffer.length) {\n if (buffer[offset] !== 0xff) break\n const marker = buffer[offset + 1]!\n // SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n const height = buffer.readUInt16BE(offset + 5)\n const width = buffer.readUInt16BE(offset + 7)\n return { width, height }\n }\n const length = buffer.readUInt16BE(offset + 2)\n offset += 2 + length\n }\n return { width: 0, height: 0 }\n}\n\n/** @internal Exported for testing */\nexport function parseWebPDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n // RIFF header: bytes 0-3 = \"RIFF\", 8-11 = \"WEBP\"\n const chunk = buffer.toString('ascii', 12, 16)\n if (chunk === 'VP8 ') {\n // Lossy VP8\n const width = buffer.readUInt16LE(26) & 0x3fff\n const height = buffer.readUInt16LE(28) & 0x3fff\n return { width, height }\n }\n if (chunk === 'VP8L') {\n // Lossless VP8L\n const bits = buffer.readUInt32LE(21)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n if (chunk === 'VP8X') {\n // Extended VP8X\n const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)\n const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)\n return { width, height }\n }\n return { width: 0, height: 0 }\n}\n\n/**\n * Resize an image using native platform capabilities.\n * Uses sharp if available, falls back to canvas API.\n */\nasync function resizeImage(\n input: string,\n output: string,\n width: number,\n format: ImageFormat,\n quality: number,\n): Promise<void> {\n try {\n // Try sharp (the standard Node.js image processing library)\n const sharp = await import('sharp').then((m) => m.default ?? m)\n let pipeline = sharp(input).resize(width)\n\n switch (format) {\n case 'webp':\n pipeline = pipeline.webp({ quality })\n break\n case 'avif':\n pipeline = pipeline.avif({ quality })\n break\n case 'jpeg':\n pipeline = pipeline.jpeg({ quality, mozjpeg: true })\n break\n case 'png':\n pipeline = pipeline.png({ compressionLevel: 9 })\n break\n }\n\n await pipeline.toFile(output)\n } catch {\n // sharp not available — copy original as fallback\n warnSharpMissing()\n const content = await readFile(input)\n await writeFile(output, content)\n }\n}\n\n/**\n * Generate a tiny blur placeholder as a base64 data URI.\n */\nasync function generateBlurPlaceholder(input: string, size: number): Promise<string> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const buffer = await sharp(input)\n .resize(size, size, { fit: 'inside' })\n .blur(2)\n .webp({ quality: 20 })\n .toBuffer()\n\n return `data:image/webp;base64,${buffer.toString('base64')}`\n } catch {\n // sharp not available — return a transparent placeholder\n return \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E\"\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image-plugin.js","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { basename, extname, join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Image processing Vite plugin ──────────────────────────────────────────\n//\n// Processes images at build time:\n// - Generates multiple sizes for responsive srcset\n// - Converts to modern formats (WebP, AVIF)\n// - Creates tiny blur placeholders (base64 inline)\n// - Outputs optimized images to the build directory\n//\n// Usage in code:\n// import heroImg from \"./hero.jpg?optimize\"\n// // → { src, srcset, width, height, placeholder }\n//\n// Or use the component helper:\n// import { Image } from \"@pyreon/zero/image\"\n// <Image src=\"/hero.jpg\" width={1920} height={1080} optimize />\n\n/**\n * CDN provider — rewrites image URLs to CDN endpoints.\n * Return the rewritten URL, or null to use local processing.\n */\nexport type ImageCdnProvider = (src: string, opts: {\n width: number\n quality: number\n format: ImageFormat\n}) => string | null\n\n/** Built-in CDN providers. */\nexport const cdnProviders = {\n /** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */\n cloudinary: (cloudName: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,\n\n /** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */\n imgix: (domain: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,\n\n /** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */\n vercel: (): ImageCdnProvider => (src, { width, quality }) =>\n `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,\n\n /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */\n bunny: (pullZone: string): ImageCdnProvider => (src, { width, quality }) =>\n `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`,\n} as const\n\n/** Placeholder generation strategy. */\nexport type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none'\n\n/** SVG processing options for ?component imports. */\nexport interface SvgOptions {\n /** Replace fill/stroke with currentColor. Default: true */\n currentColor?: boolean\n /** Default size (width/height). */\n defaultSize?: number\n}\n\nexport interface ImagePluginConfig {\n /** Output directory for processed images. Default: \"assets/img\" */\n outDir?: string\n /** Default widths for responsive images. Default: [640, 1024, 1920] */\n widths?: number[]\n /** Output formats. Default: [\"webp\"] */\n formats?: ImageFormat[]\n /** Quality for lossy formats (1-100). Default: 80 */\n quality?: number\n /** Blur placeholder size in px. Default: 16 */\n placeholderSize?: number\n /** Placeholder strategy. Default: \"blur\" */\n placeholder?: PlaceholderStrategy\n /** File patterns to process. Default: /\\.(jpe?g|png|webp|avif)$/i */\n include?: RegExp\n /**\n * CDN provider for URL rewriting. When set, images are NOT processed\n * locally — URLs are rewritten to the CDN endpoint.\n *\n * @example\n * ```ts\n * imagePlugin({ cdn: cdnProviders.cloudinary('my-cloud') })\n * imagePlugin({ cdn: cdnProviders.vercel() })\n * ```\n */\n cdn?: ImageCdnProvider\n /**\n * SVG processing options. Enables `?component` import for inline SVGs.\n *\n * @example\n * ```tsx\n * import Logo from './logo.svg?component'\n * <Logo width={24} class=\"text-primary\" />\n * ```\n */\n svg?: SvgOptions | boolean\n}\n\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\n/** Per-format source set for <picture> <source> elements. */\nexport interface FormatSource {\n /** MIME type. e.g. \"image/webp\", \"image/avif\" */\n type: string\n /** srcset string for this format. e.g. \"/img-640.webp 640w, /img-1920.webp 1920w\" */\n srcset: string\n}\n\nexport interface ProcessedImage {\n /** Fallback source path (last format, largest width). */\n src: string\n /** Fallback srcset string (last format). */\n srcset: string\n /** Intrinsic width. */\n width: number\n /** Intrinsic height. */\n height: number\n /** Base64 blur placeholder data URI. */\n placeholder: string\n /** Per-format source sets for <picture> element. Ordered by priority (best format first). */\n formats: FormatSource[]\n /** Flat list of all sources. */\n sources: Array<{ src: string; width: number; format: string }>\n}\n\nconst IMAGE_EXT_RE = /\\.(jpe?g|png|webp|avif)$/i\n\n/**\n * Zero image processing Vite plugin.\n *\n * Transforms image imports with query params into optimized responsive images:\n *\n * @example\n * // vite.config.ts\n * import { imagePlugin } from \"@pyreon/zero/image-plugin\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),\n * ],\n * }\n *\n * @example\n * // In a component — import with ?optimize\n * import hero from \"./images/hero.jpg?optimize\"\n * // hero = { src, srcset, width, height, placeholder }\n *\n * <Image {...hero} alt=\"Hero\" priority />\n */\nexport function imagePlugin(config: ImagePluginConfig = {}): Plugin {\n const defaultWidths = config.widths ?? [640, 1024, 1920]\n const defaultFormats = config.formats ?? ['webp']\n const quality = config.quality ?? 80\n const placeholderSize = config.placeholderSize ?? 16\n const placeholderStrategy = config.placeholder ?? 'blur'\n const outSubDir = config.outDir ?? 'assets/img'\n const include = config.include ?? IMAGE_EXT_RE\n const cdn = config.cdn\n const svgOpts: SvgOptions | false = config.svg === true\n ? { currentColor: true }\n : config.svg === false || config.svg === undefined\n ? false\n : config.svg\n\n let root = ''\n let outDir = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-images',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n outDir = resolvedConfig.build.outDir\n isBuild = resolvedConfig.command === 'build'\n },\n\n async resolveId(id) {\n // SVG as component: import Logo from './logo.svg?component'\n if (svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')) {\n return `\\0virtual:zero-svg:${id}`\n }\n // Handle ?optimize query on image imports\n if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {\n return `\\0virtual:zero-image:${id}`\n }\n return null\n },\n\n async load(id) {\n // SVG component loading\n if (id.startsWith('\\0virtual:zero-svg:')) {\n const rawPath = id.replace('\\0virtual:zero-svg:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, rawPath) : rawPath\n if (!existsSync(absPath)) return null\n\n let svg = await readFile(absPath, 'utf-8')\n\n // Replace fill/stroke with currentColor\n if (svgOpts && (svgOpts as SvgOptions).currentColor !== false) {\n svg = svg\n .replace(/fill=\"(?!none)[^\"]*\"/g, 'fill=\"currentColor\"')\n .replace(/stroke=\"(?!none)[^\"]*\"/g, 'stroke=\"currentColor\"')\n }\n\n // Add default size from config\n const defaultSize = svgOpts && (svgOpts as SvgOptions).defaultSize\n if (defaultSize && !svg.includes('width=')) {\n svg = svg.replace('<svg', `<svg width=\"${defaultSize}\" height=\"${defaultSize}\"`)\n }\n\n // Export as Pyreon component\n return `\nimport { h } from '@pyreon/core'\nconst _svg = ${JSON.stringify(svg)}\nexport default function SvgComponent(props) {\n const el = h('span', {\n ...props,\n dangerouslySetInnerHTML: { __html: _svg },\n style: [\n 'display:inline-flex;align-items:center;justify-content:center',\n props.width ? 'width:' + props.width + 'px' : '',\n props.height ? 'height:' + props.height + 'px' : '',\n props.style || '',\n ].filter(Boolean).join(';'),\n })\n return el\n}\n`\n }\n\n // Image optimization loading\n if (!id.startsWith('\\0virtual:zero-image:')) return null\n\n const rawPath = id.replace('\\0virtual:zero-image:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath\n\n // CDN mode — rewrite URLs, no local processing\n if (cdn) {\n const metadata = await getImageMetadata(absPath)\n const sources = defaultWidths.map((w) => ({\n src: cdn(rawPath, { width: w, quality, format: defaultFormats[0]! }) ?? rawPath,\n width: w,\n format: defaultFormats[0]! as string,\n }))\n const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(', ')\n const result: ProcessedImage = {\n src: sources[sources.length - 1]?.src ?? rawPath,\n srcset,\n width: metadata.width,\n height: metadata.height,\n placeholder: placeholderStrategy === 'none' ? ''\n : await generateBlurPlaceholder(absPath, placeholderSize),\n formats: defaultFormats.map((fmt) => ({\n type: `image/${fmt}`,\n srcset: defaultWidths\n .map((w) => `${cdn(rawPath, { width: w, quality, format: fmt }) ?? rawPath} ${w}w`)\n .join(', '),\n })),\n sources,\n }\n return `export default ${JSON.stringify(result)}`\n }\n\n if (!isBuild) {\n const result = await loadDevImage(absPath, rawPath, placeholderSize)\n return `export default ${JSON.stringify(result)}`\n }\n\n const processed = await processImage(absPath, {\n widths: defaultWidths,\n formats: defaultFormats,\n quality,\n placeholderSize,\n outSubDir,\n outDir: join(root, outDir),\n })\n\n await emitProcessedSources(processed, outSubDir, this)\n rebuildFormatSrcsets(processed, absPath)\n\n return `export default ${JSON.stringify(processed)}`\n },\n }\n}\n\nasync function loadDevImage(\n absPath: string,\n rawPath: string,\n placeholderSize: number,\n): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`\n\n return {\n src: publicPath,\n srcset: '',\n width: metadata.width,\n height: metadata.height,\n placeholder: await generateBlurPlaceholder(absPath, placeholderSize),\n formats: [],\n sources: [{ src: publicPath, width: metadata.width, format: 'original' }],\n }\n}\n\nasync function emitProcessedSources(\n processed: ProcessedImage,\n outSubDir: string,\n ctx: {\n emitFile: (f: { type: 'asset'; fileName: string; source: Uint8Array }) => void\n },\n) {\n for (const source of processed.sources) {\n const fileName = join(outSubDir, basename(source.src))\n const content = await readFile(source.src)\n ctx.emitFile({ type: 'asset', fileName, source: content })\n source.src = `/${fileName}`\n }\n}\n\nfunction rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {\n const formatGroups = new Map<string, string[]>()\n for (const s of processed.sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push(`${s.src} ${s.width}w`)\n }\n processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({\n type: `image/${fmt}`,\n srcset: entries.join(', '),\n }))\n\n const lastFormat = processed.formats.at(-1)\n processed.srcset = lastFormat?.srcset ?? ''\n processed.src = processed.sources.at(-1)?.src ?? fallbackPath\n}\n\n// ─── Image processing utilities ─────────────────────────────────────────────\n\ninterface ProcessOptions {\n widths: number[]\n formats: ImageFormat[]\n quality: number\n placeholderSize: number\n outSubDir: string\n outDir: string\n}\n\nasync function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const ext = extname(absPath)\n const name = basename(absPath, ext)\n const sources: Array<{ src: string; width: number; format: string }> = []\n\n // Ensure output directory exists\n const processedDir = join(opts.outDir, opts.outSubDir)\n if (!existsSync(processedDir)) {\n await mkdir(processedDir, { recursive: true })\n }\n\n // Generate resized variants — iterate formats first so sources are grouped by format\n for (const format of opts.formats) {\n for (const targetWidth of opts.widths) {\n // Don't upscale\n const width = Math.min(targetWidth, metadata.width)\n const outName = `${name}-${width}.${format}`\n const outPath = join(processedDir, outName)\n\n await resizeImage(absPath, outPath, width, format, opts.quality)\n sources.push({ src: outPath, width, format })\n }\n }\n\n // Build per-format source sets for <picture>\n const formatGroups = new Map<string, Array<{ src: string; width: number }>>()\n for (const s of sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push({ src: s.src, width: s.width })\n }\n\n const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({\n type: `image/${fmt === 'jpeg' ? 'jpeg' : fmt}`,\n srcset: group.map((s) => `${s.src} ${s.width}w`).join(', '),\n }))\n\n // Fallback: last format's srcset\n const fallbackFormat = formats[formats.length - 1]\n const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!\n\n // Generate blur placeholder\n const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)\n\n return {\n src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,\n srcset: fallbackFormat?.srcset ?? '',\n width: metadata.width,\n height: metadata.height,\n placeholder,\n formats,\n sources,\n }\n}\n\ninterface ImageMetadata {\n width: number\n height: number\n format: string\n}\n\n/**\n * Read basic image metadata.\n * Uses minimal binary header parsing — no external dependencies.\n */\nasync function getImageMetadata(absPath: string): Promise<ImageMetadata> {\n const buffer = await readFile(absPath)\n const ext = extname(absPath).toLowerCase()\n\n if (ext === '.png') {\n // PNG: width at bytes 16-19, height at 20-23 (big-endian)\n const width = buffer.readUInt32BE(16)\n const height = buffer.readUInt32BE(20)\n return { width, height, format: 'png' }\n }\n\n if (ext === '.jpg' || ext === '.jpeg') {\n // JPEG: scan for SOF markers\n const dimensions = parseJpegDimensions(buffer)\n return { ...dimensions, format: 'jpeg' }\n }\n\n if (ext === '.webp') {\n // WebP: VP8 header\n const dimensions = parseWebPDimensions(buffer)\n return { ...dimensions, format: 'webp' }\n }\n\n // Fallback\n return { width: 0, height: 0, format: ext.slice(1) }\n}\n\n/** @internal Exported for testing */\nexport function parseJpegDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n let offset = 2 // Skip SOI marker\n while (offset < buffer.length) {\n if (buffer[offset] !== 0xff) break\n const marker = buffer[offset + 1]!\n // SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n const height = buffer.readUInt16BE(offset + 5)\n const width = buffer.readUInt16BE(offset + 7)\n return { width, height }\n }\n const length = buffer.readUInt16BE(offset + 2)\n offset += 2 + length\n }\n return { width: 0, height: 0 }\n}\n\n/** @internal Exported for testing */\nexport function parseWebPDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n // RIFF header: bytes 0-3 = \"RIFF\", 8-11 = \"WEBP\"\n const chunk = buffer.toString('ascii', 12, 16)\n if (chunk === 'VP8 ') {\n // Lossy VP8\n const width = buffer.readUInt16LE(26) & 0x3fff\n const height = buffer.readUInt16LE(28) & 0x3fff\n return { width, height }\n }\n if (chunk === 'VP8L') {\n // Lossless VP8L\n const bits = buffer.readUInt32LE(21)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n if (chunk === 'VP8X') {\n // Extended VP8X\n const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)\n const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)\n return { width, height }\n }\n return { width: 0, height: 0 }\n}\n\n/**\n * Resize an image using native platform capabilities.\n * Uses sharp if available, falls back to canvas API.\n */\nasync function resizeImage(\n input: string,\n output: string,\n width: number,\n format: ImageFormat,\n quality: number,\n): Promise<void> {\n try {\n // Try sharp (the standard Node.js image processing library)\n const sharp = await import('sharp').then((m) => m.default ?? m)\n let pipeline = sharp(input).resize(width)\n\n switch (format) {\n case 'webp':\n pipeline = pipeline.webp({ quality })\n break\n case 'avif':\n pipeline = pipeline.avif({ quality })\n break\n case 'jpeg':\n pipeline = pipeline.jpeg({ quality, mozjpeg: true })\n break\n case 'png':\n pipeline = pipeline.png({ compressionLevel: 9 })\n break\n }\n\n await pipeline.toFile(output)\n } catch {\n // sharp not available — copy original as fallback\n warnSharpMissing()\n const content = await readFile(input)\n await writeFile(output, content)\n }\n}\n\n/**\n * Generate a tiny blur placeholder as a base64 data URI.\n */\nasync function generateBlurPlaceholder(input: string, size: number): Promise<string> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const buffer = await sharp(input)\n .resize(size, size, { fit: 'inside' })\n .blur(2)\n .webp({ quality: 20 })\n .toBuffer()\n\n return `data:image/webp;base64,${buffer.toString('base64')}`\n } catch {\n // sharp not available — return a transparent placeholder\n return \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E\"\n }\n}\n"],"mappings":"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"}
|