@aerobuilt/core 0.2.10 → 0.3.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.
Files changed (39) hide show
  1. package/dist/build-script-analysis-Bd9EyItC.mjs +193 -0
  2. package/dist/{entry-dev.d.ts → entry-dev.d.mts} +6 -16
  3. package/dist/entry-dev.mjs +127 -0
  4. package/dist/{entry-editor.d.ts → entry-editor.d.mts} +24 -22
  5. package/dist/entry-editor.mjs +3 -0
  6. package/dist/entry-prod.d.mts +10 -0
  7. package/dist/entry-prod.mjs +15 -0
  8. package/dist/routing-Bai79LCq.mjs +198 -0
  9. package/dist/runtime/index.d.mts +65 -0
  10. package/dist/runtime/index.mjs +207 -0
  11. package/dist/runtime/instance.d.mts +19 -0
  12. package/dist/runtime/instance.mjs +45 -0
  13. package/dist/types-CLHGhnGA.d.mts +209 -0
  14. package/dist/types.d.mts +2 -0
  15. package/dist/types.mjs +1 -0
  16. package/dist/utils/aliases.d.mts +38 -0
  17. package/dist/utils/aliases.mjs +117 -0
  18. package/dist/utils/{redirects.d.ts → redirects.d.mts} +7 -12
  19. package/dist/utils/redirects.mjs +21 -0
  20. package/dist/vite/{index.d.ts → index.d.mts} +5 -12
  21. package/dist/vite/index.mjs +1890 -0
  22. package/package.json +25 -20
  23. package/dist/chunk-4DAK56WB.js +0 -36
  24. package/dist/chunk-5ZNUGZOW.js +0 -238
  25. package/dist/chunk-F7MXQXLM.js +0 -15
  26. package/dist/chunk-JAMYN2VX.js +0 -133
  27. package/dist/chunk-VTEG2UU3.js +0 -184
  28. package/dist/entry-dev.js +0 -101
  29. package/dist/entry-editor.js +0 -14
  30. package/dist/entry-prod.d.ts +0 -19
  31. package/dist/entry-prod.js +0 -19
  32. package/dist/runtime/index.d.ts +0 -74
  33. package/dist/runtime/index.js +0 -7
  34. package/dist/runtime/instance.d.ts +0 -31
  35. package/dist/runtime/instance.js +0 -10
  36. package/dist/types.d.ts +0 -202
  37. package/dist/types.js +0 -0
  38. package/dist/utils/redirects.js +0 -6
  39. package/dist/vite/index.js +0 -1991
@@ -0,0 +1,1890 @@
1
+ import { a as isDynamicRoutePattern, i as expandRoutePattern, n as resolvePageName, o as toPosix, s as toPosixRelative, t as pagePathToKey } from "../routing-Bai79LCq.mjs";
2
+ import { a as compileInterpolationFromSegments, i as isDirectiveAttr, o as tokenizeCurlyInterpolation, t as analyzeBuildScript } from "../build-script-analysis-Bd9EyItC.mjs";
3
+ import { loadTsconfigAliases, mergeWithDefaultAliases } from "../utils/aliases.mjs";
4
+ import { redirectsToRouteRules } from "../utils/redirects.mjs";
5
+ import { createRequire } from "node:module";
6
+ import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
7
+ import { nitro } from "nitro/vite";
8
+ import { parseHTML } from "linkedom";
9
+ import path from "path";
10
+ import path$1 from "node:path";
11
+ import { minify } from "html-minifier-next";
12
+ import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { createServer } from "vite";
14
+ import { spawn } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ //#region src/utils/parse.ts
18
+ /**
19
+ * General-purpose expression parsing utilities.
20
+ */
21
+ /**
22
+ * Extract top-level property keys from an object-literal expression string.
23
+ *
24
+ * @param expr - e.g. `{ title: expr, id: 42 }` or `{ config }` (shorthand).
25
+ * @returns e.g. `['title', 'id']` or `['config']`. Does not support spread; callers must handle `...obj` separately.
26
+ */
27
+ function extractObjectKeys(expr) {
28
+ let inner = expr.trim();
29
+ while (inner.startsWith("{") && inner.endsWith("}")) inner = inner.slice(1, -1).trim();
30
+ if (!inner) return [];
31
+ const keys = [];
32
+ let depth = 0;
33
+ let current = "";
34
+ for (let i = 0; i < inner.length; i++) {
35
+ const char = inner[i];
36
+ if (char === "{" || char === "[" || char === "(") {
37
+ depth++;
38
+ current += char;
39
+ } else if (char === "}" || char === "]" || char === ")") {
40
+ depth--;
41
+ current += char;
42
+ } else if (char === "," && depth === 0) {
43
+ const key = extractKeyFromEntry(current.trim());
44
+ if (key) keys.push(key);
45
+ current = "";
46
+ } else current += char;
47
+ }
48
+ const lastKey = extractKeyFromEntry(current.trim());
49
+ if (lastKey) keys.push(lastKey);
50
+ return keys;
51
+ }
52
+ /** Extract key from one property entry: `key: value` → key, shorthand `ident` → ident; returns null for spread or invalid. */
53
+ function extractKeyFromEntry(entry) {
54
+ if (!entry) return null;
55
+ if (entry.startsWith("...")) return null;
56
+ const colonIdx = entry.indexOf(":");
57
+ if (colonIdx > 0) return entry.slice(0, colonIdx).trim();
58
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(entry)) return entry;
59
+ return null;
60
+ }
61
+
62
+ //#endregion
63
+ //#region src/vite/defaults.ts
64
+ /** Virtual URL prefix for extracted client scripts (e.g. `/@aero/client/client/pages/home.js`). Root-relative; no filesystem path. */
65
+ const CLIENT_SCRIPT_PREFIX = "/@aero/client/";
66
+ /**
67
+ * Virtual URL for one client script. Single script uses `.js`, multiple use `.0.js`, `.1.js`, etc.
68
+ * Use this in the Vite transform and static build so URL generation is consistent.
69
+ */
70
+ function getClientScriptVirtualUrl(baseName, index, total) {
71
+ const suffix = total === 1 ? ".js" : `.${index}.js`;
72
+ return CLIENT_SCRIPT_PREFIX + baseName + suffix;
73
+ }
74
+ /** Virtual module ID requested by the app; resolved to `RESOLVED_*` so `load()` can re-export from the real runtime instance. */
75
+ const RUNTIME_INSTANCE_MODULE_ID = "virtual:aero/runtime-instance";
76
+ /** Resolved ID (with `\0` prefix) so Vite treats it as an internal module. */
77
+ const RESOLVED_RUNTIME_INSTANCE_MODULE_ID = "\0virtual:aero/runtime-instance";
78
+ /** Prefix for virtual empty-CSS modules used when Vite requests .html?html-proxy&inline-css (Aero .html are JS, not HTML with styles). */
79
+ const AERO_EMPTY_INLINE_CSS_PREFIX = "\0aero:empty-inline-css:";
80
+ /** Prefix for virtual HTML template modules. Resolving .html to this id returns compiled JS so vite:build-html never sees raw/compiled HTML. */
81
+ const AERO_HTML_VIRTUAL_PREFIX = "\0aero-html:";
82
+ /** Default directory names: client source, server (Nitro), dist output. */
83
+ const DEFAULT_DIRS = {
84
+ client: "client",
85
+ server: "server",
86
+ dist: "dist"
87
+ };
88
+ /** Default API route prefix (e.g. `/api`). */
89
+ const DEFAULT_API_PREFIX = "/api";
90
+ /** HTML attributes scanned for absolute URLs during static build rewrite (href, src, hx-*, action). */
91
+ const LINK_ATTRS = [
92
+ "href",
93
+ "src",
94
+ "action",
95
+ "hx-get",
96
+ "hx-post",
97
+ "hx-put",
98
+ "hx-patch",
99
+ "hx-delete"
100
+ ];
101
+ /** URLs matching this regex are not rewritten (external, protocol, hash, etc.). */
102
+ const SKIP_PROTOCOL_REGEX = /^(?:https?:\/\/|\/\/|mailto:|tel:|data:|javascript:|#|blob:|file:\/\/)/i;
103
+ function getServerDir(dirs) {
104
+ return dirs?.server ?? DEFAULT_DIRS.server;
105
+ }
106
+ /**
107
+ * Resolve optional user dir overrides with defaults (DEFAULT_DIRS).
108
+ *
109
+ * @param dirs - Optional partial AeroDirs (e.g. `{ dist: 'build' }` or `{ server: 'server' }`).
110
+ * @returns ResolvedAeroDirs with all keys set.
111
+ */
112
+ function resolveDirs(dirs) {
113
+ return {
114
+ client: dirs?.client ?? DEFAULT_DIRS.client,
115
+ server: getServerDir(dirs),
116
+ dist: dirs?.dist ?? DEFAULT_DIRS.dist
117
+ };
118
+ }
119
+
120
+ //#endregion
121
+ //#region src/compiler/constants.ts
122
+ /**
123
+ * Shared constants for the Aero compiler (parser, codegen, helpers).
124
+ *
125
+ * @remarks
126
+ * Attribute names are used with optional `data-` prefix (e.g. `data-each`). Script taxonomy uses
127
+ * `is:build`, `is:inline`, `is:blocking`; default scripts are treated as client (virtual module).
128
+ */
129
+ /** Prefix for data attributes (e.g. `data-each` → ATTR_PREFIX + ATTR_EACH). */
130
+ const ATTR_PREFIX = "data-";
131
+ /** Attribute for spreading props onto a component: `data-props` or `data-props="{ ... }"`. */
132
+ const ATTR_PROPS = "props";
133
+ /** Attribute for iteration: `data-each="{ item in items }"`. */
134
+ const ATTR_EACH = "each";
135
+ const ATTR_IF = "if";
136
+ const ATTR_ELSE_IF = "else-if";
137
+ const ATTR_ELSE = "else";
138
+ /** Slot name (on `<slot>` or content). */
139
+ const ATTR_NAME = "name";
140
+ const ATTR_SLOT = "slot";
141
+ /** Script runs at build time; extracted and becomes render function body. */
142
+ const ATTR_IS_BUILD = "is:build";
143
+ /** Script left in template in place; not extracted. */
144
+ const ATTR_IS_INLINE = "is:inline";
145
+ /** Script hoisted to head; extracted. */
146
+ const ATTR_IS_BLOCKING = "is:blocking";
147
+ /** Script/style receives data from template: `pass:data="{ config }"` or `pass:data="{ ...theme }"`. */
148
+ const ATTR_PASS_DATA = "pass:data";
149
+ /** Script external source (HTML attribute). */
150
+ const ATTR_SRC = "src";
151
+ const TAG_SLOT = "slot";
152
+ /** Default slot name when no name is given. */
153
+ const SLOT_NAME_DEFAULT = "default";
154
+ /** Matches `item in items` for data-each (captures: loop variable, iterable expression). */
155
+ const EACH_REGEX = /^(\w+)\s+in\s+(.+)$/;
156
+ /** Matches tag names ending with `-component` or `-layout`. */
157
+ const COMPONENT_SUFFIX_REGEX = /-(component|layout)$/;
158
+ /** Self-closing tag: `<tag ... />`. */
159
+ const SELF_CLOSING_TAG_REGEX = /<([a-z0-9-]+)([^>]*?)\/>/gi;
160
+ const SELF_CLOSING_TAIL_REGEX = /\/>$/;
161
+ /** HTML void elements that have no closing tag. */
162
+ const VOID_TAGS = new Set([
163
+ "area",
164
+ "base",
165
+ "br",
166
+ "col",
167
+ "embed",
168
+ "hr",
169
+ "img",
170
+ "input",
171
+ "link",
172
+ "meta",
173
+ "param",
174
+ "source",
175
+ "track",
176
+ "wbr"
177
+ ]);
178
+
179
+ //#endregion
180
+ //#region src/compiler/parser.ts
181
+ /** Serialize element attributes to a string, excluding given names (case-insensitive). Values are XML-escaped. */
182
+ function getAttrsString(element, exclude) {
183
+ const parts = [];
184
+ const attrs = element.attributes;
185
+ if (!attrs) return "";
186
+ const excludeLower = new Set([...exclude].map((s) => s.toLowerCase()));
187
+ for (let i = 0; i < attrs.length; i++) {
188
+ const a = attrs[i];
189
+ if (!a || excludeLower.has(a.name.toLowerCase())) continue;
190
+ const escaped = a.value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
191
+ parts.push(`${a.name}="${escaped}"`);
192
+ }
193
+ return parts.join(" ").trim();
194
+ }
195
+ /** True if element is inside SVG or MathML (foreign content). */
196
+ function isInForeignNamespace(el) {
197
+ let parent = el.parentElement;
198
+ while (parent) {
199
+ const tag = parent.tagName?.toLowerCase();
200
+ if (tag === "svg" || tag === "math") return true;
201
+ parent = parent.parentElement;
202
+ }
203
+ return false;
204
+ }
205
+ /** True if element is inside <head>. */
206
+ function isInHead(el) {
207
+ let parent = el.parentElement;
208
+ while (parent) {
209
+ if (parent.tagName?.toLowerCase() === "head") return true;
210
+ parent = parent.parentElement;
211
+ }
212
+ return false;
213
+ }
214
+ /** Collect all <script> elements in document order that are not inside SVG/MathML. */
215
+ function collectScriptElements(doc) {
216
+ const scripts = [];
217
+ const walk = (node) => {
218
+ if (node.nodeType === 1) {
219
+ const el = node;
220
+ if (el.tagName?.toLowerCase() === "script") {
221
+ if (!isInForeignNamespace(el)) scripts.push(el);
222
+ return;
223
+ }
224
+ }
225
+ for (let i = 0; i < node.childNodes.length; i++) walk(node.childNodes[i]);
226
+ };
227
+ walk(doc);
228
+ return scripts;
229
+ }
230
+ /**
231
+ * Parse HTML and extract Aero script blocks; return build script, client/inline/blocking script arrays, and template.
232
+ *
233
+ * @remarks
234
+ * DOM-first approach: parse full document with linkedom, walk script elements (skipping SVG/MathML),
235
+ * classify by attributes (is:build, is:inline, is:blocking, src, pass:data), mutate DOM (remove/replace),
236
+ * then serialize to produce template. Scripts inside HTML comments are not in the DOM and are left in place.
237
+ * BOM is stripped first.
238
+ *
239
+ * @param html - Full template HTML (may include BOM).
240
+ * @returns ParseResult with buildScript, clientScripts, inlineScripts, blockingScripts, and template (script blocks removed/replaced).
241
+ */
242
+ function parse(html) {
243
+ html = html.replace(/^\uFEFF/, "");
244
+ html = html.replace(SELF_CLOSING_TAG_REGEX, (match, tagName, attrs) => {
245
+ const tag = String(tagName).toLowerCase();
246
+ if (VOID_TAGS.has(tag)) return match;
247
+ return `<${tagName}${attrs}></${tagName}>`;
248
+ });
249
+ const isFullDocument = /<\s*html[\s>]/i.test(html);
250
+ let doc;
251
+ if (isFullDocument) doc = parseHTML(html).document;
252
+ else doc = parseHTML(`<html><head></head><body>${html}</body></html>`).document;
253
+ const buildContent = [];
254
+ const clientScripts = [];
255
+ const inlineScripts = [];
256
+ const blockingScripts = [];
257
+ const scriptElements = collectScriptElements(doc);
258
+ const toRemove = [];
259
+ for (const scriptEl of scriptElements) {
260
+ const inHead = isInHead(scriptEl);
261
+ const hasBuild = scriptEl.hasAttribute(ATTR_IS_BUILD);
262
+ const hasInline = scriptEl.hasAttribute(ATTR_IS_INLINE);
263
+ const hasBlocking = scriptEl.hasAttribute(ATTR_IS_BLOCKING);
264
+ const src = scriptEl.getAttribute(ATTR_SRC) ?? "";
265
+ const passData = scriptEl.getAttribute(ATTR_PASS_DATA) ?? void 0;
266
+ const attrsExcludeTaxonomy = new Set([
267
+ ATTR_IS_BUILD,
268
+ ATTR_IS_INLINE,
269
+ ATTR_IS_BLOCKING
270
+ ]);
271
+ let cleanedAttrs = getAttrsString(scriptEl, attrsExcludeTaxonomy);
272
+ if (!hasInline && !inHead) cleanedAttrs = getAttrsString(scriptEl, new Set([...attrsExcludeTaxonomy, ATTR_PASS_DATA]));
273
+ cleanedAttrs = cleanedAttrs.replace(/\s+/g, " ").trim();
274
+ const content = (scriptEl.textContent ?? "").trim();
275
+ if (hasBuild) {
276
+ buildContent.push(content);
277
+ toRemove.push(scriptEl);
278
+ continue;
279
+ }
280
+ if (hasInline) {
281
+ inlineScripts.push({
282
+ attrs: cleanedAttrs,
283
+ content,
284
+ passDataExpr: passData
285
+ });
286
+ scriptEl.removeAttribute(ATTR_IS_INLINE);
287
+ continue;
288
+ }
289
+ if (hasBlocking) {
290
+ blockingScripts.push({
291
+ attrs: cleanedAttrs,
292
+ content,
293
+ passDataExpr: passData
294
+ });
295
+ toRemove.push(scriptEl);
296
+ continue;
297
+ }
298
+ if (src) {
299
+ const isLocal = !src.startsWith("http://") && !src.startsWith("https://");
300
+ const hasType = scriptEl.hasAttribute("type");
301
+ if (isLocal && !hasType) {
302
+ scriptEl.setAttribute("type", "module");
303
+ scriptEl.removeAttribute("defer");
304
+ }
305
+ continue;
306
+ }
307
+ if (inHead && scriptEl.attributes.length > 0) continue;
308
+ clientScripts.push({
309
+ attrs: cleanedAttrs,
310
+ content,
311
+ passDataExpr: passData,
312
+ injectInHead: inHead
313
+ });
314
+ toRemove.push(scriptEl);
315
+ }
316
+ for (const el of toRemove) el.remove();
317
+ const buildScript = buildContent.length > 0 ? { content: buildContent.join("\n") } : null;
318
+ let template;
319
+ if (isFullDocument) template = doc.documentElement ? doc.documentElement.outerHTML : String(doc);
320
+ else template = doc.body ? doc.body.innerHTML : "";
321
+ return {
322
+ buildScript,
323
+ clientScripts,
324
+ inlineScripts,
325
+ blockingScripts,
326
+ template: template.trim()
327
+ };
328
+ }
329
+
330
+ //#endregion
331
+ //#region src/compiler/helpers.ts
332
+ /**
333
+ * Helpers for the Aero codegen: interpolation, attributes, slots, and render-function emission.
334
+ *
335
+ * @remarks
336
+ * Used by `codegen.ts` to compile `{ expr }` to template literals, build props/slots code, and emit
337
+ * the top-level render wrapper.
338
+ */
339
+ /**
340
+ * Validate that a value is a single well-formed braced expression using the same tokenizer as
341
+ * attribute interpolation. Used for pass:data (and optionally other braced directives); emission
342
+ * stays expression-passthrough.
343
+ *
344
+ * @param value - Raw attribute value (e.g. pass:data value).
345
+ * @param options - Optional directive and tagName for error message.
346
+ * @returns Trimmed value (including braces).
347
+ * @throws If value is not exactly one interpolation segment spanning the whole trimmed string.
348
+ */
349
+ function validateSingleBracedExpression(value, options = {}) {
350
+ const trimmed = value.trim();
351
+ const segments = tokenizeCurlyInterpolation(trimmed, { attributeMode: true });
352
+ if (!(segments.length === 1 && segments[0].kind === "interpolation" && segments[0].start === 0 && segments[0].end === trimmed.length)) {
353
+ const directive = options.directive ?? "directive";
354
+ const tagName = options.tagName ?? "element";
355
+ throw new Error(`Directive \`${directive}\` on <${tagName}> must use a braced expression, e.g. ${directive}="{ expression }".`);
356
+ }
357
+ return trimmed;
358
+ }
359
+ /**
360
+ * Compile text for use inside a template literal; replaces `{ expr }` with `${ expr }`.
361
+ *
362
+ * @param text - Raw text (may contain `{...}` interpolation).
363
+ * @returns String safe for embedding in a template literal (backticks escaped).
364
+ */
365
+ function compileInterpolation(text) {
366
+ if (!text) return "";
367
+ return compileInterpolationFromSegments(tokenizeCurlyInterpolation(text, { attributeMode: false }));
368
+ }
369
+ /**
370
+ * Compile an attribute value: `{ expr }` → interpolation; `{{` / `}}` → literal `{` / `}`.
371
+ *
372
+ * @param text - Attribute value string.
373
+ * @returns String safe for template literal (backticks escaped, double-braces as literals).
374
+ */
375
+ function compileAttributeInterpolation(text) {
376
+ if (!text) return "";
377
+ return compileInterpolationFromSegments(tokenizeCurlyInterpolation(text, { attributeMode: true }));
378
+ }
379
+ /** True if `name` equals `attr` or `prefix + attr` (e.g. `each` or `data-each`). */
380
+ function isAttr(name, attr, prefix) {
381
+ return name === attr || name === prefix + attr;
382
+ }
383
+ /** Remove outer braces: `"{ expr }"` → `expr`. */
384
+ function stripBraces(s) {
385
+ const trimmed = s.trim();
386
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed.slice(1, -1).trim();
387
+ return trimmed;
388
+ }
389
+ /** Convert kebab-case to camelCase (e.g. `my-component` → `myComponent`). */
390
+ function kebabToCamelCase(s) {
391
+ return s.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
392
+ }
393
+ /** Build a props object code string from key-value entries and optional spread (e.g. `{ ...base, title }`). */
394
+ function buildPropsString(entries, spreadExpr) {
395
+ if (spreadExpr) return entries.length > 0 ? `{ ${spreadExpr}, ${entries.join(", ")} }` : `{ ${spreadExpr} }`;
396
+ return `{ ${entries.join(", ")} }`;
397
+ }
398
+ /** Escape backticks for safe embedding in generated template literals. */
399
+ function escapeBackticks(s) {
400
+ return s.replace(/`/g, "\\`");
401
+ }
402
+ /** Emit code for a slots object whose values are variable names (e.g. `{ "default": __slot0 }`). */
403
+ function emitSlotsObjectVars(slotsMap) {
404
+ return "{ " + Object.entries(slotsMap).map(([k, varName]) => `"${k}": ${varName}`).join(", ") + " }";
405
+ }
406
+ /**
407
+ * Emit the default async render function: destructured context, script block, optional style/script/head blocks, then body that appends to `__out`.
408
+ * Single source of truth for the shape of the compiled render function; codegen calls this with all sections.
409
+ *
410
+ * @param script - Build script content (imports + user code).
411
+ * @param body - Generated statements that build the HTML string.
412
+ * @param options - Optional getStaticPathsFn, rootStyles, rootScripts, styleCode, rootScriptsLines, headScriptsLines.
413
+ * @returns Full module source (getStaticPaths + default render function).
414
+ */
415
+ function emitRenderFunction(script, body, options = {}) {
416
+ const { getStaticPathsFn, rootStyles, rootScripts, styleCode = "", rootScriptsLines = [], headScriptsLines = [] } = options;
417
+ const stylesCode = rootStyles && rootStyles.length > 0 ? rootStyles.map((s) => `styles?.add(${JSON.stringify(s)});`).join("\n ") : "";
418
+ const scriptsCode = rootScripts && rootScripts.length > 0 ? rootScripts.map((s) => `scripts?.add(${JSON.stringify(s)});`).join("\n ") : "";
419
+ const rootScriptsBlock = rootScriptsLines.length > 0 ? rootScriptsLines.join("\n ") : "";
420
+ const headScriptsBlock = headScriptsLines.length > 0 ? headScriptsLines.map((s) => `injectedHeadScripts?.add(${s});`).join("\n ") : "";
421
+ const renderFn = `export default async function(Aero) {
422
+ const { ${getRenderContextDestructurePattern()} } = Aero;
423
+ ${script}
424
+ ${styleCode}
425
+ ${stylesCode}
426
+ ${scriptsCode}
427
+ ${rootScriptsBlock}
428
+ ${headScriptsBlock}
429
+ let __out = '';
430
+ ${body}return __out;
431
+ }`;
432
+ if (getStaticPathsFn) return `${getStaticPathsFn}\n\n${renderFn}`.trim();
433
+ return renderFn.trim();
434
+ }
435
+ /**
436
+ * Pairs of [inputKey, destructuredVarName] for the 4th argument to Aero.renderComponent(..., input).
437
+ * Must stay in sync with runtime createContext / AeroRenderInput fields used by renderComponent.
438
+ */
439
+ const RENDER_COMPONENT_CONTEXT_PAIRS = [
440
+ ["request", "request"],
441
+ ["url", "url"],
442
+ ["params", "params"],
443
+ ["site", "__aero_site"],
444
+ ["styles", "styles"],
445
+ ["scripts", "scripts"],
446
+ ["headScripts", "injectedHeadScripts"]
447
+ ];
448
+ /** Emit the 4th (context) argument to Aero.renderComponent(component, props, slots, CONTEXT). Used by emit.ts and codegen.ts. */
449
+ function getRenderComponentContextArg() {
450
+ return `{ ${RENDER_COMPONENT_CONTEXT_PAIRS.map(([key, varName]) => key === varName ? key : `${key}: ${varName}`).join(", ")} }`;
451
+ }
452
+ /** Build destructuring pattern for the render function: request, url, params, site: __aero_site, ... */
453
+ function getRenderContextDestructurePattern() {
454
+ return `slots = {}, renderComponent, ${RENDER_COMPONENT_CONTEXT_PAIRS.map(([key, varName]) => key === varName ? key : `${key}: ${varName}`).join(", ")}`;
455
+ }
456
+ /** Emit `let varName = '';` for a slot accumulator. */
457
+ function emitSlotVar(varName) {
458
+ return `let ${varName} = '';\n`;
459
+ }
460
+ /** Emit `outVar += \`content\`;` (default `outVar` is `__out`). */
461
+ function emitAppend(content, outVar = "__out") {
462
+ return `${outVar} += \`${content}\`;\n`;
463
+ }
464
+ /** Emit `if (condition) {`. */
465
+ function emitIf(condition) {
466
+ return `if (${condition}) {\n`;
467
+ }
468
+ /** Emit `} else if (condition) {`. */
469
+ function emitElseIf(condition) {
470
+ return `} else if (${condition}) {\n`;
471
+ }
472
+ /** Emit `} else {`. */
473
+ function emitElse() {
474
+ return `} else {\n`;
475
+ }
476
+ /** Emit `}`. */
477
+ function emitEnd() {
478
+ return `}\n`;
479
+ }
480
+ /** Emit `for (const item of items) {`. */
481
+ function emitForOf(item, items) {
482
+ return `for (const ${item} of ${items}) {\n`;
483
+ }
484
+ /** Emit `outVar += slots['name'] ?? \`defaultContent\`;` (default `outVar` is `__out`). */
485
+ function emitSlotOutput(name, defaultContent, outVar = "__out") {
486
+ return `${outVar} += slots['${name}'] ?? \`${defaultContent}\`;\n`;
487
+ }
488
+
489
+ //#endregion
490
+ //#region src/compiler/emit.ts
491
+ const DEFAULT_OUT = "__out";
492
+ function outVarFor(node, defaultVar) {
493
+ switch (node.kind) {
494
+ case "Append":
495
+ case "Slot":
496
+ case "Component": return node.outVar ?? defaultVar;
497
+ case "ScriptPassData":
498
+ case "StylePassData": return node.outVar;
499
+ default: return defaultVar;
500
+ }
501
+ }
502
+ /**
503
+ * Emit a list of IR nodes to JS statements appending to `outVar`.
504
+ *
505
+ * @param ir - List of IR nodes (body or style fragment).
506
+ * @param outVar - Accumulator variable (default `__out`).
507
+ * @returns Generated JS string.
508
+ */
509
+ function emitToJS(ir, outVar = DEFAULT_OUT) {
510
+ let out = "";
511
+ for (const node of ir) out += emitNode(node, outVar);
512
+ return out;
513
+ }
514
+ function emitNode(node, outVar) {
515
+ switch (node.kind) {
516
+ case "Append": return emitAppend(node.content, outVarFor(node, outVar));
517
+ case "For": return emitForOf(node.item, node.items) + emitToJS(node.body, outVar) + emitEnd();
518
+ case "If": {
519
+ let code = emitIf(node.condition) + emitToJS(node.body, outVar);
520
+ if (node.elseIf?.length) for (const branch of node.elseIf) {
521
+ code = code.slice(0, -2) + emitElseIf(branch.condition);
522
+ code += emitToJS(branch.body, outVar);
523
+ }
524
+ if (node.else?.length) {
525
+ code = code.slice(0, -2) + emitElse();
526
+ code += emitToJS(node.else, outVar);
527
+ }
528
+ return code + emitEnd();
529
+ }
530
+ case "Slot": return emitSlotOutput(node.name, node.defaultContent, outVarFor(node, outVar));
531
+ case "SlotVar": return emitSlotVar(node.varName);
532
+ case "Component": {
533
+ let code = "";
534
+ for (const [slotName, slotIR] of Object.entries(node.slots)) {
535
+ const slotVar = node.slotVarMap[slotName];
536
+ if (slotVar === void 0) continue;
537
+ code += emitSlotVar(slotVar);
538
+ code += emitToJS(slotIR, slotVar);
539
+ }
540
+ const slotsString = emitSlotsObjectVars(node.slotVarMap);
541
+ const targetVar = outVarFor(node, outVar);
542
+ code += `${targetVar} += await Aero.renderComponent(${node.baseName}, ${node.propsString}, ${slotsString}, ${getRenderComponentContextArg()});\n`;
543
+ return code;
544
+ }
545
+ case "ScriptPassData": {
546
+ let code = "";
547
+ if (!node.isModule) code += emitAppend("\\n{\\n", node.outVar);
548
+ const jsMapExpr = `Object.entries(${node.passDataExpr}).map(([k, v]) => "\\nconst " + k + " = " + JSON.stringify(v) + ";").join("")`;
549
+ code += emitAppend(`\${${jsMapExpr}}\\n`, node.outVar);
550
+ return code;
551
+ }
552
+ case "StylePassData": {
553
+ const cssMapExpr = `Object.entries(${node.passDataExpr}).map(([k, v]) => "\\n --" + k + ": " + String(v) + ";").join("")`;
554
+ return emitAppend(`\n:root {\${${cssMapExpr}}\n}\n`, node.outVar);
555
+ }
556
+ default: return "";
557
+ }
558
+ }
559
+ /**
560
+ * Emit body and style IR to JS, and wrap style in a style var + styles?.add().
561
+ * Matches current codegen behavior: bodyCode from body IR, styleCode from style IR.
562
+ */
563
+ function emitBodyAndStyle(ir) {
564
+ const bodyCode = emitToJS(ir.body, DEFAULT_OUT);
565
+ let styleCode = "";
566
+ if (ir.style.length > 0) {
567
+ const styleVar = `__out_style_${Math.random().toString(36).slice(2)}`;
568
+ styleCode += `let ${styleVar} = '';\n`;
569
+ styleCode += emitToJS(ir.style, styleVar);
570
+ styleCode += `styles?.add(${styleVar});\n`;
571
+ }
572
+ return {
573
+ bodyCode,
574
+ styleCode
575
+ };
576
+ }
577
+
578
+ //#endregion
579
+ //#region src/compiler/resolver.ts
580
+ var Resolver = class {
581
+ root;
582
+ rootAbs;
583
+ resolvePathFn;
584
+ importer;
585
+ constructor(options) {
586
+ this.root = options.root;
587
+ this.rootAbs = path.resolve(this.root);
588
+ this.importer = options.importer ?? options.root;
589
+ this.resolvePathFn = options.resolvePath || ((v, _importer) => v);
590
+ }
591
+ /** Normalize resolved path: root-relative with `/` if under root, else posix-normalized; always forward slashes. */
592
+ normalizeResolved(next) {
593
+ if (path.isAbsolute(next)) {
594
+ const absolute = path.resolve(next);
595
+ if (absolute === this.rootAbs || absolute.startsWith(this.rootAbs + path.sep)) next = "/" + path.relative(this.rootAbs, absolute);
596
+ else next = path.posix.normalize(toPosix(next));
597
+ }
598
+ return toPosix(next);
599
+ }
600
+ /**
601
+ * Resolve an import specifier (e.g. `@components/header`) to a path.
602
+ * Returns the original specifier if the resolved value does not look like a path.
603
+ *
604
+ * @param specifier - Import specifier from the source file.
605
+ * @returns Resolved path or unchanged specifier.
606
+ */
607
+ resolveImport(specifier) {
608
+ let next = this.resolvePathFn(specifier, this.importer);
609
+ if (!/^(\.{1,2}\/|\/|@|~)/.test(next)) return specifier;
610
+ next = this.normalizeResolved(next);
611
+ return next;
612
+ }
613
+ /**
614
+ * Resolve an attribute value that may be a path (e.g. `src` on script).
615
+ * Same rules as `resolveImport`; returns unchanged value if not path-like.
616
+ *
617
+ * @param value - Raw attribute value.
618
+ * @returns Resolved path or unchanged value.
619
+ */
620
+ resolveAttrValue(value) {
621
+ let next = this.resolvePathFn(value, this.importer);
622
+ if (!/^(\.{1,2}\/|\/|@|~)/.test(next)) return value;
623
+ next = this.normalizeResolved(next);
624
+ return next;
625
+ }
626
+ };
627
+
628
+ //#endregion
629
+ //#region src/compiler/codegen.ts
630
+ /** Internal lowerer: walks DOM nodes and builds IR; used by compile(). */
631
+ var Lowerer = class {
632
+ resolver;
633
+ slotCounter = 0;
634
+ constructor(resolver) {
635
+ this.resolver = resolver;
636
+ }
637
+ /** Checks if node has if/data-if attribute */
638
+ hasIfAttr(node) {
639
+ return node.nodeType === 1 && (node.hasAttribute(ATTR_IF) || node.hasAttribute(ATTR_PREFIX + ATTR_IF));
640
+ }
641
+ /** Checks if node has else-if/data-else-if attribute */
642
+ hasElseIfAttr(node) {
643
+ return node.nodeType === 1 && (node.hasAttribute(ATTR_ELSE_IF) || node.hasAttribute(ATTR_PREFIX + ATTR_ELSE_IF));
644
+ }
645
+ /** Checks if node has else/data-else attribute */
646
+ hasElseAttr(node) {
647
+ return node.nodeType === 1 && (node.hasAttribute(ATTR_ELSE) || node.hasAttribute(ATTR_PREFIX + ATTR_ELSE));
648
+ }
649
+ /** Gets the condition value from if/else-if attribute */
650
+ getCondition(node, attr) {
651
+ const plainValue = node.getAttribute(attr);
652
+ if (plainValue !== null) return this.requireBracedExpression(plainValue, attr, node);
653
+ const dataAttr = ATTR_PREFIX + attr;
654
+ const dataValue = node.getAttribute(dataAttr);
655
+ if (dataValue !== null) return this.requireBracedExpression(dataValue, dataAttr, node);
656
+ return null;
657
+ }
658
+ /**
659
+ * Require directive value to be a braced expression; optionally strip outer braces.
660
+ *
661
+ * @param value - Raw attribute value.
662
+ * @param directive - Attribute name for error message (e.g. `each`, `pass:data`).
663
+ * @param node - DOM node for error message (tag name).
664
+ * @param options - `strip: true` (default) returns inner expression; `strip: false` returns trimmed value including braces (e.g. for pass:data).
665
+ * @returns Trimmed value, with or without outer braces per options.
666
+ */
667
+ requireBracedExpression(value, directive, node, options) {
668
+ const strip = options?.strip !== false;
669
+ const trimmed = value.trim();
670
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
671
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
672
+ throw new Error(`Directive \`${directive}\` on <${tagName}> must use a braced expression, e.g. ${directive}="{ expression }".`);
673
+ }
674
+ return strip ? stripBraces(trimmed) : trimmed;
675
+ }
676
+ isSingleWrappedExpression(value) {
677
+ const trimmed = value.trim();
678
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
679
+ if (trimmed.startsWith("{{") || trimmed.endsWith("}}")) return false;
680
+ let depth = 0;
681
+ for (let i = 0; i < trimmed.length; i++) {
682
+ const char = trimmed[i];
683
+ if (char === "{") depth++;
684
+ if (char === "}") {
685
+ depth--;
686
+ if (depth < 0) return false;
687
+ if (depth === 0 && i !== trimmed.length - 1) return false;
688
+ }
689
+ }
690
+ return depth === 0;
691
+ }
692
+ /** Parses component attributes, extracting props and data-props */
693
+ parseComponentAttributes(node) {
694
+ const propsEntries = [];
695
+ let dataPropsExpression = null;
696
+ if (node.attributes) for (let i = 0; i < node.attributes.length; i++) {
697
+ const attr = node.attributes[i];
698
+ if (isAttr(attr.name, ATTR_EACH, ATTR_PREFIX)) continue;
699
+ if (isAttr(attr.name, ATTR_IF, ATTR_PREFIX)) continue;
700
+ if (isAttr(attr.name, ATTR_ELSE_IF, ATTR_PREFIX)) continue;
701
+ if (isAttr(attr.name, ATTR_ELSE, ATTR_PREFIX)) continue;
702
+ if (isAttr(attr.name, ATTR_PROPS, ATTR_PREFIX)) {
703
+ const value = attr.value?.trim() || "";
704
+ if (!value) dataPropsExpression = "...props";
705
+ else dataPropsExpression = this.requireBracedExpression(value, attr.name, node);
706
+ continue;
707
+ }
708
+ const rawValue = attr.value ?? "";
709
+ const escapedLiteral = escapeBackticks(rawValue);
710
+ let propVal;
711
+ if (this.isSingleWrappedExpression(rawValue)) propVal = stripBraces(escapedLiteral);
712
+ else {
713
+ const compiled = compileAttributeInterpolation(rawValue);
714
+ propVal = compiled.includes("${") || rawValue.includes("{{") || rawValue.includes("}}") ? `\`${compiled}\`` : `"${escapedLiteral}"`;
715
+ }
716
+ propsEntries.push(`${attr.name}: ${propVal}`);
717
+ }
718
+ return { propsString: buildPropsString(propsEntries, dataPropsExpression) };
719
+ }
720
+ /** Parses element attributes, extracting data-each and building the attribute string */
721
+ parseElementAttributes(node) {
722
+ const attributes = [];
723
+ let loopData = null;
724
+ let passDataExpr = null;
725
+ if (node.attributes) for (let i = 0; i < node.attributes.length; i++) {
726
+ const attr = node.attributes[i];
727
+ if (isAttr(attr.name, ATTR_EACH, ATTR_PREFIX)) {
728
+ const match = this.requireBracedExpression(attr.value || "", attr.name, node).match(EACH_REGEX);
729
+ if (!match) {
730
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
731
+ throw new Error(`Directive \`${attr.name}\` on <${tagName}> must match "{ item in items }".`);
732
+ }
733
+ loopData = {
734
+ item: match[1],
735
+ items: match[2]
736
+ };
737
+ continue;
738
+ }
739
+ if (isAttr(attr.name, ATTR_IF, ATTR_PREFIX)) continue;
740
+ if (isAttr(attr.name, ATTR_ELSE_IF, ATTR_PREFIX)) continue;
741
+ if (isAttr(attr.name, ATTR_ELSE, ATTR_PREFIX)) continue;
742
+ if (isAttr(attr.name, ATTR_PASS_DATA, ATTR_PREFIX)) {
743
+ passDataExpr = validateSingleBracedExpression(attr.value || "", {
744
+ directive: attr.name,
745
+ tagName: node?.tagName?.toLowerCase?.() || "element"
746
+ });
747
+ continue;
748
+ }
749
+ if (attr.name === ATTR_IS_INLINE) continue;
750
+ let val = this.resolver.resolveAttrValue(attr.value ?? "");
751
+ if (!isDirectiveAttr(attr.name)) val = compileAttributeInterpolation(val);
752
+ attributes.push(`${attr.name}="${val}"`);
753
+ }
754
+ return {
755
+ attrString: attributes.length ? " " + attributes.join(" ") : "",
756
+ loopData,
757
+ passDataExpr
758
+ };
759
+ }
760
+ /** Dispatch by node type: text (3) → compileText, element (1) → compileElement; other types return []. */
761
+ compileNode(node, skipInterpolation = false, outVar = "__out") {
762
+ switch (node.nodeType) {
763
+ case 3: return this.compileText(node, skipInterpolation, outVar);
764
+ case 1: return this.compileElement(node, skipInterpolation, outVar);
765
+ default: return [];
766
+ }
767
+ }
768
+ /** Lower a list of nodes (e.g. body children) to IR. */
769
+ compileFragment(nodes) {
770
+ return this.compileChildNodes(nodes, false, "__out");
771
+ }
772
+ compileChildNodes(nodes, skipInterpolation, outVar) {
773
+ if (!nodes) return [];
774
+ const out = [];
775
+ let i = 0;
776
+ while (i < nodes.length) {
777
+ const node = nodes[i];
778
+ if (this.hasIfAttr(node)) {
779
+ const { nodes: chainNodes, consumed } = this.compileConditionalChain(nodes, i, skipInterpolation, outVar);
780
+ out.push(...chainNodes);
781
+ i += consumed;
782
+ continue;
783
+ }
784
+ out.push(...this.compileNode(node, skipInterpolation, outVar));
785
+ i++;
786
+ }
787
+ return out;
788
+ }
789
+ /**
790
+ * Lowers a conditional chain (if/else-if/else siblings) into one IR If node.
791
+ * Returns the IR and how many DOM nodes were consumed.
792
+ */
793
+ compileConditionalChain(nodes, startIndex, skipInterpolation, outVar) {
794
+ let i = startIndex;
795
+ let condition = null;
796
+ let body = [];
797
+ const elseIf = [];
798
+ let elseBody;
799
+ while (i < nodes.length) {
800
+ const node = nodes[i];
801
+ if (!node || node.nodeType !== 1) {
802
+ if (node?.nodeType === 3 && node.textContent?.trim() === "") {
803
+ i++;
804
+ continue;
805
+ }
806
+ break;
807
+ }
808
+ if (condition === null) {
809
+ if (!this.hasIfAttr(node)) break;
810
+ condition = this.getCondition(node, ATTR_IF);
811
+ body = this.compileElement(node, skipInterpolation, outVar);
812
+ i++;
813
+ } else if (this.hasElseIfAttr(node)) {
814
+ const elseIfCondition = this.getCondition(node, ATTR_ELSE_IF);
815
+ elseIf.push({
816
+ condition: elseIfCondition,
817
+ body: this.compileElement(node, skipInterpolation, outVar)
818
+ });
819
+ i++;
820
+ } else if (this.hasElseAttr(node)) {
821
+ elseBody = this.compileElement(node, skipInterpolation, outVar);
822
+ i++;
823
+ break;
824
+ } else break;
825
+ }
826
+ return {
827
+ nodes: [{
828
+ kind: "If",
829
+ condition,
830
+ body,
831
+ ...elseIf.length > 0 && { elseIf },
832
+ ...elseBody && elseBody.length > 0 && { else: elseBody }
833
+ }],
834
+ consumed: i - startIndex
835
+ };
836
+ }
837
+ compileText(node, skipInterpolation, outVar) {
838
+ const text = node.textContent || "";
839
+ if (!text) return [];
840
+ return [{
841
+ kind: "Append",
842
+ content: skipInterpolation ? escapeBackticks(text) : compileInterpolation(text),
843
+ outVar
844
+ }];
845
+ }
846
+ /** Lower one element: slot, component (-component/-layout), or regular HTML (with optional data-each, pass:data). */
847
+ compileElement(node, skipInterpolation, outVar) {
848
+ const tagName = node.tagName.toLowerCase();
849
+ if (tagName === TAG_SLOT) return this.compileSlot(node, skipInterpolation, outVar);
850
+ if (COMPONENT_SUFFIX_REGEX.test(tagName)) return this.compileComponent(node, tagName, skipInterpolation, outVar);
851
+ const { attrString, loopData, passDataExpr } = this.parseElementAttributes(node);
852
+ const childSkip = skipInterpolation || tagName === "style" || tagName === "script" && !passDataExpr;
853
+ const inner = [];
854
+ if (VOID_TAGS.has(tagName)) inner.push({
855
+ kind: "Append",
856
+ content: `<${tagName}${attrString}>`,
857
+ outVar
858
+ });
859
+ else {
860
+ inner.push({
861
+ kind: "Append",
862
+ content: `<${tagName}${attrString}>`,
863
+ outVar
864
+ });
865
+ const isScript = tagName === "script";
866
+ const isStyle = tagName === "style";
867
+ let closeBlock = false;
868
+ if (isScript && passDataExpr) {
869
+ const result = this.emitScriptPassDataIR(passDataExpr, node, outVar);
870
+ inner.push(...result.nodes);
871
+ closeBlock = result.closeBlock;
872
+ } else if (isStyle && passDataExpr) inner.push({
873
+ kind: "StylePassData",
874
+ passDataExpr,
875
+ outVar
876
+ });
877
+ inner.push(...this.compileChildNodes(node.childNodes, childSkip, outVar));
878
+ if (closeBlock) inner.push({
879
+ kind: "Append",
880
+ content: "\\n}\\n",
881
+ outVar
882
+ });
883
+ inner.push({
884
+ kind: "Append",
885
+ content: `</${tagName}>`,
886
+ outVar
887
+ });
888
+ }
889
+ if (loopData) return [{
890
+ kind: "For",
891
+ item: loopData.item,
892
+ items: loopData.items,
893
+ body: inner
894
+ }];
895
+ return inner;
896
+ }
897
+ /**
898
+ * Builds IR for injecting `pass:data` into a `<script>` tag.
899
+ * For non-module scripts, the emitter emits the opening `{\n`; caller must append closing `}\n` (Append IR).
900
+ */
901
+ emitScriptPassDataIR(passDataExpr, node, outVar) {
902
+ const isModule = node.getAttribute("type") === "module";
903
+ return {
904
+ nodes: [{
905
+ kind: "ScriptPassData",
906
+ passDataExpr,
907
+ isModule,
908
+ outVar
909
+ }],
910
+ closeBlock: !isModule
911
+ };
912
+ }
913
+ compileSlot(node, skipInterpolation, outVar) {
914
+ return [{
915
+ kind: "Slot",
916
+ name: node.getAttribute(ATTR_NAME) || SLOT_NAME_DEFAULT,
917
+ defaultContent: this.compileSlotDefaultContent(node.childNodes, skipInterpolation),
918
+ outVar
919
+ }];
920
+ }
921
+ /** Compiles slot default content into template literal content (for simple fallbacks). */
922
+ compileSlotDefaultContent(nodes, skipInterpolation) {
923
+ if (!nodes) return "";
924
+ let out = "";
925
+ for (let i = 0; i < nodes.length; i++) {
926
+ const node = nodes[i];
927
+ if (!node) continue;
928
+ if (node.nodeType === 3) {
929
+ const text = node.textContent || "";
930
+ if (text) out += skipInterpolation ? escapeBackticks(text) : compileInterpolation(text);
931
+ } else if (node.nodeType === 1) out += this.compileElementDefaultContent(node, skipInterpolation);
932
+ }
933
+ return out;
934
+ }
935
+ /** Compiles an element for slot default content (template literal format). */
936
+ compileElementDefaultContent(node, skipInterpolation) {
937
+ const tagName = node.tagName.toLowerCase();
938
+ if (tagName === TAG_SLOT) return `\${ slots['${node.getAttribute(ATTR_NAME) || SLOT_NAME_DEFAULT}'] ?? ${this.compileSlotDefaultContent(node.childNodes, skipInterpolation)} }`;
939
+ if (COMPONENT_SUFFIX_REGEX.test(tagName)) {
940
+ const kebabBase = tagName.replace(COMPONENT_SUFFIX_REGEX, "");
941
+ const baseName = kebabToCamelCase(kebabBase);
942
+ const { propsString } = this.parseComponentAttributes(node);
943
+ return `\${ await Aero.renderComponent(${baseName}, ${propsString}, {}, ${getRenderComponentContextArg()}) }`;
944
+ }
945
+ const { attrString } = this.parseElementAttributes(node);
946
+ if (VOID_TAGS.has(tagName)) return `<${tagName}${attrString}>`;
947
+ const childSkip = skipInterpolation || tagName === "style" || tagName === "script";
948
+ return `<${tagName}${attrString}>${this.compileSlotDefaultContent(node.childNodes, childSkip)}</${tagName}>`;
949
+ }
950
+ compileComponent(node, tagName, skipInterpolation, outVar) {
951
+ const kebabBase = tagName.replace(COMPONENT_SUFFIX_REGEX, "");
952
+ const baseName = kebabToCamelCase(kebabBase);
953
+ const { propsString } = this.parseComponentAttributes(node);
954
+ const slotVarMap = {};
955
+ const slotContentMap = { [SLOT_NAME_DEFAULT]: [] };
956
+ if (node.childNodes) for (let i = 0; i < node.childNodes.length; i++) {
957
+ const child = node.childNodes[i];
958
+ let slotName = SLOT_NAME_DEFAULT;
959
+ if (child.nodeType === 1) {
960
+ const slotAttr = child.getAttribute(ATTR_SLOT);
961
+ if (slotAttr) slotName = slotAttr;
962
+ }
963
+ slotContentMap[slotName] = slotContentMap[slotName] || [];
964
+ slotContentMap[slotName].push(child);
965
+ }
966
+ const slots = {};
967
+ for (const [slotName, children] of Object.entries(slotContentMap)) {
968
+ const slotVar = `__slot_${this.slotCounter++}`;
969
+ slotVarMap[slotName] = slotVar;
970
+ const slotIR = [];
971
+ for (const child of children) {
972
+ if (child.nodeType === 1) {
973
+ if (child.tagName?.toLowerCase() === TAG_SLOT && child.hasAttribute(ATTR_NAME) && child.hasAttribute(ATTR_SLOT)) {
974
+ const passthroughName = child.getAttribute(ATTR_NAME);
975
+ const defaultContent = this.compileSlotDefaultContent(child.childNodes, skipInterpolation);
976
+ slotIR.push({
977
+ kind: "Slot",
978
+ name: passthroughName,
979
+ defaultContent,
980
+ outVar: slotVar
981
+ });
982
+ continue;
983
+ }
984
+ }
985
+ slotIR.push(...this.compileNode(child, skipInterpolation, slotVar));
986
+ }
987
+ slots[slotName] = slotIR;
988
+ }
989
+ return [{
990
+ kind: "Component",
991
+ baseName,
992
+ propsString,
993
+ slots,
994
+ slotVarMap,
995
+ outVar
996
+ }];
997
+ }
998
+ };
999
+ /**
1000
+ * Compile a parsed template and options into a JavaScript module string (default async render function + optional getStaticPaths).
1001
+ *
1002
+ * @param parsed - Result from parser (buildScript, clientScripts, inlineScripts, blockingScripts, template).
1003
+ * @param options - Root, resolvePath, and optional overrides for client/inline/blocking script arrays.
1004
+ * @returns Module source: optional getStaticPaths export and default async function(Aero) that returns HTML string.
1005
+ */
1006
+ function compile(parsed, options) {
1007
+ options.inlineScripts ?? parsed.inlineScripts;
1008
+ const resolver = new Resolver({
1009
+ root: options.root,
1010
+ resolvePath: options.resolvePath,
1011
+ importer: options.importer
1012
+ });
1013
+ const lowerer = new Lowerer(resolver);
1014
+ let script = parsed.buildScript ? parsed.buildScript.content : "";
1015
+ const analysis = analyzeBuildScript(script);
1016
+ script = analysis.scriptWithoutImportsAndGetStaticPaths;
1017
+ const getStaticPathsFn = analysis.getStaticPathsFn;
1018
+ const importsLines = [];
1019
+ const quote = "\"";
1020
+ for (const imp of analysis.imports) {
1021
+ const modExpr = `await import(${quote}${resolver.resolveImport(imp.specifier)}${quote})`;
1022
+ if (imp.defaultBinding) importsLines.push(`const ${imp.defaultBinding} = (${modExpr}).default`);
1023
+ else if (imp.namedBindings.length > 0) {
1024
+ const names = imp.namedBindings.map((b) => b.imported === b.local ? b.local : `${b.imported} as ${b.local}`).join(", ");
1025
+ importsLines.push(`const {${names}} = ${modExpr}`);
1026
+ } else if (imp.namespaceBinding) importsLines.push(`const ${imp.namespaceBinding} = ${modExpr}`);
1027
+ }
1028
+ const importsCode = importsLines.join("\n");
1029
+ const { document } = parseHTML(`
1030
+ <html lang="en">
1031
+ <body>${parsed.template.replace(SELF_CLOSING_TAG_REGEX, (match, tagName, attrs) => {
1032
+ const tag = String(tagName).toLowerCase();
1033
+ if (VOID_TAGS.has(tag)) return match.replace(SELF_CLOSING_TAIL_REGEX, ">");
1034
+ return `<${tagName}${attrs}></${tagName}>`;
1035
+ })}</body>
1036
+ </html>
1037
+ `);
1038
+ let styleCode = "";
1039
+ if (document.body) {
1040
+ const children = Array.from(document.body.childNodes);
1041
+ for (const node of children) if (node.nodeType === 1 && node.tagName === "STYLE") {
1042
+ const styleVar = `__out_style_${Math.random().toString(36).slice(2)}`;
1043
+ const styleIR = lowerer.compileNode(node, false, styleVar);
1044
+ styleCode += `let ${styleVar} = '';\n`;
1045
+ styleCode += emitToJS(styleIR, styleVar);
1046
+ styleCode += `styles?.add(${styleVar});\n`;
1047
+ node.remove();
1048
+ }
1049
+ }
1050
+ const { bodyCode } = emitBodyAndStyle({
1051
+ body: document.body ? lowerer.compileFragment(document.body.childNodes) : [],
1052
+ style: []
1053
+ });
1054
+ const rootScripts = [];
1055
+ const headScripts = [];
1056
+ const virtualPrefix = "/@aero/client/";
1057
+ if (options.clientScripts?.some((c) => c.content.startsWith(virtualPrefix)) ?? false) script = `function __aeroScriptUrl(p){return '/'+'@aero/client/'+p}\n` + script;
1058
+ if (options.clientScripts && options.clientScripts.length > 0) for (const clientScript of options.clientScripts) {
1059
+ const attrs = clientScript.attrs ?? "";
1060
+ const baseAttrs = attrs.includes("type=") ? attrs : `type="module"${attrs ? " " + attrs : ""}`;
1061
+ const urlExpr = clientScript.content.startsWith(virtualPrefix) ? `__aeroScriptUrl(${JSON.stringify(clientScript.content.slice(14))})` : JSON.stringify(clientScript.content);
1062
+ const tagExpr = `'<script ${baseAttrs.replace(/'/g, "\\'")} src="'+${urlExpr}+'"><\/script>'`;
1063
+ const isHead = clientScript.injectInHead;
1064
+ if (clientScript.passDataExpr) {
1065
+ const jsonExpr = `JSON.stringify(${clientScript.passDataExpr})`;
1066
+ if (isHead) headScripts.push(`(function(){const __pid=Aero.nextPassDataId();\`<\`+'script type="application/json" id="'+__pid+'" class="__aero_data">'+${jsonExpr}+'</'+'script>';window.__aero_data_next=JSON.parse(document.getElementById("'+__pid+'").textContent);})();${tagExpr}`);
1067
+ else rootScripts.push(`(function(){const __pid=Aero.nextPassDataId();scripts?.add(\`<script type="application/json" id="\${__pid}" class="__aero_data">\${${jsonExpr}}<\/script>\`);scripts?.add(\`<script>window.__aero_data_next=JSON.parse(document.getElementById("\${__pid}").textContent);<\/script>\`);scripts?.add(${tagExpr});})();`);
1068
+ } else if (isHead) headScripts.push(tagExpr);
1069
+ else rootScripts.push(`scripts?.add(${tagExpr});`);
1070
+ }
1071
+ if (options.blockingScripts) for (const blockingScript of options.blockingScripts) if (blockingScript.passDataExpr) {
1072
+ const jsMapExpr = `Object.entries(${validateSingleBracedExpression(blockingScript.passDataExpr, {
1073
+ directive: "pass:data",
1074
+ tagName: "script"
1075
+ })}).map(([k, v]) => "\\nconst " + k + " = " + JSON.stringify(v) + ";").join("")`;
1076
+ headScripts.push(`\`<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>\${${jsMapExpr}}${blockingScript.content.replace(/`/g, "\\`")}<\/script>\``);
1077
+ } else {
1078
+ const escapedContent = blockingScript.content.replace(/'/g, "\\'");
1079
+ headScripts.push(`'<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>${escapedContent}<\/script>'`);
1080
+ }
1081
+ const renderFn = emitRenderFunction(script, bodyCode, {
1082
+ getStaticPathsFn: getStaticPathsFn || void 0,
1083
+ styleCode,
1084
+ rootScriptsLines: rootScripts,
1085
+ headScriptsLines: headScripts
1086
+ });
1087
+ return importsCode + "\n" + renderFn;
1088
+ }
1089
+
1090
+ //#endregion
1091
+ //#region src/vite/build.ts
1092
+ /**
1093
+ * Register client scripts from parsed template into a Map.
1094
+ * Converts client script content to virtual URLs for Vite bundling.
1095
+ */
1096
+ function registerClientScriptsToMap(parsed, baseName, target) {
1097
+ const total = parsed.clientScripts.length;
1098
+ for (let i = 0; i < total; i++) {
1099
+ const clientScript = parsed.clientScripts[i];
1100
+ const clientScriptUrl = getClientScriptVirtualUrl(baseName, i, total);
1101
+ target.set(clientScriptUrl, {
1102
+ content: clientScript.content,
1103
+ passDataExpr: clientScript.passDataExpr,
1104
+ injectInHead: clientScript.injectInHead
1105
+ });
1106
+ }
1107
+ }
1108
+ /** True if page has dynamic segments (e.g. `posts/[id]`). */
1109
+ function isDynamicPage(page) {
1110
+ return isDynamicRoutePattern(page.pageName);
1111
+ }
1112
+ /** Replace `[key]` in pattern with params[key]; throws if a key is missing. */
1113
+ function expandPattern(pattern, params) {
1114
+ return expandRoutePattern(pattern, params);
1115
+ }
1116
+ /** Recursively collect all .html file paths under dir. */
1117
+ function walkHtmlFiles(dir) {
1118
+ if (!fs.existsSync(dir)) return [];
1119
+ const files = [];
1120
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
1121
+ const fullPath = path$1.join(dir, item.name);
1122
+ if (item.isDirectory()) {
1123
+ files.push(...walkHtmlFiles(fullPath));
1124
+ continue;
1125
+ }
1126
+ if (item.isFile() && item.name.endsWith(".html")) files.push(fullPath);
1127
+ }
1128
+ return files;
1129
+ }
1130
+ /** Page name to route path (e.g. index → '', about → about, blog/index → blog). */
1131
+ function toRouteFromPageName(pageName) {
1132
+ if (pageName === "index") return "";
1133
+ if (pageName.endsWith("/index")) return pageName.slice(0, -6);
1134
+ return pageName;
1135
+ }
1136
+ /** Route path to output file path (e.g. '' → index.html, about → about/index.html). */
1137
+ function toOutputFile(routePath) {
1138
+ if (routePath === "") return "index.html";
1139
+ if (routePath === "404") return "404.html";
1140
+ return toPosix(path$1.join(routePath, "index.html"));
1141
+ }
1142
+ /**
1143
+ * Generate sitemap.xml from route paths. Only called when site URL is set.
1144
+ * Excludes 404. Writes to distDir/sitemap.xml.
1145
+ */
1146
+ function writeSitemap(routePaths, site, distDir) {
1147
+ const base = site.replace(/\/$/, "");
1148
+ const xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" + routePaths.filter((r) => r !== "404").map((routePath) => {
1149
+ return ` <url>\n <loc>${escapeXml(`${base}${(routePath === "" ? "" : `/${routePath}/`) || "/"}`)}</loc>\n </url>`;
1150
+ }).join("\n") + "\n</urlset>\n";
1151
+ fs.writeFileSync(path$1.join(distDir, "sitemap.xml"), xml, "utf-8");
1152
+ }
1153
+ function escapeXml(s) {
1154
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1155
+ }
1156
+ /** Relative path from fromDir to targetPath, always starting with ./ when non-empty. */
1157
+ function normalizeRelativeLink(fromDir, targetPath) {
1158
+ const rel = path$1.posix.relative(fromDir, targetPath);
1159
+ if (!rel) return "./";
1160
+ if (rel.startsWith(".")) return rel;
1161
+ return `./${rel}`;
1162
+ }
1163
+ /** Relative path to a route (directory index); appends trailing slash for non-root routes. */
1164
+ function normalizeRelativeRouteLink(fromDir, routePath) {
1165
+ const targetDir = routePath === "" ? "" : routePath;
1166
+ const rel = path$1.posix.relative(fromDir, targetDir);
1167
+ let res = !rel ? "./" : rel.startsWith(".") ? rel : `./${rel}`;
1168
+ if (routePath !== "" && routePath !== "404" && !res.endsWith("/")) res += "/";
1169
+ return res;
1170
+ }
1171
+ function normalizeRoutePathFromHref(value) {
1172
+ if (value === "/") return "";
1173
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
1174
+ }
1175
+ /** True if URL is empty or matches SKIP_PROTOCOL_REGEX (external, hash, etc.). */
1176
+ function isSkippableUrl(value) {
1177
+ if (!value) return true;
1178
+ return SKIP_PROTOCOL_REGEX.test(value);
1179
+ }
1180
+ /** Root-relative path for manifest key (posix). */
1181
+ function toManifestKey(root, filePath) {
1182
+ return toPosixRelative(filePath, root);
1183
+ }
1184
+ /** Resolve script/link src or href to absolute path; returns null for external/skippable or unresolvable. */
1185
+ function resolveTemplateAssetPath(rawValue, templateFile, root, resolvePath) {
1186
+ if (!rawValue || isSkippableUrl(rawValue)) return null;
1187
+ if (rawValue.startsWith("/")) return path$1.resolve(root, rawValue.slice(1));
1188
+ if (rawValue.startsWith("@") || rawValue.startsWith("~")) {
1189
+ const resolved = resolvePath ? resolvePath(rawValue, templateFile) : rawValue;
1190
+ return path$1.isAbsolute(resolved) ? resolved : path$1.resolve(root, resolved);
1191
+ }
1192
+ if (rawValue.startsWith("./") || rawValue.startsWith("../")) return path$1.resolve(path$1.dirname(templateFile), rawValue);
1193
+ return null;
1194
+ }
1195
+ /** All .html files under root/templateRoot (recursive). */
1196
+ function discoverTemplates(root, templateRoot) {
1197
+ return walkHtmlFiles(path$1.resolve(root, templateRoot));
1198
+ }
1199
+ /** Static pages from pagesRoot: file paths, page names (via pagePathToKey), route paths, output files; home → index when no sibling index. */
1200
+ function discoverPages(root, pagesRoot) {
1201
+ const pageFiles = walkHtmlFiles(path$1.resolve(root, pagesRoot));
1202
+ const allPageNames = new Set(pageFiles.map((f) => pagePathToKey(toPosixRelative(f, root))));
1203
+ return pageFiles.map((file) => {
1204
+ let pageName = pagePathToKey(toPosixRelative(file, root));
1205
+ if (pageName === "home" && !allPageNames.has("index")) pageName = "index";
1206
+ else if (pageName.endsWith("/home")) {
1207
+ const siblingIndex = pageName.slice(0, -5) + "/index";
1208
+ if (!allPageNames.has(siblingIndex)) pageName = siblingIndex;
1209
+ }
1210
+ const routePath = toRouteFromPageName(pageName);
1211
+ return {
1212
+ pageName,
1213
+ routePath,
1214
+ sourceFile: file,
1215
+ outputFile: toOutputFile(routePath)
1216
+ };
1217
+ });
1218
+ }
1219
+ /**
1220
+ * Discover all extracted client scripts from templates under root/templateRoot.
1221
+ *
1222
+ * @param root - Project root.
1223
+ * @param templateRoot - Directory under root containing .html templates (e.g. client).
1224
+ * @returns Map from virtual URL (e.g. `/@aero/client/client/pages/home.js`) to ScriptEntry.
1225
+ */
1226
+ function discoverClientScriptContentMap(root, templateRoot) {
1227
+ const map = /* @__PURE__ */ new Map();
1228
+ for (const file of discoverTemplates(root, templateRoot)) {
1229
+ const parsed = parse(fs.readFileSync(file, "utf-8"));
1230
+ if (parsed.clientScripts.length === 0) continue;
1231
+ registerClientScriptsToMap(parsed, toPosixRelative(file, root).replace(/\.html$/i, ""), map);
1232
+ }
1233
+ return map;
1234
+ }
1235
+ /** Rollup input entries for virtual client scripts (manifest key → virtual path); used by createBuildConfig. */
1236
+ function discoverClientScriptVirtualInputs(root, templateRoot) {
1237
+ const entries = {};
1238
+ for (const file of discoverTemplates(root, templateRoot)) {
1239
+ const parsed = parse(fs.readFileSync(file, "utf-8"));
1240
+ if (parsed.clientScripts.length === 0) continue;
1241
+ const baseName = toPosixRelative(file, root).replace(/\.html$/i, "");
1242
+ const { clientScripts } = parsed;
1243
+ const total = clientScripts.length;
1244
+ for (let i = 0; i < total; i++) {
1245
+ const virtualPath = getClientScriptVirtualUrl(baseName, i, total);
1246
+ const manifestKey = virtualPath.replace(/^\//, "");
1247
+ entries[manifestKey] = virtualPath;
1248
+ }
1249
+ }
1250
+ return entries;
1251
+ }
1252
+ /** Rollup input entries: script/link refs from templates, default client index, and assets/images. */
1253
+ function discoverAssetInputs(root, resolvePath, templateRoot = "client") {
1254
+ const entries = /* @__PURE__ */ new Map();
1255
+ for (const templateFile of discoverTemplates(root, templateRoot)) {
1256
+ const { document } = parseHTML(fs.readFileSync(templateFile, "utf-8"));
1257
+ const scripts = Array.from(document.querySelectorAll("script[src]"));
1258
+ const styles = Array.from(document.querySelectorAll("link[rel=\"stylesheet\"][href]"));
1259
+ const refs = [...scripts, ...styles];
1260
+ for (const el of refs) {
1261
+ const attr = el.hasAttribute("src") ? "src" : "href";
1262
+ const resolved = resolveTemplateAssetPath(el.getAttribute(attr) || "", templateFile, root, resolvePath);
1263
+ if (!resolved || !fs.existsSync(resolved)) continue;
1264
+ const ext = path$1.extname(resolved).toLowerCase();
1265
+ if (![
1266
+ ".js",
1267
+ ".mjs",
1268
+ ".ts",
1269
+ ".tsx",
1270
+ ".css"
1271
+ ].includes(ext)) continue;
1272
+ entries.set(toManifestKey(root, resolved), resolved);
1273
+ }
1274
+ }
1275
+ const defaultClientEntry = path$1.resolve(root, `${templateRoot}/index.ts`);
1276
+ if (fs.existsSync(defaultClientEntry)) entries.set(toManifestKey(root, defaultClientEntry), defaultClientEntry);
1277
+ const imagesDir = path$1.resolve(root, templateRoot, "assets/images");
1278
+ if (fs.existsSync(imagesDir)) {
1279
+ const imageFiles = walkFiles(imagesDir);
1280
+ for (const file of imageFiles) {
1281
+ const key = toManifestKey(root, file);
1282
+ if (entries.has(key)) continue;
1283
+ if (path$1.basename(file).startsWith(".")) continue;
1284
+ entries.set(key, file);
1285
+ }
1286
+ }
1287
+ return Object.fromEntries(entries);
1288
+ }
1289
+ /** Recursively collect all file paths under dir (no extension filter). */
1290
+ function walkFiles(dir) {
1291
+ if (!fs.existsSync(dir)) return [];
1292
+ const files = [];
1293
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
1294
+ const fullPath = path$1.join(dir, item.name);
1295
+ if (item.isDirectory()) {
1296
+ files.push(...walkFiles(fullPath));
1297
+ continue;
1298
+ }
1299
+ if (item.isFile()) files.push(fullPath);
1300
+ }
1301
+ return files;
1302
+ }
1303
+ /** Prepend `<!doctype html>` if missing. */
1304
+ function addDoctype(html) {
1305
+ return /^\s*<!doctype\s+html/i.test(html) ? html : `<!doctype html>\n${html}`;
1306
+ }
1307
+ /** Image extensions: when a manifest entry's .file is a .js chunk but .assets lists the real image, use it. */
1308
+ const ASSET_IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i;
1309
+ /** Rewrite one absolute URL to dist-relative using manifest and route set; leaves API and external URLs unchanged. */
1310
+ function rewriteAbsoluteUrl(value, fromDir, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1311
+ if (value.startsWith(apiPrefix)) return value;
1312
+ const noQuery = value.split(/[?#]/)[0] || value;
1313
+ const suffix = value.slice(noQuery.length);
1314
+ const manifestKey = noQuery.replace(/^\//, "");
1315
+ let manifestEntry = manifest[noQuery] ?? manifest[manifestKey];
1316
+ if (!manifestEntry && noQuery.startsWith("assets/")) {
1317
+ const entry = Object.values(manifest).find((e) => e?.file === noQuery || e?.file === manifestKey);
1318
+ if (entry) manifestEntry = entry;
1319
+ }
1320
+ if (manifestEntry?.file) return normalizeRelativeLink(fromDir, manifestEntry.assets?.find((a) => ASSET_IMAGE_EXT.test(a)) ?? manifestEntry.file) + suffix;
1321
+ if (noQuery.startsWith("/assets/")) return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1322
+ const route = normalizeRoutePathFromHref(noQuery);
1323
+ if (routeSet.has(route) || route === "") return (route === "404" ? normalizeRelativeLink(fromDir, toOutputFile(route)) : normalizeRelativeRouteLink(fromDir, route)) + suffix;
1324
+ return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1325
+ }
1326
+ /** Rewrite script src (virtual client → hashed asset) and LINK_ATTRS in rendered HTML; add doctype. */
1327
+ function rewriteRenderedHtml(html, outputFile, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1328
+ const fromDir = path$1.posix.dirname(outputFile);
1329
+ const { document } = parseHTML(html);
1330
+ for (const script of Array.from(document.querySelectorAll("script[src]"))) {
1331
+ const src = script.getAttribute("src") || "";
1332
+ if (src.startsWith(CLIENT_SCRIPT_PREFIX)) {
1333
+ const newSrc = rewriteAbsoluteUrl(src, fromDir, manifest, routeSet, apiPrefix);
1334
+ script.setAttribute("src", newSrc);
1335
+ script.setAttribute("type", "module");
1336
+ script.removeAttribute("defer");
1337
+ continue;
1338
+ }
1339
+ if (script.getAttribute("type") === "module") script.removeAttribute("defer");
1340
+ }
1341
+ for (const el of Array.from(document.querySelectorAll("*"))) for (const attrName of LINK_ATTRS) {
1342
+ if (!el.hasAttribute(attrName)) continue;
1343
+ const current = (el.getAttribute(attrName) || "").trim();
1344
+ if (!current || isSkippableUrl(current)) continue;
1345
+ if (!current.startsWith("/")) continue;
1346
+ el.setAttribute(attrName, rewriteAbsoluteUrl(current, fromDir, manifest, routeSet, apiPrefix));
1347
+ }
1348
+ const htmlTag = document.documentElement;
1349
+ if (htmlTag) return addDoctype(htmlTag.outerHTML);
1350
+ return addDoctype(document.toString());
1351
+ }
1352
+ function readManifest(distDir) {
1353
+ const manifestPath = path$1.join(distDir, ".vite", "manifest.json");
1354
+ if (!fs.existsSync(manifestPath)) return {};
1355
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1356
+ }
1357
+ /**
1358
+ * Render all static pages into outDir: discover pages, expand dynamic routes via getStaticPaths, run Vite in middleware mode, rewrite URLs, optionally minify.
1359
+ *
1360
+ * @param options - StaticBuildOptions (root, dirs, resolvePath, vitePlugins, minify).
1361
+ * @param outDir - Output directory (e.g. dist).
1362
+ */
1363
+ async function renderStaticPages(options, outDir) {
1364
+ const root = options.root;
1365
+ const dirs = resolveDirs(options.dirs);
1366
+ const apiPrefix = options.apiPrefix || DEFAULT_API_PREFIX;
1367
+ const discoveredPages = discoverPages(root, path$1.join(dirs.client, "pages"));
1368
+ const distDir = path$1.resolve(root, outDir);
1369
+ const manifest = readManifest(distDir);
1370
+ const previousAeroServer = process.env.AERO_SERVER;
1371
+ process.env.AERO_SERVER = "false";
1372
+ const server = await createServer({
1373
+ configFile: false,
1374
+ root,
1375
+ cacheDir: path$1.join(root, ".aero", "vite-ssr"),
1376
+ appType: "custom",
1377
+ logLevel: "error",
1378
+ plugins: options.vitePlugins ?? [],
1379
+ server: {
1380
+ middlewareMode: true,
1381
+ hmr: false,
1382
+ watch: { ignored: ["**/*"] }
1383
+ }
1384
+ });
1385
+ try {
1386
+ const runtime = await server.ssrLoadModule(RUNTIME_INSTANCE_MODULE_ID);
1387
+ const pages = [];
1388
+ for (const page of discoveredPages) {
1389
+ if (!isDynamicPage(page)) {
1390
+ pages.push(page);
1391
+ continue;
1392
+ }
1393
+ const mod = await server.ssrLoadModule(page.sourceFile);
1394
+ if (typeof mod.getStaticPaths !== "function") {
1395
+ console.warn(`[aero] ⚠ Skipping dynamic page "${path$1.relative(root, page.sourceFile)}" — no getStaticPaths() exported. Page will not be pre-rendered.`);
1396
+ continue;
1397
+ }
1398
+ const staticPaths = await mod.getStaticPaths();
1399
+ if (!Array.isArray(staticPaths) || staticPaths.length === 0) {
1400
+ console.warn(`[aero] ⚠ getStaticPaths() for "${path$1.relative(root, page.sourceFile)}" returned no paths. Page will not be pre-rendered.`);
1401
+ continue;
1402
+ }
1403
+ for (const entry of staticPaths) {
1404
+ const expandedPageName = expandPattern(page.pageName, entry.params);
1405
+ const expandedRoute = toRouteFromPageName(expandedPageName);
1406
+ pages.push({
1407
+ pageName: expandedPageName,
1408
+ routePath: expandedRoute,
1409
+ sourceFile: page.sourceFile,
1410
+ outputFile: toOutputFile(expandedRoute),
1411
+ params: entry.params,
1412
+ props: entry.props
1413
+ });
1414
+ }
1415
+ }
1416
+ const redirectFromSet = new Set((options.redirects ?? []).map((r) => r.from.replace(/^\/+|\/+$/g, "").trim() || ""));
1417
+ const pathMatchesRedirect = (page) => {
1418
+ const pathSegment = page.routePath === "" ? "" : page.routePath;
1419
+ return redirectFromSet.has(pathSegment);
1420
+ };
1421
+ const pagesToRender = options.redirects?.length ? pages.filter((p) => !pathMatchesRedirect(p)) : pages;
1422
+ const routeSet = new Set(pagesToRender.map((p) => p.routePath));
1423
+ for (const page of pagesToRender) {
1424
+ const routePath = page.routePath ? `/${page.routePath}` : "/";
1425
+ const pageUrl = new URL(routePath, "http://localhost");
1426
+ const renderTarget = isDynamicPage(page) ? toPosixRelative(page.sourceFile, path$1.resolve(root, dirs.client, "pages")).replace(/\.html$/i, "") : page.pageName;
1427
+ let rendered = await runtime.aero.render(renderTarget, {
1428
+ url: pageUrl,
1429
+ request: new Request(pageUrl.toString(), { method: "GET" }),
1430
+ routePath,
1431
+ params: page.params || {},
1432
+ props: page.props || {},
1433
+ site: options.site
1434
+ });
1435
+ rendered = rewriteRenderedHtml(addDoctype(rendered), page.outputFile, manifest, routeSet, apiPrefix);
1436
+ const isProd = typeof import.meta !== "undefined" && import.meta.env?.PROD;
1437
+ if (options.minify && isProd) rendered = await minify(rendered, {
1438
+ collapseWhitespace: true,
1439
+ removeComments: true,
1440
+ minifyCSS: true,
1441
+ minifyJS: true
1442
+ });
1443
+ const outPath = path$1.join(distDir, page.outputFile);
1444
+ fs.mkdirSync(path$1.dirname(outPath), { recursive: true });
1445
+ fs.writeFileSync(outPath, rendered, "utf-8");
1446
+ }
1447
+ if (options.site && options.site.trim() !== "") writeSitemap([...new Set(pagesToRender.map((p) => p.routePath))], options.site.trim(), distDir);
1448
+ } finally {
1449
+ await server.close();
1450
+ if (previousAeroServer === void 0) delete process.env.AERO_SERVER;
1451
+ else process.env.AERO_SERVER = previousAeroServer;
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Vite build config: outDir, manifest, emptyOutDir, rollupOptions.input from discovered assets and virtual client scripts.
1456
+ *
1457
+ * @param options - Optional dirs and resolvePath for asset discovery.
1458
+ * @param root - Project root (default process.cwd()).
1459
+ * @returns Vite UserConfig.build fragment.
1460
+ */
1461
+ function createBuildConfig(options = {}, root = process.cwd()) {
1462
+ const dirs = resolveDirs(options.dirs);
1463
+ const assetInputs = discoverAssetInputs(root, options.resolvePath, dirs.client);
1464
+ const virtualClientInputs = discoverClientScriptVirtualInputs(root, dirs.client);
1465
+ const inputs = {
1466
+ ...assetInputs,
1467
+ ...virtualClientInputs
1468
+ };
1469
+ return {
1470
+ outDir: dirs.dist,
1471
+ manifest: true,
1472
+ emptyOutDir: true,
1473
+ rollupOptions: {
1474
+ input: inputs,
1475
+ output: {
1476
+ entryFileNames(chunkInfo) {
1477
+ return `assets/${path$1.basename(chunkInfo.name)}-[hash].js`;
1478
+ },
1479
+ chunkFileNames(chunkInfo) {
1480
+ if ((chunkInfo.facadeModuleId ?? "").includes("aero-html")) return `assets/aero-[hash].js`;
1481
+ return `assets/${path$1.basename(chunkInfo.name)}-[hash].js`;
1482
+ },
1483
+ assetFileNames(assetInfo) {
1484
+ return `assets/${path$1.basename(assetInfo.name || "")}-[hash][extname]`;
1485
+ }
1486
+ }
1487
+ }
1488
+ };
1489
+ }
1490
+
1491
+ //#endregion
1492
+ //#region src/vite/index.ts
1493
+ const require = createRequire(import.meta.url);
1494
+ const AERO_DIR = ".aero";
1495
+ const NITRO_CONFIG_FILENAME = "nitro.config.mjs";
1496
+ /**
1497
+ * Generate Nitro config from Aero options and write to <projectRoot>/.aero/nitro.config.mjs.
1498
+ * root is the app/site directory (Vite config.root), e.g. examples/kitchen-sink or a create-aerobuilt project folder.
1499
+ * Returns the absolute path to .aero (Nitro cwd so it loads this file).
1500
+ */
1501
+ function writeGeneratedNitroConfig(root, serverDir, redirects) {
1502
+ const aeroDir = path.join(root, AERO_DIR);
1503
+ mkdirSync(aeroDir, { recursive: true });
1504
+ const routeRules = redirectsToRouteRules(redirects ?? []);
1505
+ const nitroConfig = {
1506
+ rootDir: "..",
1507
+ output: { dir: path.join(root, ".output") },
1508
+ scanDirs: [path.join(root, serverDir)],
1509
+ routeRules,
1510
+ noPublicDir: true
1511
+ };
1512
+ const content = `// Generated by Aero — do not edit
1513
+ export default ${JSON.stringify(nitroConfig, null, 2)}
1514
+ `;
1515
+ writeFileSync(path.join(aeroDir, NITRO_CONFIG_FILENAME), content);
1516
+ return aeroDir;
1517
+ }
1518
+ /** Run `nitro build` with generated config; used after static pages are written when options.server is true. */
1519
+ async function runNitroBuild(_root, configCwd) {
1520
+ const nitroBin = process.platform === "win32" ? "nitro.cmd" : "nitro";
1521
+ await new Promise((resolve, reject) => {
1522
+ const child = spawn(nitroBin, ["build"], {
1523
+ cwd: configCwd,
1524
+ stdio: "inherit",
1525
+ env: process.env
1526
+ });
1527
+ child.on("error", reject);
1528
+ child.on("exit", (code) => {
1529
+ if (code === 0) {
1530
+ resolve();
1531
+ return;
1532
+ }
1533
+ reject(/* @__PURE__ */ new Error(`[aero] nitro build failed with exit code ${code ?? "null"}`));
1534
+ });
1535
+ });
1536
+ }
1537
+ function createAeroConfigPlugin(state) {
1538
+ return {
1539
+ name: "vite-plugin-aero-config",
1540
+ enforce: "pre",
1541
+ config(userConfig, env) {
1542
+ const root = userConfig.root || process.cwd();
1543
+ const rawAliases = loadTsconfigAliases(root);
1544
+ state.aliasResult = mergeWithDefaultAliases(rawAliases, root, state.dirs);
1545
+ if (state.dirs.client !== DEFAULT_DIRS.client && rawAliases.projectRoot != null) console.warn("[aero] Custom dirs.client is set; ensure tsconfig paths for @pages, @layouts, @components match (e.g. @pages → \"" + state.dirs.client + "/pages\").");
1546
+ const site = state.options.site ?? "";
1547
+ return {
1548
+ base: "./",
1549
+ resolve: { alias: env?.command === "build" ? [...state.aliasResult.aliases, {
1550
+ find: "@aerobuilt/core",
1551
+ replacement: require.resolve("@aerobuilt/core/entry-prod")
1552
+ }] : state.aliasResult.aliases },
1553
+ define: { "import.meta.env.SITE": JSON.stringify(site) },
1554
+ build: createBuildConfig({
1555
+ resolvePath: state.aliasResult.resolve,
1556
+ dirs: state.options.dirs
1557
+ }, root)
1558
+ };
1559
+ },
1560
+ configResolved(resolvedConfig) {
1561
+ state.config = resolvedConfig;
1562
+ }
1563
+ };
1564
+ }
1565
+ /** True if filePath is an Aero template under client/pages, client/components, or client/layouts. */
1566
+ function isAeroTemplateHtml(filePath, root, dirs) {
1567
+ const clientBase = path.join(root, dirs.client);
1568
+ const rel = path.relative(clientBase, filePath);
1569
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
1570
+ const sep = path.sep;
1571
+ return rel.startsWith("pages" + sep) || rel.startsWith("components" + sep) || rel.startsWith("layouts" + sep);
1572
+ }
1573
+ function createAeroVirtualsPlugin(state) {
1574
+ return {
1575
+ name: "vite-plugin-aero-virtuals",
1576
+ enforce: "pre",
1577
+ buildStart() {
1578
+ if (!state.config) return;
1579
+ discoverClientScriptContentMap(state.config.root, state.dirs.client).forEach((entry, url) => state.clientScripts.set(url, entry));
1580
+ },
1581
+ async resolveId(id, importer) {
1582
+ if (id === RUNTIME_INSTANCE_MODULE_ID) return RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1583
+ if (id.startsWith(CLIENT_SCRIPT_PREFIX)) return "\0" + id;
1584
+ if (id.startsWith("\0" + CLIENT_SCRIPT_PREFIX)) return id;
1585
+ if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) return id;
1586
+ if (id.includes("html-proxy") && id.includes("inline-css")) return AERO_EMPTY_INLINE_CSS_PREFIX + id;
1587
+ if (id.startsWith("aero:content")) return null;
1588
+ const resolved = await this.resolve(id, importer, { skipSelf: true });
1589
+ if (resolved && resolved.id.endsWith(".html")) {
1590
+ if (state.config?.command === "build" && state.aliasResult && isAeroTemplateHtml(resolved.id, state.config.root, state.dirs)) return AERO_HTML_VIRTUAL_PREFIX + resolved.id.replace(/\.html$/i, ".aero");
1591
+ return resolved;
1592
+ }
1593
+ if ((id.startsWith("./") || id.startsWith("../") || id.startsWith("/") || id.startsWith("@") && !id.slice(1).split("/")[0].includes("-") && id.split("/").length < 3) && !id.includes(".") && !id.startsWith("\0") && !resolved?.id.includes("node_modules")) {
1594
+ const resolvedHtml = await this.resolve(id + ".html", importer, { skipSelf: true });
1595
+ if (resolvedHtml) {
1596
+ if (state.config?.command === "build" && state.aliasResult && isAeroTemplateHtml(resolvedHtml.id, state.config.root, state.dirs)) return AERO_HTML_VIRTUAL_PREFIX + resolvedHtml.id.replace(/\.html$/i, ".aero");
1597
+ return resolvedHtml;
1598
+ }
1599
+ }
1600
+ return null;
1601
+ },
1602
+ load(id) {
1603
+ if (id === RESOLVED_RUNTIME_INSTANCE_MODULE_ID) return `export { aero, onUpdate } from ${JSON.stringify(state.runtimeInstancePath)}`;
1604
+ if (id.startsWith(AERO_EMPTY_INLINE_CSS_PREFIX)) return "/* aero: no inline styles */";
1605
+ if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) {
1606
+ const filePath = id.slice(AERO_HTML_VIRTUAL_PREFIX.length).replace(/\.aero$/i, ".html");
1607
+ if (!state.config || !state.aliasResult) return null;
1608
+ this.addWatchFile(filePath);
1609
+ try {
1610
+ const parsed = parse(readFileSync(filePath, "utf-8"));
1611
+ const baseName = toPosixRelative(filePath, state.config.root).replace(/\.html$/i, "");
1612
+ registerClientScriptsToMap(parsed, baseName, state.clientScripts);
1613
+ for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1614
+ return {
1615
+ code: compile(parsed, {
1616
+ root: state.config.root,
1617
+ clientScripts: parsed.clientScripts,
1618
+ blockingScripts: parsed.blockingScripts,
1619
+ inlineScripts: parsed.inlineScripts,
1620
+ resolvePath: state.aliasResult.resolve,
1621
+ importer: filePath
1622
+ }),
1623
+ map: null
1624
+ };
1625
+ } catch {
1626
+ return null;
1627
+ }
1628
+ }
1629
+ if (id.startsWith("\0" + CLIENT_SCRIPT_PREFIX)) {
1630
+ const virtualId = id.slice(1);
1631
+ const entry = state.clientScripts.get(virtualId);
1632
+ if (!entry) return "";
1633
+ if (entry.passDataExpr) {
1634
+ const keys = extractObjectKeys(entry.passDataExpr);
1635
+ if (keys.length > 0) return `var __aero_data=(typeof window!=='undefined'&&window.__aero_data_next!==undefined)?window.__aero_data_next:{};if(typeof window!=='undefined')delete window.__aero_data_next;const { ${keys.join(", ")} } = __aero_data;\n` + entry.content;
1636
+ }
1637
+ return entry.content;
1638
+ }
1639
+ return null;
1640
+ }
1641
+ };
1642
+ }
1643
+ function createAeroTransformPlugin(state) {
1644
+ return {
1645
+ name: "vite-plugin-aero-transform",
1646
+ enforce: "pre",
1647
+ transform(code, id) {
1648
+ if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) return null;
1649
+ if (!id.endsWith(".html")) return null;
1650
+ if (!state.config || !state.aliasResult) return null;
1651
+ try {
1652
+ const parsed = parse(code);
1653
+ if (parsed.clientScripts.length > 0) {
1654
+ const baseName = toPosixRelative(id, state.config.root).replace(/\.html$/i, "");
1655
+ registerClientScriptsToMap(parsed, baseName, state.clientScripts);
1656
+ for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1657
+ }
1658
+ return {
1659
+ code: compile(parsed, {
1660
+ root: state.config.root,
1661
+ clientScripts: parsed.clientScripts,
1662
+ blockingScripts: parsed.blockingScripts,
1663
+ inlineScripts: parsed.inlineScripts,
1664
+ resolvePath: state.aliasResult.resolve,
1665
+ importer: id
1666
+ }),
1667
+ map: null
1668
+ };
1669
+ } catch (err) {
1670
+ const relativePath = path.relative(state.config.root, id);
1671
+ this.error(`[aero] Error compiling ${relativePath}: ${err.message}`);
1672
+ }
1673
+ }
1674
+ };
1675
+ }
1676
+ function createAeroSsrPlugin(state) {
1677
+ return {
1678
+ name: "vite-plugin-aero-ssr",
1679
+ configureServer(server) {
1680
+ server.middlewares.use(async (req, res, next) => {
1681
+ if (!req.url) return next();
1682
+ if (req.method && req.method.toUpperCase() !== "GET") return next();
1683
+ if (!req.headers.accept?.includes("text/html")) return next();
1684
+ const pathname = req.url.split("?")[0] || "/";
1685
+ if (pathname.startsWith(state.apiPrefix) || pathname.startsWith("/@fs") || pathname.startsWith("/@id")) return next();
1686
+ const ext = path.extname(pathname);
1687
+ if (ext && ext !== ".html") return next();
1688
+ const redirects = state.options.redirects;
1689
+ if (redirects?.length) {
1690
+ for (const rule of redirects) if (pathname === rule.from) {
1691
+ res.statusCode = rule.status ?? 302;
1692
+ res.setHeader("Location", rule.to);
1693
+ res.end();
1694
+ return;
1695
+ }
1696
+ }
1697
+ try {
1698
+ const pageName = resolvePageName(req.url);
1699
+ const mod = await server.ssrLoadModule(RUNTIME_INSTANCE_MODULE_ID);
1700
+ const requestUrl = new URL(req.url, "http://localhost");
1701
+ const requestHeaders = new Headers();
1702
+ for (const [name, value] of Object.entries(req.headers)) {
1703
+ if (value === void 0) continue;
1704
+ if (Array.isArray(value)) {
1705
+ for (const item of value) requestHeaders.append(name, item);
1706
+ continue;
1707
+ }
1708
+ requestHeaders.set(name, value);
1709
+ }
1710
+ const request = new Request(requestUrl.toString(), {
1711
+ method: req.method || "GET",
1712
+ headers: requestHeaders
1713
+ });
1714
+ let renderPageName = pageName;
1715
+ let renderInput = {
1716
+ url: requestUrl,
1717
+ request,
1718
+ routePath: pathname,
1719
+ site: state.options.site
1720
+ };
1721
+ const middleware = state.options.middleware;
1722
+ if (middleware?.length) {
1723
+ const ctx = {
1724
+ url: requestUrl,
1725
+ request,
1726
+ routePath: pathname,
1727
+ pageName,
1728
+ site: state.options.site
1729
+ };
1730
+ for (const handler of middleware) {
1731
+ const result = await Promise.resolve(handler(ctx));
1732
+ if (result && "redirect" in result) {
1733
+ res.statusCode = result.redirect.status ?? 302;
1734
+ res.setHeader("Location", result.redirect.url);
1735
+ res.end();
1736
+ return;
1737
+ }
1738
+ if (result && "response" in result) {
1739
+ res.statusCode = result.response.status;
1740
+ result.response.headers.forEach((v, k) => res.setHeader(k, v));
1741
+ const body = await result.response.arrayBuffer();
1742
+ res.end(Buffer.from(body));
1743
+ return;
1744
+ }
1745
+ if (result && "rewrite" in result) {
1746
+ if (result.rewrite.pageName !== void 0) renderPageName = result.rewrite.pageName;
1747
+ const { pageName: _pn, ...rest } = result.rewrite;
1748
+ renderInput = {
1749
+ ...renderInput,
1750
+ ...rest
1751
+ };
1752
+ }
1753
+ }
1754
+ }
1755
+ let rendered = await mod.aero.render(renderPageName, renderInput);
1756
+ if (rendered === null) {
1757
+ res.statusCode = 404;
1758
+ rendered = await mod.aero.render("404", renderInput);
1759
+ }
1760
+ if (rendered === null) {
1761
+ res.statusCode = 404;
1762
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1763
+ res.end("<h1>404 — Not Found</h1>");
1764
+ return;
1765
+ }
1766
+ rendered = addDoctype(rendered);
1767
+ const transformed = await server.transformIndexHtml(req.url, rendered);
1768
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1769
+ res.end(transformed);
1770
+ } catch (err) {
1771
+ next(err);
1772
+ }
1773
+ });
1774
+ }
1775
+ };
1776
+ }
1777
+ /**
1778
+ * Aero Vite plugin factory. Returns an array of plugins: config, virtuals, transform, SSR,
1779
+ * static-build, image optimizer, and optionally Nitro (serve only).
1780
+ * HMR for templates and content is handled by Vite's dependency graph when the app uses a single
1781
+ * client entry that imports @aerobuilt/core and calls aero.mount().
1782
+ *
1783
+ * @param options - AeroOptions (server, apiPrefix, dirs). Server can be disabled at runtime via AERO_SERVER=false.
1784
+ * @returns PluginOption[] to pass to Vite's plugins array.
1785
+ */
1786
+ function aero(options = {}) {
1787
+ const dirs = resolveDirs(options.dirs);
1788
+ const apiPrefix = options.apiPrefix || DEFAULT_API_PREFIX;
1789
+ const enableNitro = options.server === true && process.env.AERO_SERVER !== "false";
1790
+ const runtimeInstanceMjsPath = fileURLToPath(new URL("../runtime/instance.mjs", import.meta.url));
1791
+ const runtimeInstanceJsPath = fileURLToPath(new URL("../runtime/instance.js", import.meta.url));
1792
+ const runtimeInstanceTsPath = fileURLToPath(new URL("../runtime/instance.ts", import.meta.url));
1793
+ const runtimeInstancePath = existsSync(runtimeInstanceMjsPath) ? runtimeInstanceMjsPath : existsSync(runtimeInstanceJsPath) ? runtimeInstanceJsPath : runtimeInstanceTsPath;
1794
+ const state = {
1795
+ config: null,
1796
+ aliasResult: null,
1797
+ clientScripts: /* @__PURE__ */ new Map(),
1798
+ runtimeInstancePath,
1799
+ dirs,
1800
+ apiPrefix,
1801
+ options
1802
+ };
1803
+ const aeroConfigPlugin = createAeroConfigPlugin(state);
1804
+ const aeroVirtualsPlugin = createAeroVirtualsPlugin(state);
1805
+ const aeroTransformPlugin = createAeroTransformPlugin(state);
1806
+ const aeroSsrPlugin = createAeroSsrPlugin(state);
1807
+ /** Plugins needed for static build (resolve, load, transform); no SSR/HMR. */
1808
+ const aeroCorePlugins = [
1809
+ aeroConfigPlugin,
1810
+ aeroVirtualsPlugin,
1811
+ aeroTransformPlugin
1812
+ ];
1813
+ const plugins = [
1814
+ aeroConfigPlugin,
1815
+ aeroVirtualsPlugin,
1816
+ aeroTransformPlugin,
1817
+ aeroSsrPlugin,
1818
+ {
1819
+ name: "vite-plugin-aero-static",
1820
+ apply: "build",
1821
+ async closeBundle() {
1822
+ const root = state.config.root;
1823
+ const outDir = state.config.build.outDir;
1824
+ const shouldMinifyHtml = state.config.build.minify !== false && typeof import.meta !== "undefined" && import.meta.env?.PROD;
1825
+ const staticPlugins = options.staticServerPlugins?.length ? [...aeroCorePlugins, ...options.staticServerPlugins] : aeroCorePlugins;
1826
+ await renderStaticPages({
1827
+ root,
1828
+ resolvePath: state.aliasResult.resolve,
1829
+ dirs: options.dirs,
1830
+ apiPrefix,
1831
+ vitePlugins: staticPlugins,
1832
+ minify: shouldMinifyHtml,
1833
+ site: options.site,
1834
+ redirects: options.redirects
1835
+ }, outDir);
1836
+ if (enableNitro) await runNitroBuild(root, writeGeneratedNitroConfig(root, dirs.server, options.redirects));
1837
+ }
1838
+ },
1839
+ ViteImageOptimizer({
1840
+ exclude: void 0,
1841
+ include: void 0,
1842
+ includePublic: true,
1843
+ logStats: true,
1844
+ ansiColors: true,
1845
+ svg: {
1846
+ multipass: true,
1847
+ plugins: [{
1848
+ name: "preset-default",
1849
+ params: {
1850
+ overrides: { cleanupNumericValues: false },
1851
+ cleanupIDs: {
1852
+ minify: false,
1853
+ remove: false
1854
+ },
1855
+ convertPathData: false
1856
+ }
1857
+ }]
1858
+ },
1859
+ png: { quality: 80 },
1860
+ jpeg: { quality: 80 },
1861
+ jpg: { quality: 80 },
1862
+ tiff: { quality: 80 },
1863
+ gif: {},
1864
+ webp: { lossless: true },
1865
+ avif: { lossless: true }
1866
+ })
1867
+ ];
1868
+ if (enableNitro) {
1869
+ const rawNitroPlugins = nitro({ serverDir: dirs.server });
1870
+ const nitroPlugins = Array.isArray(rawNitroPlugins) ? rawNitroPlugins : [rawNitroPlugins];
1871
+ for (const nitroPlugin of nitroPlugins) {
1872
+ if (!nitroPlugin || typeof nitroPlugin !== "object") continue;
1873
+ const originalApply = nitroPlugin.apply;
1874
+ plugins.push({
1875
+ ...nitroPlugin,
1876
+ apply(pluginConfig, env) {
1877
+ if (env.command !== "serve") return false;
1878
+ if (env.isPreview) return false;
1879
+ if (typeof originalApply === "function") return originalApply(pluginConfig, env);
1880
+ if (originalApply) return originalApply === "serve";
1881
+ return true;
1882
+ }
1883
+ });
1884
+ }
1885
+ }
1886
+ return plugins;
1887
+ }
1888
+
1889
+ //#endregion
1890
+ export { aero };