@aerobuilt/core 0.3.1 → 0.3.3

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,67 @@ 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
+ * When changing script taxonomy (is:build, is:inline, etc.), update all consumers per
38
+ * _reference/script-taxonomy-sync.md.
39
+ */
40
+ /** Prefix for data attributes (e.g. `data-each` → ATTR_PREFIX + ATTR_EACH). */
41
+ const ATTR_PREFIX = "data-";
42
+ /** Attribute for spreading props onto a component: `data-props` or `data-props="{ ... }"`. */
43
+ const ATTR_PROPS = "props";
44
+ /** Attribute for iteration: `data-each="{ item in items }"`. */
45
+ const ATTR_EACH = "each";
46
+ const ATTR_IF = "if";
47
+ const ATTR_ELSE_IF = "else-if";
48
+ const ATTR_ELSE = "else";
49
+ /** Slot name (on `<slot>` or content). */
50
+ const ATTR_NAME = "name";
51
+ const ATTR_SLOT = "slot";
52
+ /** Script runs at build time; extracted and becomes render function body. */
53
+ const ATTR_IS_BUILD = "is:build";
54
+ /** Script left in template in place; not extracted. */
55
+ const ATTR_IS_INLINE = "is:inline";
56
+ /** Script hoisted to head; extracted. */
57
+ const ATTR_IS_BLOCKING = "is:blocking";
58
+ /** Script/style receives data from template: `pass:data="{ config }"` or `pass:data="{ ...theme }"`. */
59
+ const ATTR_PASS_DATA = "pass:data";
60
+ /** Script external source (HTML attribute). */
61
+ const ATTR_SRC = "src";
62
+ const TAG_SLOT = "slot";
63
+ /** Default slot name when no name is given. */
64
+ const SLOT_NAME_DEFAULT = "default";
65
+ /** Matches `item in items` for data-each (captures: loop variable, iterable expression). */
66
+ const EACH_REGEX = /^(\w+)\s+in\s+(.+)$/;
67
+ /** Matches tag names ending with `-component` or `-layout`. */
68
+ const COMPONENT_SUFFIX_REGEX = /-(component|layout)$/;
69
+ /** Self-closing tag: `<tag ... />`. */
70
+ const SELF_CLOSING_TAG_REGEX = /<([a-z0-9-]+)([^>]*?)\/>/gi;
71
+ const SELF_CLOSING_TAIL_REGEX = /\/>$/;
72
+ /** HTML void elements that have no closing tag. */
73
+ const VOID_TAGS = new Set([
74
+ "area",
75
+ "base",
76
+ "br",
77
+ "col",
78
+ "embed",
79
+ "hr",
80
+ "img",
81
+ "input",
82
+ "link",
83
+ "meta",
84
+ "param",
85
+ "source",
86
+ "track",
87
+ "wbr"
88
+ ]);
89
+
29
90
  //#endregion
30
91
  //#region src/compiler/build-script-analysis.ts
31
92
  /**
@@ -128,6 +189,92 @@ function analyzeBuildScript(script) {
128
189
  };
129
190
  }
130
191
  /**
192
+ * Extract the props type name from a build script that uses `Aero.props as TypeName`
193
+ * or `const { ... } = Aero.props as TypeName`.
194
+ *
195
+ * @param script - Raw build script content (JS or TS).
196
+ * @returns The type name and whether it was from destructuring, or null if not found.
197
+ */
198
+ function getPropsTypeFromBuildScript(script) {
199
+ if (!script.trim()) return null;
200
+ const result = parseSync(BUILD_SCRIPT_FILENAME, script, {
201
+ sourceType: "module",
202
+ range: true,
203
+ lang: "ts"
204
+ });
205
+ if (result.errors.length > 0) return null;
206
+ const body = result.program.body;
207
+ if (!body) return null;
208
+ for (const stmt of body) {
209
+ const found = findPropsTypeInNode(stmt);
210
+ if (found) return found;
211
+ }
212
+ return null;
213
+ }
214
+ function findPropsTypeInNode(node) {
215
+ if (!node || typeof node !== "object") return null;
216
+ const n = node;
217
+ if (n.type === "TSAsExpression") {
218
+ const expr = n.expression;
219
+ const typeAnnotation = n.typeAnnotation;
220
+ if (isAeroProps(expr)) {
221
+ const typeName = getTypeNameFromAnnotation(typeAnnotation);
222
+ if (typeName) return {
223
+ typeName,
224
+ isFromDestructuring: false
225
+ };
226
+ }
227
+ return null;
228
+ }
229
+ if (n.type === "VariableDeclaration") {
230
+ const declarations = n.declarations;
231
+ for (const decl of declarations ?? []) {
232
+ const d = decl;
233
+ const init = d.init;
234
+ const id = d.id;
235
+ const isDestructuring = id && typeof id === "object" && id.type === "ObjectPattern";
236
+ const found = findPropsTypeInNode(init);
237
+ if (found) return {
238
+ ...found,
239
+ isFromDestructuring: !!isDestructuring
240
+ };
241
+ }
242
+ return null;
243
+ }
244
+ for (const key of [
245
+ "init",
246
+ "expression",
247
+ "argument",
248
+ "body",
249
+ "consequent",
250
+ "alternate"
251
+ ]) {
252
+ const child = n[key];
253
+ if (Array.isArray(child)) for (const c of child) {
254
+ const found = findPropsTypeInNode(c);
255
+ if (found) return found;
256
+ }
257
+ else if (child) {
258
+ const found = findPropsTypeInNode(child);
259
+ if (found) return found;
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+ function isAeroProps(expr) {
265
+ if (expr?.type !== "MemberExpression") return false;
266
+ const obj = expr.object;
267
+ const prop = expr.property;
268
+ const objName = obj?.type === "Identifier" ? obj.name : obj?.name;
269
+ const propName = prop?.type === "Identifier" ? prop.name : prop?.name;
270
+ return objName === "Aero" && propName === "props";
271
+ }
272
+ function getTypeNameFromAnnotation(annotation) {
273
+ if (!annotation) return null;
274
+ if (annotation.type === "TSTypeReference") return annotation.typeName?.name;
275
+ return null;
276
+ }
277
+ /**
131
278
  * Analyze build script for editor use: same as analyzeBuildScript but returns imports with
132
279
  * source ranges (full statement and per-binding) so the extension can map to vscode.Range.
133
280
  *
@@ -190,4 +337,4 @@ function analyzeBuildScriptForEditor(script) {
190
337
  }
191
338
 
192
339
  //#endregion
193
- export { compileInterpolationFromSegments as a, isDirectiveAttr as i, analyzeBuildScriptForEditor as n, tokenizeCurlyInterpolation as o, DEFAULT_DIRECTIVE_PREFIXES as r, analyzeBuildScript as t };
340
+ export { VOID_TAGS as C, tokenizeCurlyInterpolation as D, compileInterpolationFromSegments as E, TAG_SLOT as S, isDirectiveAttr as T, COMPONENT_SUFFIX_REGEX as _, ATTR_ELSE as a, SELF_CLOSING_TAIL_REGEX as b, ATTR_IS_BLOCKING as c, ATTR_NAME as d, ATTR_PASS_DATA as f, ATTR_SRC as g, ATTR_SLOT as h, ATTR_EACH as i, ATTR_IS_BUILD as l, ATTR_PROPS as m, analyzeBuildScriptForEditor as n, ATTR_ELSE_IF as o, ATTR_PREFIX as p, getPropsTypeFromBuildScript as r, ATTR_IF as s, analyzeBuildScript as t, ATTR_IS_INLINE as u, EACH_REGEX as v, DEFAULT_DIRECTIVE_PREFIXES as w, SLOT_NAME_DEFAULT as x, SELF_CLOSING_TAG_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.
@@ -62,6 +65,19 @@ interface BuildScriptImportForEditor extends BuildScriptImport {
62
65
  interface BuildScriptAnalysisForEditorResult {
63
66
  imports: BuildScriptImportForEditor[];
64
67
  }
68
+ /** Result of getPropsTypeFromBuildScript: the type name used in `Aero.props as TypeName`. */
69
+ interface PropsTypeResult {
70
+ typeName: string;
71
+ isFromDestructuring: boolean;
72
+ }
73
+ /**
74
+ * Extract the props type name from a build script that uses `Aero.props as TypeName`
75
+ * or `const { ... } = Aero.props as TypeName`.
76
+ *
77
+ * @param script - Raw build script content (JS or TS).
78
+ * @returns The type name and whether it was from destructuring, or null if not found.
79
+ */
80
+ declare function getPropsTypeFromBuildScript(script: string): PropsTypeResult | null;
65
81
  /**
66
82
  * Analyze build script for editor use: same as analyzeBuildScript but returns imports with
67
83
  * source ranges (full statement and per-binding) so the extension can map to vscode.Range.
@@ -71,4 +87,4 @@ interface BuildScriptAnalysisForEditorResult {
71
87
  */
72
88
  declare function analyzeBuildScriptForEditor(script: string): BuildScriptAnalysisForEditorResult;
73
89
  //#endregion
74
- export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, type InterpolationSegment, type LiteralSegment, type Segment, type TokenizeOptions, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
90
+ export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, COMPONENT_SUFFIX_REGEX, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, type InterpolationSegment, type LiteralSegment, type PropsTypeResult, type Segment, type TokenizeOptions, analyzeBuildScriptForEditor, compileInterpolationFromSegments, getPropsTypeFromBuildScript, 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 { D as tokenizeCurlyInterpolation, E as compileInterpolationFromSegments, T as isDirectiveAttr, _ as COMPONENT_SUFFIX_REGEX, n as analyzeBuildScriptForEditor, r as getPropsTypeFromBuildScript, w as DEFAULT_DIRECTIVE_PREFIXES } from "./build-script-analysis-1t1wROdQ.mjs";
2
2
 
3
- export { DEFAULT_DIRECTIVE_PREFIXES, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
3
+ export { COMPONENT_SUFFIX_REGEX, DEFAULT_DIRECTIVE_PREFIXES, analyzeBuildScriptForEditor, compileInterpolationFromSegments, getPropsTypeFromBuildScript, isDirectiveAttr, tokenizeCurlyInterpolation };
@@ -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 { C as VOID_TAGS, D as tokenizeCurlyInterpolation, E as compileInterpolationFromSegments, S as TAG_SLOT, T as isDirectiveAttr, _ as COMPONENT_SUFFIX_REGEX, a as ATTR_ELSE, b as SELF_CLOSING_TAIL_REGEX, c as ATTR_IS_BLOCKING, d as ATTR_NAME, f as ATTR_PASS_DATA, g as ATTR_SRC, h as ATTR_SLOT, i as ATTR_EACH, l as ATTR_IS_BUILD, m as ATTR_PROPS, o as ATTR_ELSE_IF, p as ATTR_PREFIX, s as ATTR_IF, t as analyzeBuildScript, u as ATTR_IS_INLINE, v as EACH_REGEX, x as SLOT_NAME_DEFAULT, y as SELF_CLOSING_TAG_REGEX } from "../build-script-analysis-1t1wROdQ.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";
@@ -7,6 +7,7 @@ import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
7
7
  import { nitro } from "nitro/vite";
8
8
  import { parseHTML } from "linkedom";
9
9
  import path from "path";
10
+ import { transformSync } from "oxc-transform";
10
11
  import path$1 from "node:path";
11
12
  import { minify } from "html-minifier-next";
12
13
  import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -64,17 +65,18 @@ function extractKeyFromEntry(entry) {
64
65
  /** Virtual URL prefix for extracted client scripts (e.g. `/@aero/client/client/pages/home.js`). Root-relative; no filesystem path. */
65
66
  const CLIENT_SCRIPT_PREFIX = "/@aero/client/";
66
67
  /**
67
- * Virtual URL for one client script. Single script uses `.js`, multiple use `.0.js`, `.1.js`, etc.
68
+ * Virtual URL for one client script.
69
+ * Single script uses `.ts`, multiple use `.0.ts`, `.1.ts`, etc.
68
70
  * Use this in the Vite transform and static build so URL generation is consistent.
69
71
  */
70
72
  function getClientScriptVirtualUrl(baseName, index, total) {
71
- const suffix = total === 1 ? ".js" : `.${index}.js`;
73
+ const suffix = total === 1 ? ".ts" : `.${index}.ts`;
72
74
  return CLIENT_SCRIPT_PREFIX + baseName + suffix;
73
75
  }
74
76
  /** 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";
77
+ const RUNTIME_INSTANCE_MODULE_ID = "virtual:aero/runtime-instance.ts";
76
78
  /** Resolved ID (with `\0` prefix) so Vite treats it as an internal module. */
77
- const RESOLVED_RUNTIME_INSTANCE_MODULE_ID = "\0virtual:aero/runtime-instance";
79
+ const RESOLVED_RUNTIME_INSTANCE_MODULE_ID = "\0virtual:aero/runtime-instance.ts";
78
80
  /** Prefix for virtual empty-CSS modules used when Vite requests .html?html-proxy&inline-css (Aero .html are JS, not HTML with styles). */
79
81
  const AERO_EMPTY_INLINE_CSS_PREFIX = "\0aero:empty-inline-css:";
80
82
  /** Prefix for virtual HTML template modules. Resolving .html to this id returns compiled JS so vite:build-html never sees raw/compiled HTML. */
@@ -117,65 +119,6 @@ function resolveDirs(dirs) {
117
119
  };
118
120
  }
119
121
 
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
122
  //#endregion
180
123
  //#region src/compiler/parser.ts
181
124
  /** Serialize element attributes to a string, excluding given names (case-insensitive). Values are XML-escaped. */
@@ -627,6 +570,11 @@ var Resolver = class {
627
570
 
628
571
  //#endregion
629
572
  //#region src/compiler/codegen.ts
573
+ /** Strip TypeScript syntax from a script string, returning plain JavaScript. */
574
+ function stripTypes(code, filename = "script.ts") {
575
+ if (!code.trim()) return code;
576
+ return transformSync(filename, code, { typescript: { onlyRemoveTypeImports: true } }).code.replace(/(?:^|\n)\s*export\s*\{\s*\}\s*;?/g, "");
577
+ }
630
578
  /** Internal lowerer: walks DOM nodes and builds IR; used by compile(). */
631
579
  var Lowerer = class {
632
580
  resolver;
@@ -1012,7 +960,7 @@ function compile(parsed, options) {
1012
960
  const lowerer = new Lowerer(resolver);
1013
961
  let script = parsed.buildScript ? parsed.buildScript.content : "";
1014
962
  const analysis = analyzeBuildScript(script);
1015
- script = analysis.scriptWithoutImportsAndGetStaticPaths;
963
+ script = stripTypes(analysis.scriptWithoutImportsAndGetStaticPaths);
1016
964
  const getStaticPathsFn = analysis.getStaticPathsFn;
1017
965
  const importsLines = [];
1018
966
  const quote = "\"";
@@ -1067,15 +1015,18 @@ function compile(parsed, options) {
1067
1015
  } else if (isHead) headScripts.push(tagExpr);
1068
1016
  else rootScripts.push(`scripts?.add(${tagExpr});`);
1069
1017
  }
1070
- if (options.blockingScripts) for (const blockingScript of options.blockingScripts) if (blockingScript.passDataExpr) {
1071
- const jsMapExpr = `Object.entries(${validateSingleBracedExpression(blockingScript.passDataExpr, {
1072
- directive: "pass:data",
1073
- tagName: "script"
1074
- })}).map(([k, v]) => "\\nconst " + k + " = " + JSON.stringify(v) + ";").join("")`;
1075
- headScripts.push(`\`<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>\${${jsMapExpr}}${blockingScript.content.replace(/`/g, "\\`")}<\/script>\``);
1076
- } else {
1077
- const escapedContent = blockingScript.content.replace(/'/g, "\\'");
1078
- headScripts.push(`'<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>${escapedContent}<\/script>'`);
1018
+ if (options.blockingScripts) for (const blockingScript of options.blockingScripts) {
1019
+ const strippedContent = stripTypes(blockingScript.content, "blocking.ts");
1020
+ if (blockingScript.passDataExpr) {
1021
+ const jsMapExpr = `Object.entries(${validateSingleBracedExpression(blockingScript.passDataExpr, {
1022
+ directive: "pass:data",
1023
+ tagName: "script"
1024
+ })}).map(([k, v]) => "\\nconst " + k + " = " + JSON.stringify(v) + ";").join("")`;
1025
+ headScripts.push(`\`<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>\${${jsMapExpr}}${strippedContent.replace(/`/g, "\\`")}<\/script>\``);
1026
+ } else {
1027
+ const escapedContent = strippedContent.replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
1028
+ headScripts.push(`\`<script${blockingScript.attrs ? " " + blockingScript.attrs : ""}>${escapedContent}<\/script>\``);
1029
+ }
1079
1030
  }
1080
1031
  const renderFn = emitRenderFunction(script, bodyCode, {
1081
1032
  getStaticPathsFn: getStaticPathsFn || void 0,
@@ -1085,6 +1036,109 @@ function compile(parsed, options) {
1085
1036
  });
1086
1037
  return importsCode + "\n" + renderFn;
1087
1038
  }
1039
+ /**
1040
+ * Compile an HTML template source into a JavaScript module string. Single entry for parse + compile.
1041
+ * When optional `parsed` is provided (e.g. after registering client scripts in the plugin), it is used to avoid parsing twice.
1042
+ *
1043
+ * @param htmlSource - Raw HTML template string.
1044
+ * @param options - CompileOptions (root, resolvePath, importer, optional script overrides).
1045
+ * @param parsed - Optional pre-parsed result; when provided, used instead of parsing htmlSource again.
1046
+ * @returns Module source (async render function + optional getStaticPaths).
1047
+ */
1048
+ function compileTemplate(htmlSource, options, parsed) {
1049
+ const p = parsed ?? parse(htmlSource);
1050
+ return compile(p, {
1051
+ ...options,
1052
+ clientScripts: options.clientScripts ?? p.clientScripts,
1053
+ inlineScripts: options.inlineScripts ?? p.inlineScripts,
1054
+ blockingScripts: options.blockingScripts ?? p.blockingScripts
1055
+ });
1056
+ }
1057
+
1058
+ //#endregion
1059
+ //#region src/vite/rewrite.ts
1060
+ /** Route path to output file path (e.g. '' → index.html, about → about/index.html). */
1061
+ function toOutputFile(routePath) {
1062
+ if (routePath === "") return "index.html";
1063
+ if (routePath === "404") return "404.html";
1064
+ return toPosix(path$1.join(routePath, "index.html"));
1065
+ }
1066
+ /** Relative path from fromDir to targetPath, always starting with ./ when non-empty. */
1067
+ function normalizeRelativeLink(fromDir, targetPath) {
1068
+ const rel = path$1.posix.relative(fromDir, targetPath);
1069
+ if (!rel) return "./";
1070
+ if (rel.startsWith(".")) return rel;
1071
+ return `./${rel}`;
1072
+ }
1073
+ /** Relative path to a route (directory index); appends trailing slash for non-root routes. */
1074
+ function normalizeRelativeRouteLink(fromDir, routePath) {
1075
+ const targetDir = routePath === "" ? "" : routePath;
1076
+ const rel = path$1.posix.relative(fromDir, targetDir);
1077
+ let res = !rel ? "./" : rel.startsWith(".") ? rel : `./${rel}`;
1078
+ if (routePath !== "" && routePath !== "404" && !res.endsWith("/")) res += "/";
1079
+ return res;
1080
+ }
1081
+ function normalizeRoutePathFromHref(value) {
1082
+ if (value === "/") return "";
1083
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
1084
+ }
1085
+ function isSkippableUrl$1(value) {
1086
+ if (!value) return true;
1087
+ return SKIP_PROTOCOL_REGEX.test(value);
1088
+ }
1089
+ const ASSET_IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i;
1090
+ /** Rewrite one absolute URL to dist-relative using manifest and route set; leaves API and external URLs unchanged. */
1091
+ function rewriteAbsoluteUrl(value, fromDir, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1092
+ if (value.startsWith(apiPrefix)) return value;
1093
+ const noQuery = value.split(/[?#]/)[0] || value;
1094
+ const suffix = value.slice(noQuery.length);
1095
+ const manifestKey = noQuery.replace(/^\//, "");
1096
+ let manifestEntry = manifest[noQuery] ?? manifest[manifestKey];
1097
+ if (!manifestEntry && noQuery.startsWith("assets/")) {
1098
+ const entry = Object.values(manifest).find((e) => e?.file === noQuery || e?.file === manifestKey);
1099
+ if (entry) manifestEntry = entry;
1100
+ }
1101
+ if (manifestEntry?.file) return normalizeRelativeLink(fromDir, manifestEntry.assets?.find((a) => ASSET_IMAGE_EXT.test(a)) ?? manifestEntry.file) + suffix;
1102
+ if (noQuery.startsWith("/assets/")) return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1103
+ const route = normalizeRoutePathFromHref(noQuery);
1104
+ if (routeSet.has(route) || route === "") return (route === "404" ? normalizeRelativeLink(fromDir, toOutputFile(route)) : normalizeRelativeRouteLink(fromDir, route)) + suffix;
1105
+ return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1106
+ }
1107
+ /** Rewrite script src (virtual client → hashed asset) and LINK_ATTRS in rendered HTML; add doctype. */
1108
+ function rewriteRenderedHtml(html, outputFile, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1109
+ const fromDir = path$1.posix.dirname(outputFile);
1110
+ const { document } = parseHTML(html);
1111
+ for (const script of Array.from(document.querySelectorAll("script[src]"))) {
1112
+ const src = script.getAttribute("src") || "";
1113
+ if (src.startsWith(CLIENT_SCRIPT_PREFIX)) {
1114
+ const newSrc = rewriteAbsoluteUrl(src, fromDir, manifest, routeSet, apiPrefix);
1115
+ script.setAttribute("src", newSrc);
1116
+ script.setAttribute("type", "module");
1117
+ script.removeAttribute("defer");
1118
+ continue;
1119
+ }
1120
+ if (script.getAttribute("type") === "module") script.removeAttribute("defer");
1121
+ }
1122
+ for (const el of Array.from(document.querySelectorAll("*"))) for (const attrName of LINK_ATTRS) {
1123
+ if (!el.hasAttribute(attrName)) continue;
1124
+ const current = (el.getAttribute(attrName) || "").trim();
1125
+ if (!current || isSkippableUrl$1(current)) continue;
1126
+ if (!current.startsWith("/")) continue;
1127
+ el.setAttribute(attrName, rewriteAbsoluteUrl(current, fromDir, manifest, routeSet, apiPrefix));
1128
+ }
1129
+ const htmlTag = document.documentElement;
1130
+ if (htmlTag) return addDoctype(htmlTag.outerHTML);
1131
+ return addDoctype(document.toString());
1132
+ }
1133
+ /** Prepend `<!doctype html>` if missing. */
1134
+ function addDoctype(html) {
1135
+ return /^\s*<!doctype\s+html/i.test(html) ? html : `<!doctype html>\n${html}`;
1136
+ }
1137
+ function readManifest(distDir) {
1138
+ const manifestPath = path$1.join(distDir, ".vite", "manifest.json");
1139
+ if (!fs.existsSync(manifestPath)) return {};
1140
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1141
+ }
1088
1142
 
1089
1143
  //#endregion
1090
1144
  //#region src/vite/build.ts
@@ -1132,12 +1186,6 @@ function toRouteFromPageName(pageName) {
1132
1186
  if (pageName.endsWith("/index")) return pageName.slice(0, -6);
1133
1187
  return pageName;
1134
1188
  }
1135
- /** Route path to output file path (e.g. '' → index.html, about → about/index.html). */
1136
- function toOutputFile(routePath) {
1137
- if (routePath === "") return "index.html";
1138
- if (routePath === "404") return "404.html";
1139
- return toPosix(path$1.join(routePath, "index.html"));
1140
- }
1141
1189
  /**
1142
1190
  * Generate sitemap.xml from route paths. Only called when site URL is set.
1143
1191
  * Excludes 404. Writes to distDir/sitemap.xml.
@@ -1152,34 +1200,15 @@ function writeSitemap(routePaths, site, distDir) {
1152
1200
  function escapeXml(s) {
1153
1201
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1154
1202
  }
1155
- /** Relative path from fromDir to targetPath, always starting with ./ when non-empty. */
1156
- function normalizeRelativeLink(fromDir, targetPath) {
1157
- const rel = path$1.posix.relative(fromDir, targetPath);
1158
- if (!rel) return "./";
1159
- if (rel.startsWith(".")) return rel;
1160
- return `./${rel}`;
1161
- }
1162
- /** Relative path to a route (directory index); appends trailing slash for non-root routes. */
1163
- function normalizeRelativeRouteLink(fromDir, routePath) {
1164
- const targetDir = routePath === "" ? "" : routePath;
1165
- const rel = path$1.posix.relative(fromDir, targetDir);
1166
- let res = !rel ? "./" : rel.startsWith(".") ? rel : `./${rel}`;
1167
- if (routePath !== "" && routePath !== "404" && !res.endsWith("/")) res += "/";
1168
- return res;
1169
- }
1170
- function normalizeRoutePathFromHref(value) {
1171
- if (value === "/") return "";
1172
- return value.replace(/^\/+/, "").replace(/\/+$/, "");
1203
+ /** Root-relative path for manifest key (posix). */
1204
+ function toManifestKey(root, filePath) {
1205
+ return toPosixRelative(filePath, root);
1173
1206
  }
1174
1207
  /** True if URL is empty or matches SKIP_PROTOCOL_REGEX (external, hash, etc.). */
1175
1208
  function isSkippableUrl(value) {
1176
1209
  if (!value) return true;
1177
1210
  return SKIP_PROTOCOL_REGEX.test(value);
1178
1211
  }
1179
- /** Root-relative path for manifest key (posix). */
1180
- function toManifestKey(root, filePath) {
1181
- return toPosixRelative(filePath, root);
1182
- }
1183
1212
  /** Resolve script/link src or href to absolute path; returns null for external/skippable or unresolvable. */
1184
1213
  function resolveTemplateAssetPath(rawValue, templateFile, root, resolvePath) {
1185
1214
  if (!rawValue || isSkippableUrl(rawValue)) return null;
@@ -1299,60 +1328,6 @@ function walkFiles(dir) {
1299
1328
  }
1300
1329
  return files;
1301
1330
  }
1302
- /** Prepend `<!doctype html>` if missing. */
1303
- function addDoctype(html) {
1304
- return /^\s*<!doctype\s+html/i.test(html) ? html : `<!doctype html>\n${html}`;
1305
- }
1306
- /** Image extensions: when a manifest entry's .file is a .js chunk but .assets lists the real image, use it. */
1307
- const ASSET_IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i;
1308
- /** Rewrite one absolute URL to dist-relative using manifest and route set; leaves API and external URLs unchanged. */
1309
- function rewriteAbsoluteUrl(value, fromDir, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1310
- if (value.startsWith(apiPrefix)) return value;
1311
- const noQuery = value.split(/[?#]/)[0] || value;
1312
- const suffix = value.slice(noQuery.length);
1313
- const manifestKey = noQuery.replace(/^\//, "");
1314
- let manifestEntry = manifest[noQuery] ?? manifest[manifestKey];
1315
- if (!manifestEntry && noQuery.startsWith("assets/")) {
1316
- const entry = Object.values(manifest).find((e) => e?.file === noQuery || e?.file === manifestKey);
1317
- if (entry) manifestEntry = entry;
1318
- }
1319
- if (manifestEntry?.file) return normalizeRelativeLink(fromDir, manifestEntry.assets?.find((a) => ASSET_IMAGE_EXT.test(a)) ?? manifestEntry.file) + suffix;
1320
- if (noQuery.startsWith("/assets/")) return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1321
- const route = normalizeRoutePathFromHref(noQuery);
1322
- if (routeSet.has(route) || route === "") return (route === "404" ? normalizeRelativeLink(fromDir, toOutputFile(route)) : normalizeRelativeRouteLink(fromDir, route)) + suffix;
1323
- return normalizeRelativeLink(fromDir, noQuery.replace(/^\//, "")) + suffix;
1324
- }
1325
- /** Rewrite script src (virtual client → hashed asset) and LINK_ATTRS in rendered HTML; add doctype. */
1326
- function rewriteRenderedHtml(html, outputFile, manifest, routeSet, apiPrefix = DEFAULT_API_PREFIX) {
1327
- const fromDir = path$1.posix.dirname(outputFile);
1328
- const { document } = parseHTML(html);
1329
- for (const script of Array.from(document.querySelectorAll("script[src]"))) {
1330
- const src = script.getAttribute("src") || "";
1331
- if (src.startsWith(CLIENT_SCRIPT_PREFIX)) {
1332
- const newSrc = rewriteAbsoluteUrl(src, fromDir, manifest, routeSet, apiPrefix);
1333
- script.setAttribute("src", newSrc);
1334
- script.setAttribute("type", "module");
1335
- script.removeAttribute("defer");
1336
- continue;
1337
- }
1338
- if (script.getAttribute("type") === "module") script.removeAttribute("defer");
1339
- }
1340
- for (const el of Array.from(document.querySelectorAll("*"))) for (const attrName of LINK_ATTRS) {
1341
- if (!el.hasAttribute(attrName)) continue;
1342
- const current = (el.getAttribute(attrName) || "").trim();
1343
- if (!current || isSkippableUrl(current)) continue;
1344
- if (!current.startsWith("/")) continue;
1345
- el.setAttribute(attrName, rewriteAbsoluteUrl(current, fromDir, manifest, routeSet, apiPrefix));
1346
- }
1347
- const htmlTag = document.documentElement;
1348
- if (htmlTag) return addDoctype(htmlTag.outerHTML);
1349
- return addDoctype(document.toString());
1350
- }
1351
- function readManifest(distDir) {
1352
- const manifestPath = path$1.join(distDir, ".vite", "manifest.json");
1353
- if (!fs.existsSync(manifestPath)) return {};
1354
- return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1355
- }
1356
1331
  /**
1357
1332
  * Render all static pages into outDir: discover pages, expand dynamic routes via getStaticPaths, run Vite in middleware mode, rewrite URLs, optionally minify.
1358
1333
  *
@@ -1591,7 +1566,7 @@ function clientGlobPrefix(clientDir) {
1591
1566
  * Ensures template resolution works for custom dirs (e.g. dirs.client === 'frontend').
1592
1567
  * runtimeImportPath: path that resolves to @aerobuilt/core/runtime from the generated file (e.g. relative to .aero/ for SSR).
1593
1568
  */
1594
- function getRuntimeInstanceVirtualSource(clientDir, runtimeImportPath = "@aerobuilt/core/runtime") {
1569
+ function getRuntimeInstanceVirtualSource(clientDir, runtimeImportPath = "aerobuilt/runtime") {
1595
1570
  const prefix = clientGlobPrefix(clientDir);
1596
1571
  const componentsPattern = `${prefix}/components/**/*.html`;
1597
1572
  const layoutsPattern = `${prefix}/layouts/*.html`;
@@ -1637,7 +1612,10 @@ function createAeroVirtualsPlugin(state) {
1637
1612
  discoverClientScriptContentMap(state.config.root, state.dirs.client).forEach((entry, url) => state.clientScripts.set(url, entry));
1638
1613
  },
1639
1614
  async resolveId(id, importer) {
1640
- if (id === RUNTIME_INSTANCE_MODULE_ID) return state.generatedRuntimeInstancePath ?? RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1615
+ if (id === RUNTIME_INSTANCE_MODULE_ID) {
1616
+ if (state.config?.command === "build" && state.generatedRuntimeInstancePath) return state.generatedRuntimeInstancePath;
1617
+ return RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1618
+ }
1641
1619
  if (id.startsWith(CLIENT_SCRIPT_PREFIX)) return "\0" + id;
1642
1620
  if (id.startsWith("\0" + CLIENT_SCRIPT_PREFIX)) return id;
1643
1621
  if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) return id;
@@ -1665,19 +1643,20 @@ function createAeroVirtualsPlugin(state) {
1665
1643
  if (!state.config || !state.aliasResult) return null;
1666
1644
  this.addWatchFile(filePath);
1667
1645
  try {
1668
- const parsed = parse(readFileSync(filePath, "utf-8"));
1646
+ const code = readFileSync(filePath, "utf-8");
1647
+ const parsed = parse(code);
1669
1648
  const baseName = toPosixRelative(filePath, state.config.root).replace(/\.html$/i, "");
1670
1649
  registerClientScriptsToMap(parsed, baseName, state.clientScripts);
1671
1650
  for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1672
1651
  return {
1673
- code: compile(parsed, {
1652
+ code: compileTemplate(code, {
1674
1653
  root: state.config.root,
1675
1654
  clientScripts: parsed.clientScripts,
1676
1655
  blockingScripts: parsed.blockingScripts,
1677
1656
  inlineScripts: parsed.inlineScripts,
1678
1657
  resolvePath: state.aliasResult.resolve,
1679
1658
  importer: filePath
1680
- }),
1659
+ }, parsed),
1681
1660
  map: null
1682
1661
  };
1683
1662
  } catch {
@@ -1714,14 +1693,14 @@ function createAeroTransformPlugin(state) {
1714
1693
  for (let i = 0; i < parsed.clientScripts.length; i++) parsed.clientScripts[i].content = getClientScriptVirtualUrl(baseName, i, parsed.clientScripts.length);
1715
1694
  }
1716
1695
  return {
1717
- code: compile(parsed, {
1696
+ code: compileTemplate(code, {
1718
1697
  root: state.config.root,
1719
1698
  clientScripts: parsed.clientScripts,
1720
1699
  blockingScripts: parsed.blockingScripts,
1721
1700
  inlineScripts: parsed.inlineScripts,
1722
1701
  resolvePath: state.aliasResult.resolve,
1723
1702
  importer: id
1724
- }),
1703
+ }, parsed),
1725
1704
  map: null
1726
1705
  };
1727
1706
  } catch (err) {
package/env.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Ambient type declarations for Aero templates.
3
+ *
4
+ * Include in your project by adding `"types": ["@aerobuilt/core/env"]` to tsconfig.json,
5
+ * or via a triple-slash directive: `/// <reference types="@aerobuilt/core/env" />`
6
+ *
7
+ * These declarations type the globals available inside `<script is:build>` blocks.
8
+ *
9
+ * Single source of truth: the language server generates its preamble from this file.
10
+ * When changing globals (Aero, renderComponent, *.html, aero:content), run:
11
+ * pnpm --dir packages/language-server exec npm run prebuild
12
+ */
13
+
14
+ /**
15
+ * The `Aero` context object available in build scripts. Provides access to
16
+ * component props, request data, and rendering utilities.
17
+ *
18
+ * Use `Aero.props` (capital A). The lowercase `aero` is not defined at runtime.
19
+ */
20
+ declare const Aero: {
21
+ /** Props passed to the current component or page. */
22
+ props: Record<string, any>
23
+ /** Named slot content (key to HTML string). */
24
+ slots: Record<string, string>
25
+ /** The incoming request (available during SSR). */
26
+ request: Request
27
+ /** The request URL. */
28
+ url: URL
29
+ /** Dynamic route parameters (e.g. `{ id: '42' }` for `/posts/[id]`). */
30
+ params: Record<string, string>
31
+ /** Canonical site URL from `aero.config` (e.g. `'https://example.com'`). */
32
+ site?: string
33
+ }
34
+
35
+ /**
36
+ * Render a child component and return its HTML.
37
+ *
38
+ * @param component - The imported component (default import from an `.html` file).
39
+ * @param props - Props to pass to the component.
40
+ * @param slots - Named slot content.
41
+ * @returns The rendered HTML string.
42
+ */
43
+ declare function renderComponent(
44
+ component: any,
45
+ props?: Record<string, any>,
46
+ slots?: Record<string, string>,
47
+ ): Promise<string>
48
+
49
+ /** Allows importing `.html` component files in build scripts. */
50
+ declare module '*.html' {
51
+ const component: string
52
+ export default component
53
+ }
54
+
55
+ /** Content collections: getCollection, render. Used by the language server for IntelliSense. */
56
+ declare module 'aero:content' {
57
+ interface CollectionEntry {
58
+ id: string
59
+ data: Record<string, any>
60
+ body?: string
61
+ }
62
+ export function getCollection(name: string): Promise<CollectionEntry[]>
63
+ export function render(
64
+ entry: CollectionEntry | Record<string, any>,
65
+ ): Promise<{ html: string }>
66
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@aerobuilt/core",
3
- "version": "0.3.1",
4
- "type": "module",
3
+ "version": "0.3.3",
4
+ "private": false,
5
+ "homepage": "https://github.com/aerobuilt/aero",
5
6
  "license": "MIT",
6
7
  "author": "Jamie Wilson",
7
8
  "repository": {
@@ -9,12 +10,12 @@
9
10
  "url": "https://github.com/aerobuilt/aero.git",
10
11
  "directory": "packages/core"
11
12
  },
12
- "homepage": "https://github.com/aerobuilt/aero",
13
- "private": false,
14
- "types": "./dist/entry-dev.d.mts",
15
13
  "files": [
16
- "dist"
14
+ "dist",
15
+ "env.d.ts"
17
16
  ],
17
+ "type": "module",
18
+ "types": "./dist/entry-dev.d.mts",
18
19
  "exports": {
19
20
  ".": {
20
21
  "types": "./dist/entry-dev.d.mts",
@@ -48,19 +49,27 @@
48
49
  "./editor": {
49
50
  "types": "./dist/entry-editor.d.mts",
50
51
  "default": "./dist/entry-editor.mjs"
51
- }
52
+ },
53
+ "./env": "./env.d.ts"
52
54
  },
53
55
  "dependencies": {
54
56
  "get-tsconfig": "^4.13.6",
55
- "oxc-resolver": "^11.19.0",
56
57
  "html-minifier-next": "^5.1.1",
57
58
  "linkedom": "^0.18.12",
58
59
  "nitro": "^3.0.1-alpha.2",
59
60
  "oxc-parser": "^0.115.0",
61
+ "oxc-resolver": "^11.19.0",
62
+ "oxc-transform": "^0.116.0",
60
63
  "sharp": "^0.34.5",
61
64
  "svgo": "^4.0.0",
62
65
  "vite-plugin-image-optimizer": "^2.0.3",
63
- "@aerobuilt/interpolation": "0.3.1"
66
+ "@aerobuilt/interpolation": "0.3.3"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^25.3.0",
70
+ "tsdown": "^0.21.0-beta.2",
71
+ "typescript": "^5.9.3",
72
+ "vitest": "^4.0.18"
64
73
  },
65
74
  "peerDependencies": {
66
75
  "vite": "^8.0.0-0"
@@ -70,12 +79,6 @@
70
79
  "optional": true
71
80
  }
72
81
  },
73
- "devDependencies": {
74
- "@types/node": "^25.3.0",
75
- "tsdown": "^0.21.0-beta.2",
76
- "typescript": "^5.9.3",
77
- "vitest": "^4.0.18"
78
- },
79
82
  "scripts": {
80
83
  "build": "tsdown",
81
84
  "typecheck": "tsc --noEmit",