@zenithbuild/cli 0.6.17 → 0.7.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/dist/build/compiler-runtime.d.ts +59 -0
- package/dist/build/compiler-runtime.js +277 -0
- package/dist/build/expression-rewrites.d.ts +88 -0
- package/dist/build/expression-rewrites.js +372 -0
- package/dist/build/hoisted-code-transforms.d.ts +44 -0
- package/dist/build/hoisted-code-transforms.js +316 -0
- package/dist/build/merge-component-ir.d.ts +16 -0
- package/dist/build/merge-component-ir.js +257 -0
- package/dist/build/page-component-loop.d.ts +92 -0
- package/dist/build/page-component-loop.js +257 -0
- package/dist/build/page-ir-normalization.d.ts +23 -0
- package/dist/build/page-ir-normalization.js +370 -0
- package/dist/build/page-loop-metrics.d.ts +100 -0
- package/dist/build/page-loop-metrics.js +131 -0
- package/dist/build/page-loop-state.d.ts +261 -0
- package/dist/build/page-loop-state.js +92 -0
- package/dist/build/page-loop.d.ts +33 -0
- package/dist/build/page-loop.js +217 -0
- package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
- package/dist/build/scoped-identifier-rewrite.js +245 -0
- package/dist/build/server-script.d.ts +41 -0
- package/dist/build/server-script.js +210 -0
- package/dist/build/type-declarations.d.ts +16 -0
- package/dist/build/type-declarations.js +158 -0
- package/dist/build/typescript-expression-utils.d.ts +23 -0
- package/dist/build/typescript-expression-utils.js +272 -0
- package/dist/build.d.ts +10 -18
- package/dist/build.js +74 -2261
- package/dist/component-instance-ir.d.ts +2 -2
- package/dist/component-instance-ir.js +146 -39
- package/dist/component-occurrences.js +63 -15
- package/dist/config.d.ts +66 -0
- package/dist/config.js +86 -0
- package/dist/debug-script.d.ts +1 -0
- package/dist/debug-script.js +8 -0
- package/dist/dev-build-session.d.ts +23 -0
- package/dist/dev-build-session.js +421 -0
- package/dist/dev-server.js +256 -54
- package/dist/framework-components/Image.zen +316 -0
- package/dist/images/materialize.d.ts +17 -0
- package/dist/images/materialize.js +200 -0
- package/dist/images/payload.d.ts +18 -0
- package/dist/images/payload.js +65 -0
- package/dist/images/runtime.d.ts +4 -0
- package/dist/images/runtime.js +254 -0
- package/dist/images/service.d.ts +4 -0
- package/dist/images/service.js +302 -0
- package/dist/images/shared.d.ts +58 -0
- package/dist/images/shared.js +306 -0
- package/dist/index.js +2 -17
- package/dist/manifest.js +45 -0
- package/dist/preview.d.ts +4 -1
- package/dist/preview.js +59 -6
- package/dist/resolve-components.js +20 -3
- package/dist/server-contract.js +3 -2
- package/dist/server-script-composition.d.ts +39 -0
- package/dist/server-script-composition.js +133 -0
- package/dist/startup-profile.d.ts +10 -0
- package/dist/startup-profile.js +62 -0
- package/dist/toolchain-paths.d.ts +1 -0
- package/dist/toolchain-paths.js +31 -0
- package/dist/version-check.d.ts +2 -1
- package/dist/version-check.js +12 -5
- package/package.json +5 -4
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
<script setup="ts">
|
|
2
|
+
const rawProps = props as any;
|
|
3
|
+
const IMAGE_RUNTIME_GLOBAL = "__zenith_image_runtime";
|
|
4
|
+
const DEFAULT_DEVICE_SIZES = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
5
|
+
const DEFAULT_IMAGE_SIZES = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
6
|
+
|
|
7
|
+
function safeString(value: unknown): string {
|
|
8
|
+
if (typeof value === "string") return value.trim();
|
|
9
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPlainObject(value: unknown): value is Record<string, any> {
|
|
14
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeHtml(value: unknown): string {
|
|
18
|
+
return String(value || "")
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, """)
|
|
23
|
+
.replace(/'/g, "'");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildAttr(name: string, value: unknown): string {
|
|
27
|
+
if (value === null || value === undefined || value === "") return "";
|
|
28
|
+
return ` ${name}="${escapeHtml(value)}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeFormat(value: unknown): string {
|
|
32
|
+
return String(value || "").trim().toLowerCase().replace(/^\./, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRemoteUrl(value: string): boolean {
|
|
36
|
+
return /^https?:\/\//i.test(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSource(input: unknown) {
|
|
40
|
+
if (typeof input === "string") {
|
|
41
|
+
const trimmed = input.trim();
|
|
42
|
+
if (!trimmed) return null;
|
|
43
|
+
if (isRemoteUrl(trimmed)) return { kind: "remote", url: trimmed, width: null, height: null, alt: "" };
|
|
44
|
+
if (trimmed.startsWith("/")) return { kind: "local", path: trimmed, width: null, height: null, alt: "" };
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (!isPlainObject(input)) return null;
|
|
48
|
+
const rawUrl = typeof input.url === "string"
|
|
49
|
+
? input.url
|
|
50
|
+
: typeof input.src === "string"
|
|
51
|
+
? input.src
|
|
52
|
+
: typeof input.path === "string"
|
|
53
|
+
? input.path
|
|
54
|
+
: "";
|
|
55
|
+
const normalized = normalizeSource(rawUrl);
|
|
56
|
+
if (!normalized) return null;
|
|
57
|
+
return {
|
|
58
|
+
...normalized,
|
|
59
|
+
width: Number.isInteger(input.width) && input.width > 0 ? input.width : null,
|
|
60
|
+
height: Number.isInteger(input.height) && input.height > 0 ? input.height : null,
|
|
61
|
+
alt: typeof input.alt === "string" ? input.alt : ""
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildLocalImageKey(publicPath: string): string {
|
|
66
|
+
let hash = 2166136261;
|
|
67
|
+
for (let index = 0; index < publicPath.length; index += 1) {
|
|
68
|
+
hash ^= publicPath.charCodeAt(index);
|
|
69
|
+
hash = Math.imul(hash, 16777619);
|
|
70
|
+
}
|
|
71
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildLocalVariantPath(publicPath: string, width: number, quality: number, format: string): string {
|
|
75
|
+
return `/_zenith/image/local/${buildLocalImageKey(publicPath)}/w${width}-q${quality}.${normalizeFormat(format)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildRemoteVariantPath(remoteUrl: string, width: number, quality: number, format: string): string {
|
|
79
|
+
const query = new URLSearchParams();
|
|
80
|
+
query.set("url", remoteUrl);
|
|
81
|
+
query.set("w", String(width));
|
|
82
|
+
query.set("q", String(quality));
|
|
83
|
+
if (format) query.set("f", normalizeFormat(format));
|
|
84
|
+
return `/_zenith/image?${query.toString()}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function escapeRegex(value: string): string {
|
|
88
|
+
return value.replace(/[|\\{}()[\]^$+?.*]/g, "\\$&");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function globToRegExp(glob: string, isHostname = false): RegExp {
|
|
92
|
+
let pattern = escapeRegex(glob);
|
|
93
|
+
pattern = pattern.replaceAll("\\*\\*", "__DOUBLE_STAR__");
|
|
94
|
+
pattern = pattern.replaceAll("\\*", isHostname ? "[^.]*" : "[^/]*");
|
|
95
|
+
pattern = pattern.replaceAll("__DOUBLE_STAR__", ".*");
|
|
96
|
+
return new RegExp(`^${pattern}$`, "i");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hostnameMatches(hostname: string, pattern: string): boolean {
|
|
100
|
+
if (pattern.startsWith("*.")) {
|
|
101
|
+
const suffix = pattern.slice(1);
|
|
102
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length;
|
|
103
|
+
}
|
|
104
|
+
return globToRegExp(pattern, true).test(hostname);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function matchRemotePattern(inputUrl: string, patterns: any[]): boolean {
|
|
108
|
+
if (!inputUrl || !Array.isArray(patterns) || patterns.length === 0) return false;
|
|
109
|
+
const url = new URL(inputUrl);
|
|
110
|
+
const protocol = url.protocol.replace(/:$/, "").toLowerCase();
|
|
111
|
+
const hostname = url.hostname.toLowerCase();
|
|
112
|
+
const port = url.port || "";
|
|
113
|
+
const pathname = url.pathname || "/";
|
|
114
|
+
const search = url.search || "";
|
|
115
|
+
return patterns.some((pattern) => {
|
|
116
|
+
if (pattern.protocol && pattern.protocol !== protocol) return false;
|
|
117
|
+
if (!hostnameMatches(hostname, String(pattern.hostname || ""))) return false;
|
|
118
|
+
if (pattern.port && pattern.port !== port) return false;
|
|
119
|
+
if (pattern.search && pattern.search !== search) return false;
|
|
120
|
+
return globToRegExp(String(pattern.pathname || "/**")).test(pathname);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveWidthCandidates(width: number | null, sizes: string, config: any, manifestEntry: any): number[] {
|
|
125
|
+
const base = new Set<number>([
|
|
126
|
+
...((config?.deviceSizes as number[]) || DEFAULT_DEVICE_SIZES),
|
|
127
|
+
...((config?.imageSizes as number[]) || DEFAULT_IMAGE_SIZES)
|
|
128
|
+
]);
|
|
129
|
+
if (Number.isInteger(width) && width > 0) {
|
|
130
|
+
base.add(width);
|
|
131
|
+
base.add(width * 2);
|
|
132
|
+
}
|
|
133
|
+
if (!width && sizes) {
|
|
134
|
+
for (const candidate of (config?.deviceSizes as number[]) || DEFAULT_DEVICE_SIZES) {
|
|
135
|
+
base.add(candidate);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let widths = [...base].filter((entry) => Number.isInteger(entry) && entry > 0).sort((a, b) => a - b);
|
|
139
|
+
const available = Array.isArray(manifestEntry?.availableWidths) ? manifestEntry.availableWidths : null;
|
|
140
|
+
if (Array.isArray(available) && available.length > 0) {
|
|
141
|
+
widths = widths.filter((entry) => available.includes(entry));
|
|
142
|
+
if (widths.length === 0) widths = [...available];
|
|
143
|
+
}
|
|
144
|
+
return widths;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mimeTypeForFormat(format: string): string {
|
|
148
|
+
switch (normalizeFormat(format)) {
|
|
149
|
+
case "avif": return "image/avif";
|
|
150
|
+
case "webp": return "image/webp";
|
|
151
|
+
case "png": return "image/png";
|
|
152
|
+
case "jpg":
|
|
153
|
+
case "jpeg": return "image/jpeg";
|
|
154
|
+
default: return "";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pickNumeric(value: unknown, fallback: number | null): number | null {
|
|
159
|
+
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mergeStyle(style: unknown, fit: unknown, position: unknown): string {
|
|
163
|
+
const segments: string[] = [];
|
|
164
|
+
const incoming = safeString(style);
|
|
165
|
+
if (incoming) segments.push(incoming.replace(/;+\s*$/, ""));
|
|
166
|
+
const fitValue = safeString(fit);
|
|
167
|
+
const positionValue = safeString(position);
|
|
168
|
+
if (fitValue) segments.push(`object-fit: ${fitValue}`);
|
|
169
|
+
if (positionValue) segments.push(`object-position: ${positionValue}`);
|
|
170
|
+
return segments.join("; ");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildSourceTags(sources: Array<{ type: string; srcset: string; sizes: string }>): string {
|
|
174
|
+
return sources.map((entry) => {
|
|
175
|
+
const attrs = [
|
|
176
|
+
buildAttr("type", entry.type),
|
|
177
|
+
buildAttr("srcset", entry.srcset),
|
|
178
|
+
buildAttr("sizes", entry.sizes)
|
|
179
|
+
].join("");
|
|
180
|
+
return `<source${attrs}>`;
|
|
181
|
+
}).join("");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readPayload() {
|
|
185
|
+
if (typeof globalThis !== "object" || !globalThis) return null;
|
|
186
|
+
const payload = (globalThis as any)[IMAGE_RUNTIME_GLOBAL];
|
|
187
|
+
return isPlainObject(payload) ? payload : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function encodeBase64(value: string): string {
|
|
191
|
+
if (typeof Buffer !== "undefined") return Buffer.from(value, "utf8").toString("base64");
|
|
192
|
+
if (typeof btoa === "function" && typeof TextEncoder === "function") {
|
|
193
|
+
const bytes = new TextEncoder().encode(value);
|
|
194
|
+
let binary = "";
|
|
195
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
196
|
+
return btoa(binary);
|
|
197
|
+
}
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildSerializableProps(): Record<string, unknown> {
|
|
202
|
+
return {
|
|
203
|
+
src: rawProps.src,
|
|
204
|
+
alt: rawProps.alt,
|
|
205
|
+
width: rawProps.width,
|
|
206
|
+
height: rawProps.height,
|
|
207
|
+
sizes: rawProps.sizes,
|
|
208
|
+
quality: rawProps.quality,
|
|
209
|
+
priority: rawProps.priority,
|
|
210
|
+
loading: rawProps.loading,
|
|
211
|
+
decoding: rawProps.decoding,
|
|
212
|
+
fit: rawProps.fit,
|
|
213
|
+
position: rawProps.position,
|
|
214
|
+
placeholder: rawProps.placeholder,
|
|
215
|
+
unoptimized: rawProps.unoptimized,
|
|
216
|
+
class: rawProps.class,
|
|
217
|
+
style: rawProps.style
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderImage(): string {
|
|
222
|
+
const payload = readPayload();
|
|
223
|
+
if (!payload) return "";
|
|
224
|
+
|
|
225
|
+
const source = normalizeSource(rawProps.src);
|
|
226
|
+
const alt = safeString(rawProps.alt) || safeString(source?.alt);
|
|
227
|
+
if (!source || !alt) return "";
|
|
228
|
+
|
|
229
|
+
const config = isPlainObject(payload.config) ? payload.config : {};
|
|
230
|
+
const className = safeString(rawProps.class);
|
|
231
|
+
const style = mergeStyle(rawProps.style, rawProps.fit, rawProps.position);
|
|
232
|
+
const loading = rawProps.priority === true ? "eager" : safeString(rawProps.loading) || "lazy";
|
|
233
|
+
const decoding = safeString(rawProps.decoding) || "async";
|
|
234
|
+
const fetchPriority = rawProps.priority === true ? "high" : "";
|
|
235
|
+
|
|
236
|
+
let model: any = null;
|
|
237
|
+
if (source.kind === "local") {
|
|
238
|
+
const manifestEntry = isPlainObject(payload.localImages) ? payload.localImages[source.path] : null;
|
|
239
|
+
const width = pickNumeric(rawProps.width, pickNumeric(source.width, pickNumeric(manifestEntry?.width, null)));
|
|
240
|
+
const height = pickNumeric(rawProps.height, pickNumeric(source.height, pickNumeric(manifestEntry?.height, null)));
|
|
241
|
+
const quality = pickNumeric(rawProps.quality, pickNumeric(config.quality, 75)) || 75;
|
|
242
|
+
const sizes = safeString(rawProps.sizes);
|
|
243
|
+
const widths = resolveWidthCandidates(width, sizes, config, manifestEntry);
|
|
244
|
+
const fallbackFormat = safeString(manifestEntry?.originalFormat) || "jpg";
|
|
245
|
+
const sourceFormats = ((config.formats as string[]) || []).filter((format) =>
|
|
246
|
+
Array.isArray(manifestEntry?.availableFormats) ? manifestEntry.availableFormats.includes(format) : true
|
|
247
|
+
);
|
|
248
|
+
const fallbackWidth = widths.length > 0 ? widths[widths.length - 1] : (width || manifestEntry?.width || 0);
|
|
249
|
+
model = {
|
|
250
|
+
src: rawProps.unoptimized === true
|
|
251
|
+
? source.path
|
|
252
|
+
: buildLocalVariantPath(source.path, fallbackWidth, quality, fallbackFormat),
|
|
253
|
+
width,
|
|
254
|
+
height,
|
|
255
|
+
sizes,
|
|
256
|
+
sources: rawProps.unoptimized === true
|
|
257
|
+
? []
|
|
258
|
+
: sourceFormats.map((format) => ({
|
|
259
|
+
type: mimeTypeForFormat(format),
|
|
260
|
+
sizes,
|
|
261
|
+
srcset: widths.map((candidate) => `${buildLocalVariantPath(source.path, candidate, quality, format)} ${candidate}w`).join(", ")
|
|
262
|
+
})).filter((entry) => entry.type && entry.srcset)
|
|
263
|
+
};
|
|
264
|
+
} else {
|
|
265
|
+
const width = pickNumeric(rawProps.width, pickNumeric(source.width, null));
|
|
266
|
+
const height = pickNumeric(rawProps.height, pickNumeric(source.height, null));
|
|
267
|
+
const quality = pickNumeric(rawProps.quality, pickNumeric(config.quality, 75)) || 75;
|
|
268
|
+
const sizes = safeString(rawProps.sizes);
|
|
269
|
+
const widths = resolveWidthCandidates(width, sizes, config, null);
|
|
270
|
+
if (!matchRemotePattern(source.url, Array.isArray(config.remotePatterns) ? config.remotePatterns : [])) return "";
|
|
271
|
+
if (payload.mode !== "endpoint" || rawProps.unoptimized === true || !width) {
|
|
272
|
+
model = { src: source.url, width, height, sizes, sources: [] };
|
|
273
|
+
} else {
|
|
274
|
+
model = {
|
|
275
|
+
src: buildRemoteVariantPath(source.url, widths[widths.length - 1] || width, quality, ""),
|
|
276
|
+
width,
|
|
277
|
+
height,
|
|
278
|
+
sizes,
|
|
279
|
+
sources: ((config.formats as string[]) || []).map((format) => ({
|
|
280
|
+
type: mimeTypeForFormat(format),
|
|
281
|
+
sizes,
|
|
282
|
+
srcset: widths.map((candidate) => `${buildRemoteVariantPath(source.url, candidate, quality, format)} ${candidate}w`).join(", ")
|
|
283
|
+
})).filter((entry) => entry.type && entry.srcset)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!model?.src) return "";
|
|
289
|
+
const imgAttrs = [
|
|
290
|
+
buildAttr("src", model.src),
|
|
291
|
+
buildAttr("alt", alt),
|
|
292
|
+
buildAttr("class", className),
|
|
293
|
+
buildAttr("style", style),
|
|
294
|
+
buildAttr("loading", loading),
|
|
295
|
+
buildAttr("decoding", decoding),
|
|
296
|
+
buildAttr("fetchpriority", fetchPriority),
|
|
297
|
+
buildAttr("sizes", model.sizes),
|
|
298
|
+
buildAttr("width", model.width),
|
|
299
|
+
buildAttr("height", model.height)
|
|
300
|
+
].join("");
|
|
301
|
+
const sourcesHtml = buildSourceTags(model.sources || []);
|
|
302
|
+
return sourcesHtml ? `<picture>${sourcesHtml}<img${imgAttrs} /></picture>` : `<img${imgAttrs} />`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function serializeImageProps(): string {
|
|
306
|
+
const source = normalizeSource(rawProps.src);
|
|
307
|
+
const alt = safeString(rawProps.alt) || safeString(source?.alt);
|
|
308
|
+
if (!source || !alt) return "";
|
|
309
|
+
return encodeBase64(JSON.stringify(buildSerializableProps()));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const imageHtml = renderImage();
|
|
313
|
+
const imagePayload = serializeImageProps();
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
<span class="contents" data-zenith-image={imagePayload} innerHTML={imageHtml}></span>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type ImagePayload = {
|
|
2
|
+
mode: string;
|
|
3
|
+
config: Record<string, unknown>;
|
|
4
|
+
localImages: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function materializeImageMarkup(options: {
|
|
7
|
+
html: string;
|
|
8
|
+
pageAssetPath?: string | null;
|
|
9
|
+
payload: ImagePayload;
|
|
10
|
+
ssrData?: Record<string, unknown> | null;
|
|
11
|
+
routePathname?: string;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
export declare function materializeImageMarkupInHtmlFiles(options: {
|
|
14
|
+
distDir: string;
|
|
15
|
+
payload: ImagePayload;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const RUNTIME_EXPORTS = {
|
|
4
|
+
hydrate: () => () => { },
|
|
5
|
+
signal: (value) => value,
|
|
6
|
+
state: (value) => value,
|
|
7
|
+
ref: () => ({ current: null }),
|
|
8
|
+
zeneffect: () => () => { },
|
|
9
|
+
zenEffect: () => () => { },
|
|
10
|
+
zenMount: () => { },
|
|
11
|
+
zenWindow: () => undefined,
|
|
12
|
+
zenDocument: () => undefined,
|
|
13
|
+
zenOn: () => () => { },
|
|
14
|
+
zenResize: () => () => { },
|
|
15
|
+
collectRefs: (...refs) => refs.filter(Boolean)
|
|
16
|
+
};
|
|
17
|
+
function escapeRegex(value) {
|
|
18
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
function escapeHtml(value) {
|
|
21
|
+
return String(value ?? '')
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>');
|
|
26
|
+
}
|
|
27
|
+
function parseMarkerSelector(selector) {
|
|
28
|
+
const match = selector.match(/^\[([^\]=]+)=["']([^"']+)["']\]$/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
attrName: match[1],
|
|
33
|
+
attrValue: match[2]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function upsertAttributeMarkup(attributes, attrName, value) {
|
|
37
|
+
const trimmedName = String(attrName || '').trim();
|
|
38
|
+
if (!trimmedName)
|
|
39
|
+
return attributes;
|
|
40
|
+
const attrPattern = new RegExp(`(\\s${escapeRegex(trimmedName)}=)(["']).*?\\2`, 'i');
|
|
41
|
+
if (value === null || value === undefined || value === false || value === '') {
|
|
42
|
+
return attributes.replace(attrPattern, '');
|
|
43
|
+
}
|
|
44
|
+
const serialized = ` ${trimmedName}="${escapeHtml(value)}"`;
|
|
45
|
+
if (attrPattern.test(attributes)) {
|
|
46
|
+
return attributes.replace(attrPattern, serialized);
|
|
47
|
+
}
|
|
48
|
+
return `${attributes}${serialized}`;
|
|
49
|
+
}
|
|
50
|
+
function applyAttributeMarker(html, selector, attrName, value) {
|
|
51
|
+
const parsed = parseMarkerSelector(selector);
|
|
52
|
+
if (!parsed)
|
|
53
|
+
return html;
|
|
54
|
+
const markerRe = new RegExp(`<([A-Za-z][\\w:-]*)([^>]*\\s${escapeRegex(parsed.attrName)}=(["'])${escapeRegex(parsed.attrValue)}\\3[^>]*)>`, 'g');
|
|
55
|
+
return html.replace(markerRe, (match, tagName, attrs) => {
|
|
56
|
+
const nextAttrs = upsertAttributeMarkup(String(attrs || ''), attrName, value);
|
|
57
|
+
return `<${tagName}${nextAttrs}>`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function applyInnerHtmlMarker(html, selector, value) {
|
|
61
|
+
const parsed = parseMarkerSelector(selector);
|
|
62
|
+
if (!parsed)
|
|
63
|
+
return html;
|
|
64
|
+
const markerRe = new RegExp(`<([A-Za-z][\\w:-]*)([^>]*\\s${escapeRegex(parsed.attrName)}=(["'])${escapeRegex(parsed.attrValue)}\\3[^>]*)>([\\s\\S]*?)</\\1>`, 'g');
|
|
65
|
+
const replacement = value === null || value === undefined || value === false ? '' : String(value);
|
|
66
|
+
return html.replace(markerRe, (_match, tagName, attrs) => `<${tagName}${attrs}>${replacement}</${tagName}>`);
|
|
67
|
+
}
|
|
68
|
+
function stripModuleSyntax(source) {
|
|
69
|
+
let next = source.replace(/^import\s+[^;]+;\s*$/gm, '');
|
|
70
|
+
if (/(^|\n)\s*import\s+/m.test(next)) {
|
|
71
|
+
throw new Error('[Zenith:Image] Cannot materialize page asset with unresolved imports');
|
|
72
|
+
}
|
|
73
|
+
next = next.replace(/^export\s+default\s+function\s+/gm, 'function ');
|
|
74
|
+
next = next.replace(/^export\s+function\s+/gm, 'function ');
|
|
75
|
+
next = next.replace(/^export\s+const\s+/gm, 'const ');
|
|
76
|
+
next = next.replace(/^export\s+let\s+/gm, 'let ');
|
|
77
|
+
next = next.replace(/^export\s+var\s+/gm, 'var ');
|
|
78
|
+
next = next.replace(/\bexport\s*\{[^}]*\};?/g, '');
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
async function evaluatePageModule(assetPath, payload, ssrData, routePathname) {
|
|
82
|
+
const source = stripModuleSyntax(await readFile(assetPath, 'utf8'));
|
|
83
|
+
const runtimeNames = Object.keys(RUNTIME_EXPORTS);
|
|
84
|
+
const evaluator = new Function('runtime', 'payload', 'ssrData', 'routePathname', [
|
|
85
|
+
'"use strict";',
|
|
86
|
+
`const { ${runtimeNames.join(', ')} } = runtime;`,
|
|
87
|
+
'const document = {};',
|
|
88
|
+
'const location = { pathname: routePathname || "/" };',
|
|
89
|
+
'const Document = class ZenithServerDocument {};',
|
|
90
|
+
'const globalThis = {',
|
|
91
|
+
' __zenith_image_runtime: payload,',
|
|
92
|
+
' document,',
|
|
93
|
+
' location,',
|
|
94
|
+
' Document',
|
|
95
|
+
'};',
|
|
96
|
+
'if (ssrData && typeof ssrData === "object" && !Array.isArray(ssrData)) {',
|
|
97
|
+
' globalThis.__zenith_ssr_data = ssrData;',
|
|
98
|
+
'}',
|
|
99
|
+
'globalThis.globalThis = globalThis;',
|
|
100
|
+
'globalThis.window = globalThis;',
|
|
101
|
+
'globalThis.self = globalThis;',
|
|
102
|
+
source,
|
|
103
|
+
'return {',
|
|
104
|
+
' __zenith_markers: typeof __zenith_markers !== "undefined" ? __zenith_markers : [],',
|
|
105
|
+
' __zenith_expression_bindings: typeof __zenith_expression_bindings !== "undefined" ? __zenith_expression_bindings : [],',
|
|
106
|
+
' __zenith_expr_fns: typeof __zenith_expr_fns !== "undefined" ? __zenith_expr_fns : []',
|
|
107
|
+
'};'
|
|
108
|
+
].join('\n'));
|
|
109
|
+
return evaluator(RUNTIME_EXPORTS, payload, ssrData, routePathname);
|
|
110
|
+
}
|
|
111
|
+
function buildExpressionContext(ssrData) {
|
|
112
|
+
return {
|
|
113
|
+
signalMap: new Map(),
|
|
114
|
+
params: {},
|
|
115
|
+
props: {},
|
|
116
|
+
ssrData: ssrData || {},
|
|
117
|
+
componentBindings: {},
|
|
118
|
+
zenhtml: null,
|
|
119
|
+
fragment: null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function materializeImageMarkup(options) {
|
|
123
|
+
const { html, pageAssetPath, payload, ssrData = null, routePathname = '/' } = options;
|
|
124
|
+
if (!pageAssetPath || !html.includes('data-zx-data-zenith-image')) {
|
|
125
|
+
return html;
|
|
126
|
+
}
|
|
127
|
+
const namespace = await evaluatePageModule(pageAssetPath, payload, ssrData, routePathname);
|
|
128
|
+
if (!namespace) {
|
|
129
|
+
return html;
|
|
130
|
+
}
|
|
131
|
+
const markers = Array.isArray(namespace.__zenith_markers) ? namespace.__zenith_markers : [];
|
|
132
|
+
const bindings = Array.isArray(namespace.__zenith_expression_bindings) ? namespace.__zenith_expression_bindings : [];
|
|
133
|
+
const exprFns = Array.isArray(namespace.__zenith_expr_fns) ? namespace.__zenith_expr_fns : [];
|
|
134
|
+
if (markers.length === 0 || bindings.length === 0 || exprFns.length === 0) {
|
|
135
|
+
return html;
|
|
136
|
+
}
|
|
137
|
+
const markerByIndex = new Map(markers.map((marker) => [marker.index, marker]));
|
|
138
|
+
let nextHtml = html;
|
|
139
|
+
const context = buildExpressionContext(ssrData);
|
|
140
|
+
for (const binding of bindings) {
|
|
141
|
+
const marker = markerByIndex.get(Number(binding.marker_index));
|
|
142
|
+
const exprFn = Number.isInteger(binding.fn_index) ? exprFns[binding.fn_index] : null;
|
|
143
|
+
if (!marker ||
|
|
144
|
+
typeof exprFn !== 'function' ||
|
|
145
|
+
marker.kind !== 'attr' ||
|
|
146
|
+
typeof marker.selector !== 'string' ||
|
|
147
|
+
marker.selector.includes('data-zx-data-zenith-image') === false &&
|
|
148
|
+
marker.selector.includes('data-zx-innerHTML') === false) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const value = exprFn(context);
|
|
152
|
+
if (marker.attr === 'innerHTML') {
|
|
153
|
+
nextHtml = applyInnerHtmlMarker(nextHtml, marker.selector, value);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
nextHtml = applyAttributeMarker(nextHtml, marker.selector, marker.attr || '', value);
|
|
157
|
+
}
|
|
158
|
+
return nextHtml;
|
|
159
|
+
}
|
|
160
|
+
async function loadRouteManifest(distDir) {
|
|
161
|
+
try {
|
|
162
|
+
const manifestRaw = await readFile(join(distDir, 'assets', 'router-manifest.json'), 'utf8');
|
|
163
|
+
const parsed = JSON.parse(manifestRaw);
|
|
164
|
+
return Array.isArray(parsed?.routes) ? parsed.routes : [];
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function materializeImageMarkupInHtmlFiles(options) {
|
|
171
|
+
const { distDir, payload } = options;
|
|
172
|
+
const routes = await loadRouteManifest(distDir);
|
|
173
|
+
for (const route of routes) {
|
|
174
|
+
if (route.server_script && route.prerender !== true) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const outputPath = typeof route.output === 'string' ? route.output.replace(/^\//, '') : '';
|
|
178
|
+
const assetPath = typeof route.page_asset === 'string' ? route.page_asset.replace(/^\//, '') : '';
|
|
179
|
+
if (!outputPath || !assetPath)
|
|
180
|
+
continue;
|
|
181
|
+
const fullHtmlPath = join(distDir, outputPath);
|
|
182
|
+
const fullAssetPath = join(distDir, assetPath);
|
|
183
|
+
let html = '';
|
|
184
|
+
try {
|
|
185
|
+
html = await readFile(fullHtmlPath, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const nextHtml = await materializeImageMarkup({
|
|
191
|
+
html,
|
|
192
|
+
pageAssetPath: fullAssetPath,
|
|
193
|
+
payload,
|
|
194
|
+
routePathname: typeof route.path === 'string' ? route.path : '/'
|
|
195
|
+
});
|
|
196
|
+
if (nextHtml !== html) {
|
|
197
|
+
await writeFile(fullHtmlPath, nextHtml, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createImageRuntimePayload(config: any, localImages: any, mode?: string): {
|
|
2
|
+
mode: string;
|
|
3
|
+
config: {
|
|
4
|
+
formats: string[];
|
|
5
|
+
deviceSizes: number[];
|
|
6
|
+
imageSizes: number[];
|
|
7
|
+
remotePatterns: any[];
|
|
8
|
+
quality: number;
|
|
9
|
+
allowSvg: boolean;
|
|
10
|
+
maxRemoteBytes: number;
|
|
11
|
+
maxPixels: number;
|
|
12
|
+
minimumCacheTTL: number;
|
|
13
|
+
dangerouslyAllowLocalNetwork: boolean;
|
|
14
|
+
};
|
|
15
|
+
localImages: any;
|
|
16
|
+
} | null;
|
|
17
|
+
export function injectImageRuntimePayload(html: any, payload: any): any;
|
|
18
|
+
export function injectImageRuntimePayloadIntoHtmlFiles(rootDir: any, payload: any): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { imageRuntimeGlobalName, normalizeImageConfig, normalizeImageRuntimePayload } from './shared.js';
|
|
2
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function createImageRuntimePayload(config, localImages, mode = 'passthrough') {
|
|
5
|
+
return normalizeImageRuntimePayload({
|
|
6
|
+
mode,
|
|
7
|
+
config: normalizeImageConfig(config),
|
|
8
|
+
localImages: localImages && typeof localImages === 'object' ? localImages : {}
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function serializeInlineScriptJson(payload) {
|
|
12
|
+
return JSON.stringify(payload)
|
|
13
|
+
.replace(/</g, '\\u003C')
|
|
14
|
+
.replace(/>/g, '\\u003E')
|
|
15
|
+
.replace(/\//g, '\\u002F')
|
|
16
|
+
.replace(/\u2028/g, '\\u2028')
|
|
17
|
+
.replace(/\u2029/g, '\\u2029');
|
|
18
|
+
}
|
|
19
|
+
export function injectImageRuntimePayload(html, payload) {
|
|
20
|
+
const safePayload = createImageRuntimePayload(payload?.config || {}, payload?.localImages || {}, payload?.mode || 'passthrough');
|
|
21
|
+
const globalName = imageRuntimeGlobalName();
|
|
22
|
+
const serialized = serializeInlineScriptJson(safePayload);
|
|
23
|
+
const scriptTag = `<script id="zenith-image-runtime">window.${globalName} = ${serialized};</script>`;
|
|
24
|
+
const existingTagRe = /<script\b[^>]*\bid=(["'])zenith-image-runtime\1[^>]*>[\s\S]*?<\/script>/i;
|
|
25
|
+
if (existingTagRe.test(html)) {
|
|
26
|
+
return html.replace(existingTagRe, scriptTag);
|
|
27
|
+
}
|
|
28
|
+
if (/<\/head>/i.test(html)) {
|
|
29
|
+
return html.replace(/<\/head>/i, `${scriptTag}</head>`);
|
|
30
|
+
}
|
|
31
|
+
const bodyOpen = html.match(/<body\b[^>]*>/i);
|
|
32
|
+
if (bodyOpen) {
|
|
33
|
+
return html.replace(bodyOpen[0], `${bodyOpen[0]}${scriptTag}`);
|
|
34
|
+
}
|
|
35
|
+
return `${scriptTag}${html}`;
|
|
36
|
+
}
|
|
37
|
+
export async function injectImageRuntimePayloadIntoHtmlFiles(rootDir, payload) {
|
|
38
|
+
async function walk(dir) {
|
|
39
|
+
let entries = [];
|
|
40
|
+
try {
|
|
41
|
+
entries = await readdir(dir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
entries.sort((left, right) => left.localeCompare(right));
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const fullPath = join(dir, entry);
|
|
49
|
+
const info = await stat(fullPath);
|
|
50
|
+
if (info.isDirectory()) {
|
|
51
|
+
await walk(fullPath);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!entry.endsWith('.html')) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const html = await readFile(fullPath, 'utf8');
|
|
58
|
+
const next = injectImageRuntimePayload(html, payload);
|
|
59
|
+
if (next !== html) {
|
|
60
|
+
await writeFile(fullPath, next, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await walk(rootDir);
|
|
65
|
+
}
|