@aerobuilt/core 0.3.0 → 0.3.2

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.
@@ -26,6 +26,65 @@ function isDirectiveAttr(attrName, config = defaultConfig) {
26
26
  return prefixes.some((p) => attrName.startsWith(p));
27
27
  }
28
28
 
29
+ //#endregion
30
+ //#region src/compiler/constants.ts
31
+ /**
32
+ * Shared constants for the Aero compiler (parser, codegen, helpers).
33
+ *
34
+ * @remarks
35
+ * Attribute names are used with optional `data-` prefix (e.g. `data-each`). Script taxonomy uses
36
+ * `is:build`, `is:inline`, `is:blocking`; default scripts are treated as client (virtual module).
37
+ */
38
+ /** Prefix for data attributes (e.g. `data-each` → ATTR_PREFIX + ATTR_EACH). */
39
+ const ATTR_PREFIX = "data-";
40
+ /** Attribute for spreading props onto a component: `data-props` or `data-props="{ ... }"`. */
41
+ const ATTR_PROPS = "props";
42
+ /** Attribute for iteration: `data-each="{ item in items }"`. */
43
+ const ATTR_EACH = "each";
44
+ const ATTR_IF = "if";
45
+ const ATTR_ELSE_IF = "else-if";
46
+ const ATTR_ELSE = "else";
47
+ /** Slot name (on `<slot>` or content). */
48
+ const ATTR_NAME = "name";
49
+ const ATTR_SLOT = "slot";
50
+ /** Script runs at build time; extracted and becomes render function body. */
51
+ const ATTR_IS_BUILD = "is:build";
52
+ /** Script left in template in place; not extracted. */
53
+ const ATTR_IS_INLINE = "is:inline";
54
+ /** Script hoisted to head; extracted. */
55
+ const ATTR_IS_BLOCKING = "is:blocking";
56
+ /** Script/style receives data from template: `pass:data="{ config }"` or `pass:data="{ ...theme }"`. */
57
+ const ATTR_PASS_DATA = "pass:data";
58
+ /** Script external source (HTML attribute). */
59
+ const ATTR_SRC = "src";
60
+ const TAG_SLOT = "slot";
61
+ /** Default slot name when no name is given. */
62
+ const SLOT_NAME_DEFAULT = "default";
63
+ /** Matches `item in items` for data-each (captures: loop variable, iterable expression). */
64
+ const EACH_REGEX = /^(\w+)\s+in\s+(.+)$/;
65
+ /** Matches tag names ending with `-component` or `-layout`. */
66
+ const COMPONENT_SUFFIX_REGEX = /-(component|layout)$/;
67
+ /** Self-closing tag: `<tag ... />`. */
68
+ const SELF_CLOSING_TAG_REGEX = /<([a-z0-9-]+)([^>]*?)\/>/gi;
69
+ const SELF_CLOSING_TAIL_REGEX = /\/>$/;
70
+ /** HTML void elements that have no closing tag. */
71
+ const VOID_TAGS = new Set([
72
+ "area",
73
+ "base",
74
+ "br",
75
+ "col",
76
+ "embed",
77
+ "hr",
78
+ "img",
79
+ "input",
80
+ "link",
81
+ "meta",
82
+ "param",
83
+ "source",
84
+ "track",
85
+ "wbr"
86
+ ]);
87
+
29
88
  //#endregion
30
89
  //#region src/compiler/build-script-analysis.ts
31
90
  /**
@@ -190,4 +249,4 @@ function analyzeBuildScriptForEditor(script) {
190
249
  }
191
250
 
192
251
  //#endregion
193
- export { compileInterpolationFromSegments as a, isDirectiveAttr as i, analyzeBuildScriptForEditor as n, tokenizeCurlyInterpolation as o, DEFAULT_DIRECTIVE_PREFIXES as r, analyzeBuildScript as t };
252
+ export { DEFAULT_DIRECTIVE_PREFIXES as C, tokenizeCurlyInterpolation as E, VOID_TAGS as S, compileInterpolationFromSegments as T, EACH_REGEX as _, ATTR_ELSE_IF as a, SLOT_NAME_DEFAULT as b, ATTR_IS_BUILD as c, ATTR_PASS_DATA as d, ATTR_PREFIX as f, COMPONENT_SUFFIX_REGEX as g, ATTR_SRC as h, ATTR_ELSE as i, ATTR_IS_INLINE as l, ATTR_SLOT as m, analyzeBuildScriptForEditor as n, ATTR_IF as o, ATTR_PROPS as p, ATTR_EACH as r, ATTR_IS_BLOCKING as s, analyzeBuildScript as t, ATTR_NAME as u, SELF_CLOSING_TAG_REGEX as v, isDirectiveAttr as w, TAG_SLOT as x, SELF_CLOSING_TAIL_REGEX as y };
@@ -3,8 +3,7 @@ import { InterpolationSegment, LiteralSegment, Segment, TokenizeOptions, compile
3
3
  //#region src/compiler/directive-attributes.d.ts
4
4
  /**
5
5
  * Classifier for directive attributes (Alpine.js, HTMX, Vue, etc.) that should
6
- * skip { } interpolation in the compiler. Replaces ALPINE_ATTR_REGEX with a
7
- * declarative list for clearer semantics and easier extension.
6
+ * skip { } interpolation in the compiler.
8
7
  *
9
8
  * @packageDocumentation
10
9
  */
@@ -30,6 +29,10 @@ declare const DEFAULT_DIRECTIVE_PREFIXES: string[];
30
29
  */
31
30
  declare function isDirectiveAttr(attrName: string, config?: DirectiveAttrConfig): boolean;
32
31
  //#endregion
32
+ //#region src/compiler/constants.d.ts
33
+ /** Matches tag names ending with `-component` or `-layout`. */
34
+ declare const COMPONENT_SUFFIX_REGEX: RegExp;
35
+ //#endregion
33
36
  //#region src/compiler/build-script-analysis.d.ts
34
37
  /**
35
38
  * AST-based analysis of Aero build scripts: extract imports and getStaticPaths export.
@@ -71,4 +74,4 @@ interface BuildScriptAnalysisForEditorResult {
71
74
  */
72
75
  declare function analyzeBuildScriptForEditor(script: string): BuildScriptAnalysisForEditorResult;
73
76
  //#endregion
74
- export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, type InterpolationSegment, type LiteralSegment, type Segment, type TokenizeOptions, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
77
+ export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, COMPONENT_SUFFIX_REGEX, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, type InterpolationSegment, type LiteralSegment, type Segment, type TokenizeOptions, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
@@ -1,3 +1,3 @@
1
- import { a as compileInterpolationFromSegments, i as isDirectiveAttr, n as analyzeBuildScriptForEditor, o as tokenizeCurlyInterpolation, r as DEFAULT_DIRECTIVE_PREFIXES } from "./build-script-analysis-Bd9EyItC.mjs";
1
+ import { C as DEFAULT_DIRECTIVE_PREFIXES, E as tokenizeCurlyInterpolation, T as compileInterpolationFromSegments, g as COMPONENT_SUFFIX_REGEX, n as analyzeBuildScriptForEditor, w as isDirectiveAttr } from "./build-script-analysis-Bll0Ujh4.mjs";
2
2
 
3
- export { DEFAULT_DIRECTIVE_PREFIXES, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
3
+ export { COMPONENT_SUFFIX_REGEX, DEFAULT_DIRECTIVE_PREFIXES, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
@@ -31,10 +31,14 @@ const notify = () => {
31
31
  };
32
32
  if (!globalThis.__AERO_INSTANCE__) globalThis.__AERO_INSTANCE__ = instance;
33
33
  if (!globalThis.__AERO_LISTENERS__) globalThis.__AERO_LISTENERS__ = listeners;
34
- /** Eager globs so pages, layouts, and components are available synchronously for SSR/build. */
35
- const components = import.meta.glob("@components/**/*.html", { eager: true });
36
- const layouts = import.meta.glob("@layouts/*.html", { eager: true });
37
- const pages = import.meta.glob("@pages/**/*.html", { eager: true });
34
+ /**
35
+ * Eager globs so pages, layouts, and components are available synchronously for SSR/build.
36
+ * Patterns use default client dir (./client/...) so Vite's import-glob accepts them; apps with
37
+ * custom dirs rely on path aliases mapping @components/@layouts/@pages to the same structure.
38
+ */
39
+ const components = /* @__PURE__ */ Object.assign({});
40
+ const layouts = /* @__PURE__ */ Object.assign({});
41
+ const pages = /* @__PURE__ */ Object.assign({});
38
42
  aero.registerPages(components);
39
43
  aero.registerPages(layouts);
40
44
  aero.registerPages(pages);
@@ -1,5 +1,5 @@
1
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";
2
+ import { E as tokenizeCurlyInterpolation, S as VOID_TAGS, T as compileInterpolationFromSegments, _ as EACH_REGEX, a as ATTR_ELSE_IF, b as SLOT_NAME_DEFAULT, c as ATTR_IS_BUILD, d as ATTR_PASS_DATA, f as ATTR_PREFIX, g as COMPONENT_SUFFIX_REGEX, h as ATTR_SRC, i as ATTR_ELSE, l as ATTR_IS_INLINE, m as ATTR_SLOT, o as ATTR_IF, p as ATTR_PROPS, r as ATTR_EACH, s as ATTR_IS_BLOCKING, t as analyzeBuildScript, u as ATTR_NAME, v as SELF_CLOSING_TAG_REGEX, w as isDirectiveAttr, x as TAG_SLOT, y as SELF_CLOSING_TAIL_REGEX } from "../build-script-analysis-Bll0Ujh4.mjs";
3
3
  import { loadTsconfigAliases, mergeWithDefaultAliases } from "../utils/aliases.mjs";
4
4
  import { redirectsToRouteRules } from "../utils/redirects.mjs";
5
5
  import { createRequire } from "node:module";
@@ -117,65 +117,6 @@ function resolveDirs(dirs) {
117
117
  };
118
118
  }
119
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
120
  //#endregion
180
121
  //#region src/compiler/parser.ts
181
122
  /** Serialize element attributes to a string, excluding given names (case-insensitive). Values are XML-escaped. */
@@ -648,31 +589,20 @@ var Lowerer = class {
648
589
  }
649
590
  /** Gets the condition value from if/else-if attribute */
650
591
  getCondition(node, attr) {
592
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
651
593
  const plainValue = node.getAttribute(attr);
652
- if (plainValue !== null) return this.requireBracedExpression(plainValue, attr, node);
594
+ if (plainValue !== null) return stripBraces(validateSingleBracedExpression(plainValue, {
595
+ directive: attr,
596
+ tagName
597
+ }));
653
598
  const dataAttr = ATTR_PREFIX + attr;
654
599
  const dataValue = node.getAttribute(dataAttr);
655
- if (dataValue !== null) return this.requireBracedExpression(dataValue, dataAttr, node);
600
+ if (dataValue !== null) return stripBraces(validateSingleBracedExpression(dataValue, {
601
+ directive: dataAttr,
602
+ tagName
603
+ }));
656
604
  return null;
657
605
  }
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
606
  isSingleWrappedExpression(value) {
677
607
  const trimmed = value.trim();
678
608
  if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
@@ -702,7 +632,13 @@ var Lowerer = class {
702
632
  if (isAttr(attr.name, ATTR_PROPS, ATTR_PREFIX)) {
703
633
  const value = attr.value?.trim() || "";
704
634
  if (!value) dataPropsExpression = "...props";
705
- else dataPropsExpression = this.requireBracedExpression(value, attr.name, node);
635
+ else {
636
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
637
+ dataPropsExpression = stripBraces(validateSingleBracedExpression(value, {
638
+ directive: attr.name,
639
+ tagName
640
+ }));
641
+ }
706
642
  continue;
707
643
  }
708
644
  const rawValue = attr.value ?? "";
@@ -725,7 +661,11 @@ var Lowerer = class {
725
661
  if (node.attributes) for (let i = 0; i < node.attributes.length; i++) {
726
662
  const attr = node.attributes[i];
727
663
  if (isAttr(attr.name, ATTR_EACH, ATTR_PREFIX)) {
728
- const match = this.requireBracedExpression(attr.value || "", attr.name, node).match(EACH_REGEX);
664
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
665
+ const match = stripBraces(validateSingleBracedExpression(attr.value || "", {
666
+ directive: attr.name,
667
+ tagName
668
+ })).match(EACH_REGEX);
729
669
  if (!match) {
730
670
  const tagName = node?.tagName?.toLowerCase?.() || "element";
731
671
  throw new Error(`Directive \`${attr.name}\` on <${tagName}> must match "{ item in items }".`);
@@ -1086,6 +1026,109 @@ function compile(parsed, options) {
1086
1026
  });
1087
1027
  return importsCode + "\n" + renderFn;
1088
1028
  }
1029
+ /**
1030
+ * Compile an HTML template source into a JavaScript module string. Single entry for parse + compile.
1031
+ * When optional `parsed` is provided (e.g. after registering client scripts in the plugin), it is used to avoid parsing twice.
1032
+ *
1033
+ * @param htmlSource - Raw HTML template string.
1034
+ * @param options - CompileOptions (root, resolvePath, importer, optional script overrides).
1035
+ * @param parsed - Optional pre-parsed result; when provided, used instead of parsing htmlSource again.
1036
+ * @returns Module source (async render function + optional getStaticPaths).
1037
+ */
1038
+ function compileTemplate(htmlSource, options, parsed) {
1039
+ const p = parsed ?? parse(htmlSource);
1040
+ return compile(p, {
1041
+ ...options,
1042
+ clientScripts: options.clientScripts ?? p.clientScripts,
1043
+ inlineScripts: options.inlineScripts ?? p.inlineScripts,
1044
+ blockingScripts: options.blockingScripts ?? p.blockingScripts
1045
+ });
1046
+ }
1047
+
1048
+ //#endregion
1049
+ //#region src/vite/rewrite.ts
1050
+ /** Route path to output file path (e.g. '' → index.html, about → about/index.html). */
1051
+ function toOutputFile(routePath) {
1052
+ if (routePath === "") return "index.html";
1053
+ if (routePath === "404") return "404.html";
1054
+ return toPosix(path$1.join(routePath, "index.html"));
1055
+ }
1056
+ /** Relative path from fromDir to targetPath, always starting with ./ when non-empty. */
1057
+ function normalizeRelativeLink(fromDir, targetPath) {
1058
+ const rel = path$1.posix.relative(fromDir, targetPath);
1059
+ if (!rel) return "./";
1060
+ if (rel.startsWith(".")) return rel;
1061
+ return `./${rel}`;
1062
+ }
1063
+ /** Relative path to a route (directory index); appends trailing slash for non-root routes. */
1064
+ function normalizeRelativeRouteLink(fromDir, routePath) {
1065
+ const targetDir = routePath === "" ? "" : routePath;
1066
+ const rel = path$1.posix.relative(fromDir, targetDir);
1067
+ let res = !rel ? "./" : rel.startsWith(".") ? rel : `./${rel}`;
1068
+ if (routePath !== "" && routePath !== "404" && !res.endsWith("/")) res += "/";
1069
+ return res;
1070
+ }
1071
+ function normalizeRoutePathFromHref(value) {
1072
+ if (value === "/") return "";
1073
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
1074
+ }
1075
+ function isSkippableUrl$1(value) {
1076
+ if (!value) return true;
1077
+ return SKIP_PROTOCOL_REGEX.test(value);
1078
+ }
1079
+ const ASSET_IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i;
1080
+ /** Rewrite one absolute URL to dist-relative using manifest and route set; leaves API and external URLs unchanged. */
1081
+ function rewriteAbsoluteUrl(value, fromDir, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1082
+ if (value.startsWith(apiPrefix)) return value;
1083
+ const noQuery = value.split(/[?#]/)[0] || value;
1084
+ const suffix = value.slice(noQuery.length);
1085
+ const manifestKey = noQuery.replace(/^\//, "");
1086
+ let manifestEntry = manifest[noQuery] ?? manifest[manifestKey];
1087
+ if (!manifestEntry && noQuery.startsWith("assets/")) {
1088
+ const entry = Object.values(manifest).find((e) => e?.file === noQuery || e?.file === manifestKey);
1089
+ if (entry) manifestEntry = entry;
1090
+ }
1091
+ if (manifestEntry?.file) return normalizeRelativeLink(fromDir, manifestEntry.assets?.find((a) => ASSET_IMAGE_EXT.test(a)) ?? manifestEntry.file) + suffix;
1092
+ if (noQuery.startsWith("/assets/")) return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1093
+ const route = normalizeRoutePathFromHref(noQuery);
1094
+ if (routeSet.has(route) || route === "") return (route === "404" ? normalizeRelativeLink(fromDir, toOutputFile(route)) : normalizeRelativeRouteLink(fromDir, route)) + suffix;
1095
+ return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1096
+ }
1097
+ /** Rewrite script src (virtual client → hashed asset) and LINK_ATTRS in rendered HTML; add doctype. */
1098
+ function rewriteRenderedHtml(html, outputFile, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1099
+ const fromDir = path$1.posix.dirname(outputFile);
1100
+ const { document } = parseHTML(html);
1101
+ for (const script of Array.from(document.querySelectorAll("script[src]"))) {
1102
+ const src = script.getAttribute("src") || "";
1103
+ if (src.startsWith(CLIENT_SCRIPT_PREFIX)) {
1104
+ const newSrc = rewriteAbsoluteUrl(src, fromDir, manifest, routeSet, apiPrefix);
1105
+ script.setAttribute("src", newSrc);
1106
+ script.setAttribute("type", "module");
1107
+ script.removeAttribute("defer");
1108
+ continue;
1109
+ }
1110
+ if (script.getAttribute("type") === "module") script.removeAttribute("defer");
1111
+ }
1112
+ for (const el of Array.from(document.querySelectorAll("*"))) for (const attrName of LINK_ATTRS) {
1113
+ if (!el.hasAttribute(attrName)) continue;
1114
+ const current = (el.getAttribute(attrName) || "").trim();
1115
+ if (!current || isSkippableUrl$1(current)) continue;
1116
+ if (!current.startsWith("/")) continue;
1117
+ el.setAttribute(attrName, rewriteAbsoluteUrl(current, fromDir, manifest, routeSet, apiPrefix));
1118
+ }
1119
+ const htmlTag = document.documentElement;
1120
+ if (htmlTag) return addDoctype(htmlTag.outerHTML);
1121
+ return addDoctype(document.toString());
1122
+ }
1123
+ /** Prepend `<!doctype html>` if missing. */
1124
+ function addDoctype(html) {
1125
+ return /^\s*<!doctype\s+html/i.test(html) ? html : `<!doctype html>\n${html}`;
1126
+ }
1127
+ function readManifest(distDir) {
1128
+ const manifestPath = path$1.join(distDir, ".vite", "manifest.json");
1129
+ if (!fs.existsSync(manifestPath)) return {};
1130
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1131
+ }
1089
1132
 
1090
1133
  //#endregion
1091
1134
  //#region src/vite/build.ts
@@ -1133,12 +1176,6 @@ function toRouteFromPageName(pageName) {
1133
1176
  if (pageName.endsWith("/index")) return pageName.slice(0, -6);
1134
1177
  return pageName;
1135
1178
  }
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
1179
  /**
1143
1180
  * Generate sitemap.xml from route paths. Only called when site URL is set.
1144
1181
  * Excludes 404. Writes to distDir/sitemap.xml.
@@ -1153,34 +1190,15 @@ function writeSitemap(routePaths, site, distDir) {
1153
1190
  function escapeXml(s) {
1154
1191
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1155
1192
  }
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(/\/+$/, "");
1193
+ /** Root-relative path for manifest key (posix). */
1194
+ function toManifestKey(root, filePath) {
1195
+ return toPosixRelative(filePath, root);
1174
1196
  }
1175
1197
  /** True if URL is empty or matches SKIP_PROTOCOL_REGEX (external, hash, etc.). */
1176
1198
  function isSkippableUrl(value) {
1177
1199
  if (!value) return true;
1178
1200
  return SKIP_PROTOCOL_REGEX.test(value);
1179
1201
  }
1180
- /** Root-relative path for manifest key (posix). */
1181
- function toManifestKey(root, filePath) {
1182
- return toPosixRelative(filePath, root);
1183
- }
1184
1202
  /** Resolve script/link src or href to absolute path; returns null for external/skippable or unresolvable. */
1185
1203
  function resolveTemplateAssetPath(rawValue, templateFile, root, resolvePath) {
1186
1204
  if (!rawValue || isSkippableUrl(rawValue)) return null;
@@ -1300,60 +1318,6 @@ function walkFiles(dir) {
1300
1318
  }
1301
1319
  return files;
1302
1320
  }
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
1321
  /**
1358
1322
  * Render all static pages into outDir: discover pages, expand dynamic routes via getStaticPaths, run Vite in middleware mode, rewrite URLs, optionally minify.
1359
1323
  *
@@ -1493,12 +1457,15 @@ function createBuildConfig(options = {}, root = process.cwd()) {
1493
1457
  const require = createRequire(import.meta.url);
1494
1458
  const AERO_DIR = ".aero";
1495
1459
  const NITRO_CONFIG_FILENAME = "nitro.config.mjs";
1460
+ /** Filename for the generated runtime instance (uses app dirs for globs); written under .aero so Vite treats it as a real module. */
1461
+ const RUNTIME_INSTANCE_FILENAME = "runtime-instance.mjs";
1496
1462
  /**
1497
1463
  * Generate Nitro config from Aero options and write to <projectRoot>/.aero/nitro.config.mjs.
1498
1464
  * root is the app/site directory (Vite config.root), e.g. examples/kitchen-sink or a create-aerobuilt project folder.
1465
+ * distDir is the configured output dir (e.g. 'build') so the catch-all route serves from the same path at preview time.
1499
1466
  * Returns the absolute path to .aero (Nitro cwd so it loads this file).
1500
1467
  */
1501
- function writeGeneratedNitroConfig(root, serverDir, redirects) {
1468
+ function writeGeneratedNitroConfig(root, serverDir, redirects, distDir) {
1502
1469
  const aeroDir = path.join(root, AERO_DIR);
1503
1470
  mkdirSync(aeroDir, { recursive: true });
1504
1471
  const routeRules = redirectsToRouteRules(redirects ?? []);
@@ -1507,7 +1474,8 @@ function writeGeneratedNitroConfig(root, serverDir, redirects) {
1507
1474
  output: { dir: path.join(root, ".output") },
1508
1475
  scanDirs: [path.join(root, serverDir)],
1509
1476
  routeRules,
1510
- noPublicDir: true
1477
+ noPublicDir: true,
1478
+ replace: { "process.env.AERO_DIST": JSON.stringify(distDir) }
1511
1479
  };
1512
1480
  const content = `// Generated by Aero — do not edit
1513
1481
  export default ${JSON.stringify(nitroConfig, null, 2)}
@@ -1540,9 +1508,7 @@ function createAeroConfigPlugin(state) {
1540
1508
  enforce: "pre",
1541
1509
  config(userConfig, env) {
1542
1510
  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\").");
1511
+ state.aliasResult = mergeWithDefaultAliases(loadTsconfigAliases(root), root, state.dirs);
1546
1512
  const site = state.options.site ?? "";
1547
1513
  return {
1548
1514
  base: "./",
@@ -1559,6 +1525,13 @@ function createAeroConfigPlugin(state) {
1559
1525
  },
1560
1526
  configResolved(resolvedConfig) {
1561
1527
  state.config = resolvedConfig;
1528
+ const dir = path.join(resolvedConfig.root, AERO_DIR);
1529
+ mkdirSync(dir, { recursive: true });
1530
+ const filePath = path.join(dir, RUNTIME_INSTANCE_FILENAME);
1531
+ const runtimeIndexPath = path.join(path.dirname(state.runtimeInstancePath), "index.mjs");
1532
+ const runtimeImportPath = path.relative(dir, runtimeIndexPath).replace(/\\/g, "/");
1533
+ writeFileSync(filePath, getRuntimeInstanceVirtualSource(state.dirs.client, runtimeImportPath.startsWith(".") ? runtimeImportPath : "./" + runtimeImportPath), "utf-8");
1534
+ state.generatedRuntimeInstancePath = filePath;
1562
1535
  }
1563
1536
  };
1564
1537
  }
@@ -1570,6 +1543,56 @@ function isAeroTemplateHtml(filePath, root, dirs) {
1570
1543
  const sep = path.sep;
1571
1544
  return rel.startsWith("pages" + sep) || rel.startsWith("components" + sep) || rel.startsWith("layouts" + sep);
1572
1545
  }
1546
+ /**
1547
+ * Prefix for import.meta.glob patterns. In virtual modules Vite requires globs to start with '/'
1548
+ * (absolute from project root). Uses app-configured client dir so custom dirs (e.g. frontend/) resolve correctly.
1549
+ */
1550
+ function clientGlobPrefix(clientDir) {
1551
+ const normalized = clientDir.replace(/\\/g, "/").replace(/^\.\/+/, "");
1552
+ return normalized ? `/${normalized}` : "/client";
1553
+ }
1554
+ /**
1555
+ * Virtual module source for the runtime instance with glob patterns using the app's client dir.
1556
+ * Ensures template resolution works for custom dirs (e.g. dirs.client === 'frontend').
1557
+ * runtimeImportPath: path that resolves to @aerobuilt/core/runtime from the generated file (e.g. relative to .aero/ for SSR).
1558
+ */
1559
+ function getRuntimeInstanceVirtualSource(clientDir, runtimeImportPath = "@aerobuilt/core/runtime") {
1560
+ const prefix = clientGlobPrefix(clientDir);
1561
+ const componentsPattern = `${prefix}/components/**/*.html`;
1562
+ const layoutsPattern = `${prefix}/layouts/*.html`;
1563
+ const pagesPattern = `${prefix}/pages/**/*.html`;
1564
+ return `import { Aero } from ${JSON.stringify(runtimeImportPath)}
1565
+
1566
+ const instance = globalThis.__AERO_INSTANCE__ || new Aero()
1567
+ const listeners = globalThis.__AERO_LISTENERS__ || new Set()
1568
+ const aero = instance
1569
+
1570
+ const onUpdate = (cb) => {
1571
+ listeners.add(cb)
1572
+ return () => listeners.delete(cb)
1573
+ }
1574
+ const notify = () => {
1575
+ listeners.forEach((cb) => cb())
1576
+ }
1577
+
1578
+ if (!globalThis.__AERO_INSTANCE__) globalThis.__AERO_INSTANCE__ = instance
1579
+ if (!globalThis.__AERO_LISTENERS__) globalThis.__AERO_LISTENERS__ = listeners
1580
+
1581
+ const components = import.meta.glob(${JSON.stringify(componentsPattern)}, { eager: true })
1582
+ const layouts = import.meta.glob(${JSON.stringify(layoutsPattern)}, { eager: true })
1583
+ const pages = import.meta.glob(${JSON.stringify(pagesPattern)}, { eager: true })
1584
+
1585
+ aero.registerPages(components)
1586
+ aero.registerPages(layouts)
1587
+ aero.registerPages(pages)
1588
+
1589
+ notify()
1590
+
1591
+ if (import.meta.hot) import.meta.hot.accept()
1592
+
1593
+ export { aero, onUpdate }
1594
+ `;
1595
+ }
1573
1596
  function createAeroVirtualsPlugin(state) {
1574
1597
  return {
1575
1598
  name: "vite-plugin-aero-virtuals",
@@ -1579,7 +1602,7 @@ function createAeroVirtualsPlugin(state) {
1579
1602
  discoverClientScriptContentMap(state.config.root, state.dirs.client).forEach((entry, url) => state.clientScripts.set(url, entry));
1580
1603
  },
1581
1604
  async resolveId(id, importer) {
1582
- if (id === RUNTIME_INSTANCE_MODULE_ID) return RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1605
+ if (id === RUNTIME_INSTANCE_MODULE_ID) return state.generatedRuntimeInstancePath ?? RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1583
1606
  if (id.startsWith(CLIENT_SCRIPT_PREFIX)) return "\0" + id;
1584
1607
  if (id.startsWith("\0" + CLIENT_SCRIPT_PREFIX)) return id;
1585
1608
  if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) return id;
@@ -1600,26 +1623,27 @@ function createAeroVirtualsPlugin(state) {
1600
1623
  return null;
1601
1624
  },
1602
1625
  load(id) {
1603
- if (id === RESOLVED_RUNTIME_INSTANCE_MODULE_ID) return `export { aero, onUpdate } from ${JSON.stringify(state.runtimeInstancePath)}`;
1626
+ if (id === RESOLVED_RUNTIME_INSTANCE_MODULE_ID) return getRuntimeInstanceVirtualSource(state.dirs.client);
1604
1627
  if (id.startsWith(AERO_EMPTY_INLINE_CSS_PREFIX)) return "/* aero: no inline styles */";
1605
1628
  if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) {
1606
1629
  const filePath = id.slice(AERO_HTML_VIRTUAL_PREFIX.length).replace(/\.aero$/i, ".html");
1607
1630
  if (!state.config || !state.aliasResult) return null;
1608
1631
  this.addWatchFile(filePath);
1609
1632
  try {
1610
- const parsed = parse(readFileSync(filePath, "utf-8"));
1633
+ const code = readFileSync(filePath, "utf-8");
1634
+ const parsed = parse(code);
1611
1635
  const baseName = toPosixRelative(filePath, state.config.root).replace(/\.html$/i, "");
1612
1636
  registerClientScriptsToMap(parsed, baseName, state.clientScripts);
1613
1637
  for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1614
1638
  return {
1615
- code: compile(parsed, {
1639
+ code: compileTemplate(code, {
1616
1640
  root: state.config.root,
1617
1641
  clientScripts: parsed.clientScripts,
1618
1642
  blockingScripts: parsed.blockingScripts,
1619
1643
  inlineScripts: parsed.inlineScripts,
1620
1644
  resolvePath: state.aliasResult.resolve,
1621
1645
  importer: filePath
1622
- }),
1646
+ }, parsed),
1623
1647
  map: null
1624
1648
  };
1625
1649
  } catch {
@@ -1656,14 +1680,14 @@ function createAeroTransformPlugin(state) {
1656
1680
  for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1657
1681
  }
1658
1682
  return {
1659
- code: compile(parsed, {
1683
+ code: compileTemplate(code, {
1660
1684
  root: state.config.root,
1661
1685
  clientScripts: parsed.clientScripts,
1662
1686
  blockingScripts: parsed.blockingScripts,
1663
1687
  inlineScripts: parsed.inlineScripts,
1664
1688
  resolvePath: state.aliasResult.resolve,
1665
1689
  importer: id
1666
- }),
1690
+ }, parsed),
1667
1691
  map: null
1668
1692
  };
1669
1693
  } catch (err) {
@@ -1796,6 +1820,7 @@ function aero(options = {}) {
1796
1820
  aliasResult: null,
1797
1821
  clientScripts: /* @__PURE__ */ new Map(),
1798
1822
  runtimeInstancePath,
1823
+ generatedRuntimeInstancePath: null,
1799
1824
  dirs,
1800
1825
  apiPrefix,
1801
1826
  options
@@ -1833,7 +1858,7 @@ function aero(options = {}) {
1833
1858
  site: options.site,
1834
1859
  redirects: options.redirects
1835
1860
  }, outDir);
1836
- if (enableNitro) await runNitroBuild(root, writeGeneratedNitroConfig(root, dirs.server, options.redirects));
1861
+ if (enableNitro) await runNitroBuild(root, writeGeneratedNitroConfig(root, dirs.server, options.redirects, dirs.dist));
1837
1862
  }
1838
1863
  },
1839
1864
  ViteImageOptimizer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerobuilt/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Jamie Wilson",
@@ -60,7 +60,7 @@
60
60
  "sharp": "^0.34.5",
61
61
  "svgo": "^4.0.0",
62
62
  "vite-plugin-image-optimizer": "^2.0.3",
63
- "@aerobuilt/interpolation": "0.3.0"
63
+ "@aerobuilt/interpolation": "0.3.2"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "vite": "^8.0.0-0"
@@ -77,7 +77,7 @@
77
77
  "vitest": "^4.0.18"
78
78
  },
79
79
  "scripts": {
80
- "build": "tsdown src/entry-dev.ts src/entry-prod.ts src/entry-editor.ts src/types.ts src/vite/index.ts src/utils/aliases.ts src/utils/redirects.ts src/runtime/index.ts src/runtime/instance.ts --format esm --dts --clean --out-dir dist --external @content/site",
80
+ "build": "tsdown",
81
81
  "typecheck": "tsc --noEmit",
82
82
  "test": "vitest run"
83
83
  }