@aerobuilt/core 0.2.10 → 0.3.1

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 +49 -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 +1949 -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,193 @@
1
+ import { compileInterpolationFromSegments, tokenizeCurlyInterpolation } from "@aerobuilt/interpolation";
2
+ import { ImportNameKind, parseSync } from "oxc-parser";
3
+
4
+ //#region src/compiler/directive-attributes.ts
5
+ /** Default prefixes: Alpine.js (x-*) and shorthand (@, :, .). */
6
+ const DEFAULT_DIRECTIVE_PREFIXES = [
7
+ "x-",
8
+ "@",
9
+ ":",
10
+ "."
11
+ ];
12
+ const defaultConfig = {
13
+ prefixes: DEFAULT_DIRECTIVE_PREFIXES,
14
+ exactNames: []
15
+ };
16
+ /**
17
+ * Returns true if the attribute name is a directive that should skip
18
+ * { } interpolation (e.g. Alpine x-model, :disabled, @click).
19
+ *
20
+ * @param attrName - HTML attribute name (e.g. 'x-data', ':disabled').
21
+ * @param config - Optional config; uses default Alpine/shorthand prefixes when omitted.
22
+ */
23
+ function isDirectiveAttr(attrName, config = defaultConfig) {
24
+ const prefixes = config.prefixes ?? defaultConfig.prefixes;
25
+ if ((config.exactNames ?? defaultConfig.exactNames).includes(attrName)) return true;
26
+ return prefixes.some((p) => attrName.startsWith(p));
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/compiler/build-script-analysis.ts
31
+ /**
32
+ * AST-based analysis of Aero build scripts: extract imports and getStaticPaths export.
33
+ *
34
+ * @remarks
35
+ * Uses oxc-parser (TypeScript-capable) so the same pipeline supports JS and TS in
36
+ * `<script is:build>`. Returns structured data for codegen to rewrite imports and
37
+ * emit getStaticPaths as a named export.
38
+ */
39
+ const BUILD_SCRIPT_FILENAME = "build.ts";
40
+ /**
41
+ * Analyze build script source: parse with oxc, extract static imports and
42
+ * `export [async] function getStaticPaths(...)`, return bindings and script with those removed.
43
+ *
44
+ * @param script - Raw build script content (JS or TS).
45
+ * @returns Structured result for codegen. On parse error, throws.
46
+ */
47
+ function analyzeBuildScript(script) {
48
+ if (!script.trim()) return {
49
+ imports: [],
50
+ getStaticPathsFn: null,
51
+ scriptWithoutImportsAndGetStaticPaths: script
52
+ };
53
+ const result = parseSync(BUILD_SCRIPT_FILENAME, script, {
54
+ sourceType: "module",
55
+ range: true,
56
+ lang: "ts"
57
+ });
58
+ const errors = result.errors;
59
+ if (errors.length > 0) {
60
+ const first = errors[0];
61
+ throw new Error(`[aero] Build script parse error: ${first.message}${first.codeframe ? "\n" + first.codeframe : ""}`);
62
+ }
63
+ const mod = result.module;
64
+ const program = result.program;
65
+ const imports = [];
66
+ for (const imp of mod.staticImports) {
67
+ const specifier = imp.moduleRequest.value;
68
+ let defaultBinding = null;
69
+ const namedBindings = [];
70
+ let namespaceBinding = null;
71
+ for (const entry of imp.entries) {
72
+ if (entry.isType) continue;
73
+ const local = entry.localName.value;
74
+ switch (entry.importName.kind) {
75
+ case ImportNameKind.Default:
76
+ defaultBinding = local;
77
+ break;
78
+ case ImportNameKind.NamespaceObject:
79
+ namespaceBinding = local;
80
+ break;
81
+ case ImportNameKind.Name: {
82
+ const imported = entry.importName.name ?? local;
83
+ namedBindings.push({
84
+ imported,
85
+ local
86
+ });
87
+ break;
88
+ }
89
+ default: break;
90
+ }
91
+ }
92
+ imports.push({
93
+ specifier,
94
+ defaultBinding,
95
+ namedBindings,
96
+ namespaceBinding
97
+ });
98
+ }
99
+ let getStaticPathsRange = null;
100
+ const body = program.body;
101
+ if (body) for (const stmt of body) {
102
+ if (stmt.type !== "ExportNamedDeclaration") continue;
103
+ const decl = stmt.declaration;
104
+ if (!decl || decl.type !== "FunctionDeclaration") continue;
105
+ if (decl.id?.name !== "getStaticPaths") continue;
106
+ const range = stmt.range;
107
+ if (range) {
108
+ getStaticPathsRange = range;
109
+ break;
110
+ }
111
+ }
112
+ const getStaticPathsFn = getStaticPathsRange !== null ? script.slice(getStaticPathsRange[0], getStaticPathsRange[1]) : null;
113
+ const rangesToRemove = [];
114
+ if (getStaticPathsRange) rangesToRemove.push(getStaticPathsRange);
115
+ for (const imp of mod.staticImports) rangesToRemove.push([imp.start, imp.end]);
116
+ rangesToRemove.sort((a, b) => a[0] - b[0]);
117
+ const parts = [];
118
+ let lastEnd = 0;
119
+ for (const [start, end] of rangesToRemove) {
120
+ if (start > lastEnd) parts.push(script.slice(lastEnd, start));
121
+ lastEnd = end;
122
+ }
123
+ if (lastEnd < script.length) parts.push(script.slice(lastEnd));
124
+ return {
125
+ imports,
126
+ getStaticPathsFn,
127
+ scriptWithoutImportsAndGetStaticPaths: parts.join("").trim()
128
+ };
129
+ }
130
+ /**
131
+ * Analyze build script for editor use: same as analyzeBuildScript but returns imports with
132
+ * source ranges (full statement and per-binding) so the extension can map to vscode.Range.
133
+ *
134
+ * @param script - Raw build script content (JS or TS).
135
+ * @returns Imports with range and bindingRanges. On parse error, throws.
136
+ */
137
+ function analyzeBuildScriptForEditor(script) {
138
+ if (!script.trim()) return { imports: [] };
139
+ const result = parseSync(BUILD_SCRIPT_FILENAME, script, {
140
+ sourceType: "module",
141
+ range: true,
142
+ lang: "ts"
143
+ });
144
+ const errors = result.errors;
145
+ if (errors.length > 0) {
146
+ const first = errors[0];
147
+ throw new Error(`[aero] Build script parse error: ${first.message}${first.codeframe ? "\n" + first.codeframe : ""}`);
148
+ }
149
+ const mod = result.module;
150
+ const imports = [];
151
+ for (const imp of mod.staticImports) {
152
+ const specifier = imp.moduleRequest.value;
153
+ let defaultBinding = null;
154
+ const namedBindings = [];
155
+ let namespaceBinding = null;
156
+ const bindingRanges = {};
157
+ for (const entry of imp.entries) {
158
+ if (entry.isType) continue;
159
+ const local = entry.localName.value;
160
+ bindingRanges[local] = [entry.localName.start, entry.localName.end];
161
+ switch (entry.importName.kind) {
162
+ case ImportNameKind.Default:
163
+ defaultBinding = local;
164
+ break;
165
+ case ImportNameKind.NamespaceObject:
166
+ namespaceBinding = local;
167
+ break;
168
+ case ImportNameKind.Name: {
169
+ const imported = entry.importName.name ?? local;
170
+ namedBindings.push({
171
+ imported,
172
+ local
173
+ });
174
+ break;
175
+ }
176
+ default: break;
177
+ }
178
+ }
179
+ imports.push({
180
+ specifier,
181
+ defaultBinding,
182
+ namedBindings,
183
+ namespaceBinding,
184
+ range: [imp.start, imp.end],
185
+ specifierRange: [imp.moduleRequest.start, imp.moduleRequest.end],
186
+ bindingRanges
187
+ });
188
+ }
189
+ return { imports };
190
+ }
191
+
192
+ //#endregion
193
+ export { compileInterpolationFromSegments as a, isDirectiveAttr as i, analyzeBuildScriptForEditor as n, tokenizeCurlyInterpolation as o, DEFAULT_DIRECTIVE_PREFIXES as r, analyzeBuildScript as t };
@@ -1,17 +1,7 @@
1
- import { MountOptions } from './types.js';
2
- import { Aero } from './runtime/index.js';
3
- import 'vite';
4
-
5
- /**
6
- * Client entry for the Aero framework.
7
- *
8
- * @remarks
9
- * Re-exports the shared `aero` instance with a `mount()` method attached.
10
- * Used as the app's client entry point (e.g. in the main script that runs in the browser).
11
- * Assumes HTML was server-rendered or pre-rendered; `mount()` does not perform an initial
12
- * render, only sets up the root element and (in dev) HMR re-renders.
13
- */
1
+ import { d as MountOptions } from "./types-CLHGhnGA.mjs";
2
+ import { Aero } from "./runtime/index.mjs";
14
3
 
4
+ //#region src/entry-dev.d.ts
15
5
  /**
16
6
  * Attach the app to a DOM element and optionally set up HMR re-renders.
17
7
  *
@@ -28,7 +18,7 @@ import 'vite';
28
18
  */
29
19
  declare function mount(options?: MountOptions): Promise<void>;
30
20
  declare const _default: Aero & {
31
- mount: typeof mount;
21
+ mount: typeof mount;
32
22
  };
33
-
34
- export { _default as default };
23
+ //#endregion
24
+ export { _default as default };
@@ -0,0 +1,127 @@
1
+ import { n as resolvePageName } from "./routing-Bai79LCq.mjs";
2
+ import { aero, onUpdate } from "./runtime/instance.mjs";
3
+
4
+ //#region src/runtime/client.ts
5
+ /**
6
+ * Parse a full HTML string into head and body fragments.
7
+ * If the document has no head/body, body falls back to the raw HTML.
8
+ *
9
+ * @param html - Full document HTML (e.g. from the compiled render function).
10
+ * @returns Head inner HTML and body inner HTML.
11
+ */
12
+ function extractDocumentParts(html) {
13
+ const doc = new DOMParser().parseFromString(html, "text/html");
14
+ return {
15
+ head: doc.head?.innerHTML?.trim() || "",
16
+ body: doc.body?.innerHTML ?? html
17
+ };
18
+ }
19
+ /** Guard: skip starting a new render while one is in progress (avoids overlapping HMR runs). */
20
+ let rendering = false;
21
+ /** CSS selectors for nodes that must be kept in <head> during HMR (Vite dev client and dev-injected modules). */
22
+ const PERSISTENT_SELECTORS = ["script[src*=\"/@vite/client\"]", "[data-vite-dev-id]"].join(", ");
23
+ /**
24
+ * Replace document head content with new HTML while preserving nodes matching PERSISTENT_SELECTORS.
25
+ * Avoids re-adding duplicate Vite dev nodes by skipping if a node with the same data-vite-dev-id or script src already exists.
26
+ */
27
+ function updateHead(headContent) {
28
+ const headEl = document.head;
29
+ const queriedNodes = headEl.querySelectorAll(PERSISTENT_SELECTORS);
30
+ const persistentSet = new Set(Array.from(queriedNodes));
31
+ for (const node of Array.from(headEl.children)) {
32
+ if (persistentSet.has(node)) continue;
33
+ headEl.removeChild(node);
34
+ }
35
+ const frag = new DOMParser().parseFromString(`<head>${headContent}</head>`, "text/html");
36
+ const nodes = Array.from(frag.head?.childNodes || []);
37
+ for (const node of nodes) {
38
+ if (node.nodeType === Node.ELEMENT_NODE) {
39
+ const el = node;
40
+ if (el.matches(PERSISTENT_SELECTORS)) {
41
+ const devId = el.getAttribute("data-vite-dev-id");
42
+ if (devId && headEl.querySelector(`[data-vite-dev-id="${devId}"]`)) continue;
43
+ if (el instanceof HTMLScriptElement && el.src && headEl.querySelector(`script[src="${el.src}"]`)) continue;
44
+ }
45
+ }
46
+ headEl.appendChild(document.importNode(node, true));
47
+ }
48
+ }
49
+ /**
50
+ * Path prefix for content/docs routes. When on such a route in dev, we fetch HTML from the
51
+ * dev server instead of re-running the full render (including markdown) in the browser.
52
+ */
53
+ const DOCS_PATH_PREFIX = "/docs";
54
+ /**
55
+ * Re-render the current page in the browser (e.g. on HMR).
56
+ * For content routes (e.g. `/docs/*`), fetches HTML from the dev server to avoid running the
57
+ * full markdown pipeline in the browser. Otherwise resolves page name and uses `renderFn`.
58
+ *
59
+ * @param appEl - Root element to receive the new body content (e.g. `#app`).
60
+ * @param renderFn - Async function that returns full document HTML for a given page name (e.g. `aero.render`).
61
+ */
62
+ async function renderPage(appEl, renderFn) {
63
+ if (rendering) return;
64
+ rendering = true;
65
+ const pathname = window.location.pathname;
66
+ const pageName = resolvePageName(pathname);
67
+ try {
68
+ let html;
69
+ if (typeof window !== "undefined" && (pathname === DOCS_PATH_PREFIX || pathname.startsWith(DOCS_PATH_PREFIX + "/")) && import.meta.hot) {
70
+ const res = await fetch(pathname, { headers: { Accept: "text/html" } });
71
+ if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
72
+ html = await res.text();
73
+ } else html = await renderFn(pageName);
74
+ const { head, body } = extractDocumentParts(html);
75
+ if (head) updateHead(head);
76
+ appEl.innerHTML = body;
77
+ } catch (err) {
78
+ appEl.innerHTML = `<h1>Error rendering page: ${pageName}</h1><pre>${String(err)}</pre>`;
79
+ console.error("[aero] Render Error:", err);
80
+ } finally {
81
+ rendering = false;
82
+ }
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/entry-dev.ts
87
+ /** Bound `aero.render` so the same function reference is passed to `renderPage` for HMR re-renders. */
88
+ const coreRender = aero.render.bind(aero);
89
+ /** HMR state: root element for re-renders and unsubscribe. Set when mount() runs; subscription is registered once per dev session. */
90
+ const hmrState = {
91
+ lastEl: null,
92
+ unsubscribe: null
93
+ };
94
+ /**
95
+ * Attach the app to a DOM element and optionally set up HMR re-renders.
96
+ *
97
+ * @remarks
98
+ * Does not perform an initial render: we assume the document already has SSR/pre-rendered
99
+ * HTML. Only runs `onRender` if provided, then in dev (Vite HMR) subscribes to template
100
+ * updates and re-renders into the same target.
101
+ *
102
+ * @param options - Mount options. Defaults to `{ target: '#app' }`.
103
+ * @param options.target - CSS selector (e.g. `#app`) or the root `HTMLElement`. Defaults to `#app`.
104
+ * @param options.onRender - Called with the root element after mount and after each HMR re-render.
105
+ * @returns A promise that resolves immediately. Does not wait for any async render (no initial render).
106
+ * @throws When `target` is a string and no matching element is found in the document.
107
+ */
108
+ function mount(options = {}) {
109
+ const { target = "#app", onRender } = options;
110
+ const el = typeof target === "string" ? document.querySelector(target) : target;
111
+ if (!el) throw new Error("Target element not found: " + target);
112
+ hmrState.lastEl = el;
113
+ if (onRender) onRender(el);
114
+ const done = Promise.resolve();
115
+ if (import.meta.hot && !hmrState.unsubscribe) hmrState.unsubscribe = onUpdate(() => {
116
+ const el = hmrState.lastEl;
117
+ if (el) renderPage(el, coreRender).then(() => {
118
+ if (onRender) onRender(el);
119
+ });
120
+ });
121
+ return done;
122
+ }
123
+ aero.mount = mount;
124
+ var entry_dev_default = aero;
125
+
126
+ //#endregion
127
+ export { entry_dev_default as default };
@@ -1,5 +1,6 @@
1
- export { InterpolationSegment, LiteralSegment, Segment, TokenizeOptions, compileInterpolationFromSegments, tokenizeCurlyInterpolation } from '@aerobuilt/interpolation';
1
+ import { InterpolationSegment, LiteralSegment, Segment, TokenizeOptions, compileInterpolationFromSegments, tokenizeCurlyInterpolation } from "@aerobuilt/interpolation";
2
2
 
3
+ //#region src/compiler/directive-attributes.d.ts
3
4
  /**
4
5
  * Classifier for directive attributes (Alpine.js, HTMX, Vue, etc.) that should
5
6
  * skip { } interpolation in the compiler. Replaces ALPINE_ATTR_REGEX with a
@@ -13,10 +14,10 @@ export { InterpolationSegment, LiteralSegment, Segment, TokenizeOptions, compile
13
14
  * like @, :, . match event and binding syntax.
14
15
  */
15
16
  interface DirectiveAttrConfig {
16
- /** Prefixes that identify directive attributes (e.g. 'x-', '@', 'hx-'). */
17
- prefixes?: string[];
18
- /** Exact attribute names to treat as directives. */
19
- exactNames?: string[];
17
+ /** Prefixes that identify directive attributes (e.g. 'x-', '@', 'hx-'). */
18
+ prefixes?: string[];
19
+ /** Exact attribute names to treat as directives. */
20
+ exactNames?: string[];
20
21
  }
21
22
  /** Default prefixes: Alpine.js (x-*) and shorthand (@, :, .). */
22
23
  declare const DEFAULT_DIRECTIVE_PREFIXES: string[];
@@ -28,7 +29,8 @@ declare const DEFAULT_DIRECTIVE_PREFIXES: string[];
28
29
  * @param config - Optional config; uses default Alpine/shorthand prefixes when omitted.
29
30
  */
30
31
  declare function isDirectiveAttr(attrName: string, config?: DirectiveAttrConfig): boolean;
31
-
32
+ //#endregion
33
+ //#region src/compiler/build-script-analysis.d.ts
32
34
  /**
33
35
  * AST-based analysis of Aero build scripts: extract imports and getStaticPaths export.
34
36
  *
@@ -39,26 +41,26 @@ declare function isDirectiveAttr(attrName: string, config?: DirectiveAttrConfig)
39
41
  */
40
42
  /** Single import entry for codegen: specifier and bindings. */
41
43
  interface BuildScriptImport {
42
- specifier: string;
43
- defaultBinding: string | null;
44
- namedBindings: Array<{
45
- imported: string;
46
- local: string;
47
- }>;
48
- namespaceBinding: string | null;
44
+ specifier: string;
45
+ defaultBinding: string | null;
46
+ namedBindings: Array<{
47
+ imported: string;
48
+ local: string;
49
+ }>;
50
+ namespaceBinding: string | null;
49
51
  }
50
52
  /** Editor-oriented import entry: same bindings as BuildScriptImport plus source range and per-binding ranges. */
51
53
  interface BuildScriptImportForEditor extends BuildScriptImport {
52
- /** Character range of the full import statement [start, end]. */
53
- range: [number, number];
54
- /** Character range of the specifier string (path) within the script. */
55
- specifierRange: [number, number];
56
- /** Per-binding character ranges: local name -> [start, end]. */
57
- bindingRanges?: Record<string, [number, number]>;
54
+ /** Character range of the full import statement [start, end]. */
55
+ range: [number, number];
56
+ /** Character range of the specifier string (path) within the script. */
57
+ specifierRange: [number, number];
58
+ /** Per-binding character ranges: local name -> [start, end]. */
59
+ bindingRanges?: Record<string, [number, number]>;
58
60
  }
59
61
  /** Result of analyzeBuildScriptForEditor: imports with source ranges for editor use (e.g. definition provider). */
60
62
  interface BuildScriptAnalysisForEditorResult {
61
- imports: BuildScriptImportForEditor[];
63
+ imports: BuildScriptImportForEditor[];
62
64
  }
63
65
  /**
64
66
  * Analyze build script for editor use: same as analyzeBuildScript but returns imports with
@@ -68,5 +70,5 @@ interface BuildScriptAnalysisForEditorResult {
68
70
  * @returns Imports with range and bindingRanges. On parse error, throws.
69
71
  */
70
72
  declare function analyzeBuildScriptForEditor(script: string): BuildScriptAnalysisForEditorResult;
71
-
72
- export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, analyzeBuildScriptForEditor, isDirectiveAttr };
73
+ //#endregion
74
+ export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, type InterpolationSegment, type LiteralSegment, type Segment, type TokenizeOptions, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
@@ -0,0 +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";
2
+
3
+ export { DEFAULT_DIRECTIVE_PREFIXES, analyzeBuildScriptForEditor, compileInterpolationFromSegments, isDirectiveAttr, tokenizeCurlyInterpolation };
@@ -0,0 +1,10 @@
1
+ import { d as MountOptions } from "./types-CLHGhnGA.mjs";
2
+ import { Aero } from "./runtime/index.mjs";
3
+
4
+ //#region src/entry-prod.d.ts
5
+ declare function mount(options?: MountOptions): Promise<void>;
6
+ declare const _default: Aero & {
7
+ mount: typeof mount;
8
+ };
9
+ //#endregion
10
+ export { _default as default };
@@ -0,0 +1,15 @@
1
+ import { Aero } from "./runtime/index.mjs";
2
+
3
+ //#region src/entry-prod.ts
4
+ function mount(options = {}) {
5
+ const { target = "#app", onRender } = options;
6
+ const el = typeof target === "string" ? document.querySelector(target) : target;
7
+ if (!el) throw new Error("Target element not found: " + target);
8
+ if (onRender) onRender(el);
9
+ return Promise.resolve();
10
+ }
11
+ const aero = new Aero();
12
+ aero.mount = mount;
13
+
14
+ //#endregion
15
+ export { aero as default };
@@ -0,0 +1,198 @@
1
+ //#region src/utils/path.ts
2
+ /**
3
+ * Shared path utilities for the Aero framework.
4
+ */
5
+ /**
6
+ * Convert a path to POSIX format (forward slashes).
7
+ * Handles Windows backslashes and ensures consistent path format across platforms.
8
+ */
9
+ function toPosix(value) {
10
+ return value.replace(/\\/g, "/");
11
+ }
12
+ /**
13
+ * Normalize a path to be relative to root and POSIX format.
14
+ */
15
+ function toPosixRelative(value, root) {
16
+ const valuePosix = toPosix(value);
17
+ const rootPosix = toPosix(root);
18
+ if (valuePosix.startsWith(rootPosix + "/")) return valuePosix.slice(rootPosix.length + 1);
19
+ return valuePosix;
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/utils/route-pattern.ts
24
+ /** Matches a single [param] segment; capture is param name (no leading . or ..., no ]). */
25
+ const PARAM_SEGMENT_REGEX = /^\[([^.\]\[]+)\]$/;
26
+ /**
27
+ * Parses a route pattern (page name) into segments.
28
+ *
29
+ * @param pattern - Page name with optional [param] segments (e.g. `blog/[id]`, `[slug]`).
30
+ * @returns Parsed pattern; segments are static literals or params.
31
+ */
32
+ function parseRoutePattern(pattern) {
33
+ return { segments: pattern.split("/").filter(Boolean).map((seg) => {
34
+ const paramMatch = seg.match(PARAM_SEGMENT_REGEX);
35
+ if (paramMatch) return {
36
+ type: "param",
37
+ name: paramMatch[1]
38
+ };
39
+ return {
40
+ type: "static",
41
+ value: seg
42
+ };
43
+ }) };
44
+ }
45
+ /**
46
+ * Returns true if the pattern contains at least one param segment.
47
+ *
48
+ * @param pattern - Page name (e.g. `blog/[id]`).
49
+ */
50
+ function isDynamicRoutePattern(pattern) {
51
+ const { segments } = parseRoutePattern(pattern);
52
+ return segments.some((s) => s.type === "param");
53
+ }
54
+ /**
55
+ * Matches a concrete page name against a route pattern.
56
+ *
57
+ * @param pattern - Route pattern (e.g. `blog/[id]`).
58
+ * @param pageName - Requested page name (e.g. `blog/123`).
59
+ * @returns Extracted params if the page name matches, otherwise null.
60
+ */
61
+ function matchRoutePattern(pattern, pageName) {
62
+ const { segments } = parseRoutePattern(pattern);
63
+ const requestedSegments = pageName.split("/").filter(Boolean);
64
+ if (segments.length !== requestedSegments.length) return null;
65
+ const params = {};
66
+ for (let i = 0; i < segments.length; i++) {
67
+ const seg = segments[i];
68
+ const requestSeg = requestedSegments[i];
69
+ if (seg.type === "param") params[seg.name] = decodeURIComponent(requestSeg);
70
+ else if (seg.value !== requestSeg) return null;
71
+ }
72
+ return params;
73
+ }
74
+ /**
75
+ * Replaces each [key] in the pattern with params[key].
76
+ *
77
+ * @param pattern - Route pattern (e.g. `docs/[slug]`).
78
+ * @param params - Map of param names to values.
79
+ * @returns Expanded page name (e.g. `docs/intro`).
80
+ * @throws If a required param is missing from params.
81
+ */
82
+ function expandRoutePattern(pattern, params) {
83
+ const { segments } = parseRoutePattern(pattern);
84
+ const parts = [];
85
+ for (const seg of segments) if (seg.type === "param") {
86
+ if (!(seg.name in params)) throw new Error(`[aero] getStaticPaths: missing param "${seg.name}" for pattern "${pattern}". Provided params: ${JSON.stringify(params)}`);
87
+ parts.push(params[seg.name]);
88
+ } else parts.push(seg.value);
89
+ return parts.join("/");
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/utils/routing.ts
94
+ /**
95
+ * Derives a canonical page key from a resolved file path (e.g. from import.meta.glob).
96
+ * Used by the runtime when registering pages so lookup keys match build's page names.
97
+ *
98
+ * - Paths containing `pages/`: key is the segment after `pages/` (e.g. `client/pages/about.html` → `about`).
99
+ * - Multiple segments without `pages/` (e.g. layouts, components): key is full path without extension.
100
+ * - Single segment: key is that segment.
101
+ *
102
+ * @param path - Resolved path (with or without `.html`).
103
+ * @returns Canonical key for pagesMap lookup.
104
+ */
105
+ function pagePathToKey(path) {
106
+ const withoutExt = toPosix(path).replace(/\.html$/i, "");
107
+ if (withoutExt.includes("pages/")) return withoutExt.split("pages/").pop();
108
+ const segments = withoutExt.split("/").filter(Boolean);
109
+ if (segments.length > 1) return segments.join("/");
110
+ return segments.pop() || path;
111
+ }
112
+ /**
113
+ * Resolves a URL path to an Aero page name.
114
+ *
115
+ * @param url - Full URL or path (e.g. `/about`, `/about?foo=bar`, `/blog/`).
116
+ * @returns Page name for lookup in pagesMap (e.g. `index`, `about`, `blog/index`, `blog/post`).
117
+ *
118
+ * @example
119
+ * resolvePageName('/') // 'index'
120
+ * resolvePageName('/about') // 'about'
121
+ * resolvePageName('/about.html') // 'about'
122
+ * resolvePageName('/blog/') // 'blog/index'
123
+ * resolvePageName('/blog/post') // 'blog/post'
124
+ * resolvePageName('/about?foo=bar') // 'about'
125
+ */
126
+ function resolvePageName(url) {
127
+ const [pathPart] = url.split("?");
128
+ let clean = pathPart || "/";
129
+ if (clean === "/" || clean === "") return "index";
130
+ if (clean.endsWith("/")) clean = clean + "index";
131
+ clean = clean.replace(/^\//, "");
132
+ clean = clean.replace(/\.html$/, "");
133
+ return clean || "index";
134
+ }
135
+ /**
136
+ * Matches a page name (e.g. `posts/42`) against registered dynamic routes (e.g. `posts/[id]`).
137
+ * Returns the first matching module, its canonical page name, and extracted params.
138
+ *
139
+ * @param pageName - Requested page name (e.g. `blog/123`).
140
+ * @param pagesMap - Map of page key → module (from registerPages).
141
+ * @returns PageTargetResult or null if no match.
142
+ */
143
+ function resolveDynamicPage(pageName, pagesMap) {
144
+ for (const [key, mod] of Object.entries(pagesMap)) {
145
+ if (!key.includes("[") || !key.includes("]") || key.includes(".")) continue;
146
+ const params = matchRoutePattern(key, pageName);
147
+ if (params != null) return {
148
+ module: mod,
149
+ pageName: key,
150
+ params
151
+ };
152
+ }
153
+ return null;
154
+ }
155
+ /**
156
+ * Resolves a component (page name string or module) to a page target for rendering.
157
+ * Implements the same lookup order as the runtime: direct key, directory index, home fallback,
158
+ * dynamic routes, then trailing-slash stripping.
159
+ *
160
+ * @param component - Page name (e.g. `'index'`, `'about'`) or a module object.
161
+ * @param pagesMap - Map of page key → module (from registerPages).
162
+ * @returns PageTargetResult or null if not found.
163
+ */
164
+ function resolvePageTarget(component, pagesMap) {
165
+ if (typeof component !== "string") return component != null ? {
166
+ module: component,
167
+ pageName: "index",
168
+ params: {}
169
+ } : null;
170
+ const pageName = component;
171
+ let target = pagesMap[pageName];
172
+ if (!target) target = pagesMap[`${pageName}/index`];
173
+ if (!target && pageName === "index") target = pagesMap["home"];
174
+ if (!target) {
175
+ const dynamicMatch = resolveDynamicPage(pageName, pagesMap) ?? resolveDynamicPage(`${pageName}/index`, pagesMap);
176
+ if (dynamicMatch) return dynamicMatch;
177
+ }
178
+ if (!target && pageName.endsWith("/index")) {
179
+ const stripped = pageName.slice(0, -6);
180
+ target = pagesMap[stripped];
181
+ if (target) return {
182
+ module: target,
183
+ pageName: stripped,
184
+ params: {}
185
+ };
186
+ const dynamicMatch = resolveDynamicPage(stripped, pagesMap);
187
+ if (dynamicMatch) return dynamicMatch;
188
+ }
189
+ if (!target) return null;
190
+ return {
191
+ module: target,
192
+ pageName,
193
+ params: {}
194
+ };
195
+ }
196
+
197
+ //#endregion
198
+ export { isDynamicRoutePattern as a, expandRoutePattern as i, resolvePageName as n, toPosix as o, resolvePageTarget as r, toPosixRelative as s, pagePathToKey as t };