@aerobuilt/core 0.2.1 → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Aero
3
- } from "./chunk-3OZCI7DL.js";
3
+ } from "./chunk-5ZNUGZOW.js";
4
4
 
5
5
  // src/runtime/instance.ts
6
6
  var instance = globalThis.__AERO_INSTANCE__ || new Aero();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  pagePathToKey,
3
3
  resolvePageTarget
4
- } from "./chunk-7A3WBPH4.js";
4
+ } from "./chunk-JAMYN2VX.js";
5
5
 
6
6
  // src/runtime/index.ts
7
7
  var Aero = class {
@@ -11,6 +11,57 @@ function toPosixRelative(value, root) {
11
11
  return valuePosix;
12
12
  }
13
13
 
14
+ // src/utils/route-pattern.ts
15
+ var PARAM_SEGMENT_REGEX = /^\[([^.\]\[]+)\]$/;
16
+ function parseRoutePattern(pattern) {
17
+ const rawSegments = pattern.split("/").filter(Boolean);
18
+ const segments = rawSegments.map((seg) => {
19
+ const paramMatch = seg.match(PARAM_SEGMENT_REGEX);
20
+ if (paramMatch) {
21
+ return { type: "param", name: paramMatch[1] };
22
+ }
23
+ return { type: "static", value: seg };
24
+ });
25
+ return { segments };
26
+ }
27
+ function isDynamicRoutePattern(pattern) {
28
+ const { segments } = parseRoutePattern(pattern);
29
+ return segments.some((s) => s.type === "param");
30
+ }
31
+ function matchRoutePattern(pattern, pageName) {
32
+ const { segments } = parseRoutePattern(pattern);
33
+ const requestedSegments = pageName.split("/").filter(Boolean);
34
+ if (segments.length !== requestedSegments.length) return null;
35
+ const params = {};
36
+ for (let i = 0; i < segments.length; i++) {
37
+ const seg = segments[i];
38
+ const requestSeg = requestedSegments[i];
39
+ if (seg.type === "param") {
40
+ params[seg.name] = decodeURIComponent(requestSeg);
41
+ } else if (seg.value !== requestSeg) {
42
+ return null;
43
+ }
44
+ }
45
+ return params;
46
+ }
47
+ function expandRoutePattern(pattern, params) {
48
+ const { segments } = parseRoutePattern(pattern);
49
+ const parts = [];
50
+ for (const seg of segments) {
51
+ if (seg.type === "param") {
52
+ if (!(seg.name in params)) {
53
+ throw new Error(
54
+ `[aero] getStaticPaths: missing param "${seg.name}" for pattern "${pattern}". Provided params: ${JSON.stringify(params)}`
55
+ );
56
+ }
57
+ parts.push(params[seg.name]);
58
+ } else {
59
+ parts.push(seg.value);
60
+ }
61
+ }
62
+ return parts.join("/");
63
+ }
64
+
14
65
  // src/utils/routing.ts
15
66
  function pagePathToKey(path) {
16
67
  const withoutExt = toPosix(path).replace(/\.html$/i, "");
@@ -35,27 +86,10 @@ function resolvePageName(url) {
35
86
  return clean || "index";
36
87
  }
37
88
  function resolveDynamicPage(pageName, pagesMap) {
38
- const requestedSegments = pageName.split("/").filter(Boolean);
39
89
  for (const [key, mod] of Object.entries(pagesMap)) {
40
90
  if (!key.includes("[") || !key.includes("]") || key.includes(".")) continue;
41
- const keySegments = key.split("/").filter(Boolean);
42
- if (keySegments.length !== requestedSegments.length) continue;
43
- const params = {};
44
- let matched = true;
45
- for (let i = 0; i < keySegments.length; i++) {
46
- const routeSegment = keySegments[i];
47
- const requestSegment = requestedSegments[i];
48
- const dynamicMatch = routeSegment.match(/^\[(.+)\]$/);
49
- if (dynamicMatch) {
50
- params[dynamicMatch[1]] = decodeURIComponent(requestSegment);
51
- continue;
52
- }
53
- if (routeSegment !== requestSegment) {
54
- matched = false;
55
- break;
56
- }
57
- }
58
- if (matched) {
91
+ const params = matchRoutePattern(key, pageName);
92
+ if (params != null) {
59
93
  return { module: mod, pageName: key, params };
60
94
  }
61
95
  }
@@ -91,6 +125,8 @@ function resolvePageTarget(component, pagesMap) {
91
125
  export {
92
126
  toPosix,
93
127
  toPosixRelative,
128
+ isDynamicRoutePattern,
129
+ expandRoutePattern,
94
130
  pagePathToKey,
95
131
  resolvePageName,
96
132
  resolvePageTarget
@@ -0,0 +1,184 @@
1
+ // src/compiler/tokenizer.ts
2
+ import {
3
+ tokenizeCurlyInterpolation,
4
+ compileInterpolationFromSegments
5
+ } from "@aerobuilt/interpolation";
6
+
7
+ // src/compiler/directive-attributes.ts
8
+ var DEFAULT_DIRECTIVE_PREFIXES = ["x-", "@", ":", "."];
9
+ var defaultConfig = {
10
+ prefixes: DEFAULT_DIRECTIVE_PREFIXES,
11
+ exactNames: []
12
+ };
13
+ function isDirectiveAttr(attrName, config = defaultConfig) {
14
+ const prefixes = config.prefixes ?? defaultConfig.prefixes;
15
+ const exactNames = config.exactNames ?? defaultConfig.exactNames;
16
+ if (exactNames.includes(attrName)) return true;
17
+ return prefixes.some((p) => attrName.startsWith(p));
18
+ }
19
+
20
+ // src/compiler/build-script-analysis.ts
21
+ import { parseSync, ImportNameKind } from "oxc-parser";
22
+ var BUILD_SCRIPT_FILENAME = "build.ts";
23
+ function analyzeBuildScript(script) {
24
+ if (!script.trim()) {
25
+ return {
26
+ imports: [],
27
+ getStaticPathsFn: null,
28
+ scriptWithoutImportsAndGetStaticPaths: script
29
+ };
30
+ }
31
+ const result = parseSync(BUILD_SCRIPT_FILENAME, script, {
32
+ sourceType: "module",
33
+ range: true,
34
+ lang: "ts"
35
+ });
36
+ const errors = result.errors;
37
+ if (errors.length > 0) {
38
+ const first = errors[0];
39
+ throw new Error(
40
+ `[aero] Build script parse error: ${first.message}${first.codeframe ? "\n" + first.codeframe : ""}`
41
+ );
42
+ }
43
+ const mod = result.module;
44
+ const program = result.program;
45
+ const imports = [];
46
+ for (const imp of mod.staticImports) {
47
+ const specifier = imp.moduleRequest.value;
48
+ let defaultBinding = null;
49
+ const namedBindings = [];
50
+ let namespaceBinding = null;
51
+ for (const entry of imp.entries) {
52
+ if (entry.isType) continue;
53
+ const local = entry.localName.value;
54
+ switch (entry.importName.kind) {
55
+ case ImportNameKind.Default:
56
+ defaultBinding = local;
57
+ break;
58
+ case ImportNameKind.NamespaceObject:
59
+ namespaceBinding = local;
60
+ break;
61
+ case ImportNameKind.Name: {
62
+ const imported = entry.importName.name ?? local;
63
+ namedBindings.push({ imported, local });
64
+ break;
65
+ }
66
+ default:
67
+ break;
68
+ }
69
+ }
70
+ imports.push({
71
+ specifier,
72
+ defaultBinding,
73
+ namedBindings,
74
+ namespaceBinding
75
+ });
76
+ }
77
+ let getStaticPathsRange = null;
78
+ const body = program.body;
79
+ if (body) {
80
+ for (const stmt of body) {
81
+ if (stmt.type !== "ExportNamedDeclaration") continue;
82
+ const decl = stmt.declaration;
83
+ if (!decl || decl.type !== "FunctionDeclaration") continue;
84
+ const name = decl.id?.name;
85
+ if (name !== "getStaticPaths") continue;
86
+ const range = stmt.range;
87
+ if (range) {
88
+ getStaticPathsRange = range;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ const getStaticPathsFn = getStaticPathsRange !== null ? script.slice(getStaticPathsRange[0], getStaticPathsRange[1]) : null;
94
+ const rangesToRemove = [];
95
+ if (getStaticPathsRange) {
96
+ rangesToRemove.push(getStaticPathsRange);
97
+ }
98
+ for (const imp of mod.staticImports) {
99
+ rangesToRemove.push([imp.start, imp.end]);
100
+ }
101
+ rangesToRemove.sort((a, b) => a[0] - b[0]);
102
+ const parts = [];
103
+ let lastEnd = 0;
104
+ for (const [start, end] of rangesToRemove) {
105
+ if (start > lastEnd) {
106
+ parts.push(script.slice(lastEnd, start));
107
+ }
108
+ lastEnd = end;
109
+ }
110
+ if (lastEnd < script.length) {
111
+ parts.push(script.slice(lastEnd));
112
+ }
113
+ const scriptWithoutImportsAndGetStaticPaths = parts.join("").trim();
114
+ return {
115
+ imports,
116
+ getStaticPathsFn,
117
+ scriptWithoutImportsAndGetStaticPaths
118
+ };
119
+ }
120
+ function analyzeBuildScriptForEditor(script) {
121
+ if (!script.trim()) {
122
+ return { imports: [] };
123
+ }
124
+ const result = parseSync(BUILD_SCRIPT_FILENAME, script, {
125
+ sourceType: "module",
126
+ range: true,
127
+ lang: "ts"
128
+ });
129
+ const errors = result.errors;
130
+ if (errors.length > 0) {
131
+ const first = errors[0];
132
+ throw new Error(
133
+ `[aero] Build script parse error: ${first.message}${first.codeframe ? "\n" + first.codeframe : ""}`
134
+ );
135
+ }
136
+ const mod = result.module;
137
+ const imports = [];
138
+ for (const imp of mod.staticImports) {
139
+ const specifier = imp.moduleRequest.value;
140
+ let defaultBinding = null;
141
+ const namedBindings = [];
142
+ let namespaceBinding = null;
143
+ const bindingRanges = {};
144
+ for (const entry of imp.entries) {
145
+ if (entry.isType) continue;
146
+ const local = entry.localName.value;
147
+ bindingRanges[local] = [entry.localName.start, entry.localName.end];
148
+ switch (entry.importName.kind) {
149
+ case ImportNameKind.Default:
150
+ defaultBinding = local;
151
+ break;
152
+ case ImportNameKind.NamespaceObject:
153
+ namespaceBinding = local;
154
+ break;
155
+ case ImportNameKind.Name: {
156
+ const imported = entry.importName.name ?? local;
157
+ namedBindings.push({ imported, local });
158
+ break;
159
+ }
160
+ default:
161
+ break;
162
+ }
163
+ }
164
+ imports.push({
165
+ specifier,
166
+ defaultBinding,
167
+ namedBindings,
168
+ namespaceBinding,
169
+ range: [imp.start, imp.end],
170
+ specifierRange: [imp.moduleRequest.start, imp.moduleRequest.end],
171
+ bindingRanges
172
+ });
173
+ }
174
+ return { imports };
175
+ }
176
+
177
+ export {
178
+ tokenizeCurlyInterpolation,
179
+ compileInterpolationFromSegments,
180
+ DEFAULT_DIRECTIVE_PREFIXES,
181
+ isDirectiveAttr,
182
+ analyzeBuildScript,
183
+ analyzeBuildScriptForEditor
184
+ };
package/dist/entry-dev.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  aero,
3
3
  onUpdate
4
- } from "./chunk-5GK7XRII.js";
5
- import "./chunk-3OZCI7DL.js";
4
+ } from "./chunk-4DAK56WB.js";
5
+ import "./chunk-5ZNUGZOW.js";
6
6
  import {
7
7
  resolvePageName
8
- } from "./chunk-7A3WBPH4.js";
8
+ } from "./chunk-JAMYN2VX.js";
9
9
 
10
10
  // src/runtime/client.ts
11
11
  function extractDocumentParts(html) {
@@ -0,0 +1,72 @@
1
+ export { InterpolationSegment, LiteralSegment, Segment, TokenizeOptions, compileInterpolationFromSegments, tokenizeCurlyInterpolation } from '@aerobuilt/interpolation';
2
+
3
+ /**
4
+ * Classifier for directive attributes (Alpine.js, HTMX, Vue, etc.) that should
5
+ * skip { } interpolation in the compiler. Replaces ALPINE_ATTR_REGEX with a
6
+ * declarative list for clearer semantics and easier extension.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /**
11
+ * Configurable list of directive attribute prefixes and optional exact names.
12
+ * Prefixes are checked with attrName.startsWith(prefix); single-char prefixes
13
+ * like @, :, . match event and binding syntax.
14
+ */
15
+ 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[];
20
+ }
21
+ /** Default prefixes: Alpine.js (x-*) and shorthand (@, :, .). */
22
+ declare const DEFAULT_DIRECTIVE_PREFIXES: string[];
23
+ /**
24
+ * Returns true if the attribute name is a directive that should skip
25
+ * { } interpolation (e.g. Alpine x-model, :disabled, @click).
26
+ *
27
+ * @param attrName - HTML attribute name (e.g. 'x-data', ':disabled').
28
+ * @param config - Optional config; uses default Alpine/shorthand prefixes when omitted.
29
+ */
30
+ declare function isDirectiveAttr(attrName: string, config?: DirectiveAttrConfig): boolean;
31
+
32
+ /**
33
+ * AST-based analysis of Aero build scripts: extract imports and getStaticPaths export.
34
+ *
35
+ * @remarks
36
+ * Uses oxc-parser (TypeScript-capable) so the same pipeline supports JS and TS in
37
+ * `<script is:build>`. Returns structured data for codegen to rewrite imports and
38
+ * emit getStaticPaths as a named export.
39
+ */
40
+ /** Single import entry for codegen: specifier and bindings. */
41
+ interface BuildScriptImport {
42
+ specifier: string;
43
+ defaultBinding: string | null;
44
+ namedBindings: Array<{
45
+ imported: string;
46
+ local: string;
47
+ }>;
48
+ namespaceBinding: string | null;
49
+ }
50
+ /** Editor-oriented import entry: same bindings as BuildScriptImport plus source range and per-binding ranges. */
51
+ 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]>;
58
+ }
59
+ /** Result of analyzeBuildScriptForEditor: imports with source ranges for editor use (e.g. definition provider). */
60
+ interface BuildScriptAnalysisForEditorResult {
61
+ imports: BuildScriptImportForEditor[];
62
+ }
63
+ /**
64
+ * Analyze build script for editor use: same as analyzeBuildScript but returns imports with
65
+ * source ranges (full statement and per-binding) so the extension can map to vscode.Range.
66
+ *
67
+ * @param script - Raw build script content (JS or TS).
68
+ * @returns Imports with range and bindingRanges. On parse error, throws.
69
+ */
70
+ declare function analyzeBuildScriptForEditor(script: string): BuildScriptAnalysisForEditorResult;
71
+
72
+ export { type BuildScriptAnalysisForEditorResult, type BuildScriptImportForEditor, DEFAULT_DIRECTIVE_PREFIXES, type DirectiveAttrConfig, analyzeBuildScriptForEditor, isDirectiveAttr };
@@ -0,0 +1,14 @@
1
+ import {
2
+ DEFAULT_DIRECTIVE_PREFIXES,
3
+ analyzeBuildScriptForEditor,
4
+ compileInterpolationFromSegments,
5
+ isDirectiveAttr,
6
+ tokenizeCurlyInterpolation
7
+ } from "./chunk-VTEG2UU3.js";
8
+ export {
9
+ DEFAULT_DIRECTIVE_PREFIXES,
10
+ analyzeBuildScriptForEditor,
11
+ compileInterpolationFromSegments,
12
+ isDirectiveAttr,
13
+ tokenizeCurlyInterpolation
14
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Aero
3
- } from "./chunk-3OZCI7DL.js";
4
- import "./chunk-7A3WBPH4.js";
3
+ } from "./chunk-5ZNUGZOW.js";
4
+ import "./chunk-JAMYN2VX.js";
5
5
 
6
6
  // src/entry-prod.ts
7
7
  function mount(options = {}) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Aero
3
- } from "../chunk-3OZCI7DL.js";
4
- import "../chunk-7A3WBPH4.js";
3
+ } from "../chunk-5ZNUGZOW.js";
4
+ import "../chunk-JAMYN2VX.js";
5
5
  export {
6
6
  Aero
7
7
  };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  aero,
3
3
  onUpdate
4
- } from "../chunk-5GK7XRII.js";
5
- import "../chunk-3OZCI7DL.js";
6
- import "../chunk-7A3WBPH4.js";
4
+ } from "../chunk-4DAK56WB.js";
5
+ import "../chunk-5ZNUGZOW.js";
6
+ import "../chunk-JAMYN2VX.js";
7
7
  export {
8
8
  aero,
9
9
  onUpdate
@@ -1,9 +1,17 @@
1
1
  import {
2
+ analyzeBuildScript,
3
+ compileInterpolationFromSegments,
4
+ isDirectiveAttr,
5
+ tokenizeCurlyInterpolation
6
+ } from "../chunk-VTEG2UU3.js";
7
+ import {
8
+ expandRoutePattern,
9
+ isDynamicRoutePattern,
2
10
  pagePathToKey,
3
11
  resolvePageName,
4
12
  toPosix,
5
13
  toPosixRelative
6
- } from "../chunk-7A3WBPH4.js";
14
+ } from "../chunk-JAMYN2VX.js";
7
15
  import {
8
16
  redirectsToRouteRules
9
17
  } from "../chunk-F7MXQXLM.js";
@@ -95,176 +103,6 @@ function resolveDirs(dirs) {
95
103
 
96
104
  // src/compiler/parser.ts
97
105
  import { parseHTML } from "linkedom";
98
- function getCommentRanges(html) {
99
- const ranges = [];
100
- const commentRegex = /<!--[\s\S]*?-->/g;
101
- let match;
102
- while ((match = commentRegex.exec(html)) !== null) {
103
- ranges.push([match.index, match.index + match[0].length]);
104
- }
105
- return ranges;
106
- }
107
- function isInsideComment(pos, commentRanges) {
108
- return commentRanges.some(([start, end]) => pos >= start && pos < end);
109
- }
110
- function isInsideForeignNamespace(html, pos) {
111
- const before = html.slice(0, pos);
112
- const svgOpens = (before.match(/<svg[\s>]/gi) || []).length;
113
- const svgCloses = (before.match(/<\/svg\s*>/gi) || []).length;
114
- if (svgOpens > svgCloses) return true;
115
- const mathOpens = (before.match(/<math[\s>]/gi) || []).length;
116
- const mathCloses = (before.match(/<\/math\s*>/gi) || []).length;
117
- return mathOpens > mathCloses;
118
- }
119
- function parse(html) {
120
- html = html.replace(/^\uFEFF/, "");
121
- const commentRanges = getCommentRanges(html);
122
- let buildContent = [];
123
- let clientScripts = [];
124
- let inlineScripts = [];
125
- let blockingScripts = [];
126
- const SCRIPT_REGEX = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
127
- const edits = [];
128
- const scriptsToRemove = [];
129
- const isInHead = (html2, scriptStart) => {
130
- const beforeScript = html2.slice(0, scriptStart);
131
- let headOpen = -1;
132
- const headOpenRe = /<head(?=[\s>])/gi;
133
- let m;
134
- while ((m = headOpenRe.exec(beforeScript)) !== null) headOpen = m.index;
135
- const headClose = beforeScript.lastIndexOf("</head>");
136
- return headOpen > headClose;
137
- };
138
- let match = SCRIPT_REGEX.exec(html);
139
- while (match !== null) {
140
- const start = match.index;
141
- const end = match.index + match[0].length;
142
- const fullTag = match[0];
143
- const attrsMatch = match[1] || "";
144
- const content = match[2] || "";
145
- if (isInsideComment(start, commentRanges)) {
146
- match = SCRIPT_REGEX.exec(html);
147
- continue;
148
- }
149
- if (isInsideForeignNamespace(html, start)) {
150
- match = SCRIPT_REGEX.exec(html);
151
- continue;
152
- }
153
- const { document } = parseHTML(fullTag);
154
- const scriptEl = document.querySelector("script");
155
- if (scriptEl) {
156
- const passData = scriptEl.getAttribute("pass:data") || void 0;
157
- let cleanedAttrs = attrsMatch.replace(/\bis:build\b/g, "").replace(/\bis:inline\b/g, "").replace(/\bis:blocking\b/g, "");
158
- const inHead = isInHead(html, start);
159
- if (!scriptEl.hasAttribute("is:inline") && !inHead) {
160
- cleanedAttrs = cleanedAttrs.replace(/pass:data="[^"]*"/g, "").replace(/pass:data='[^']*'/g, "").replace(/pass:data=\{[^}]*\}/g, "");
161
- }
162
- cleanedAttrs = cleanedAttrs.replace(/\s+/g, " ").trim();
163
- const openingTag = fullTag.slice(0, fullTag.indexOf(">") + 1);
164
- const isPlainDefault = /^<script\s*>$/i.test(openingTag);
165
- if (isPlainDefault) {
166
- scriptsToRemove.push({
167
- start,
168
- end,
169
- type: "client",
170
- content: content.trim(),
171
- attrs: cleanedAttrs,
172
- passDataExpr: passData,
173
- injectInHead: inHead
174
- });
175
- edits.push({ start, end });
176
- } else if (scriptEl.hasAttribute("is:build")) {
177
- scriptsToRemove.push({
178
- start,
179
- end,
180
- type: "build",
181
- content: content.trim(),
182
- attrs: cleanedAttrs
183
- });
184
- edits.push({ start, end });
185
- } else if (scriptEl.hasAttribute("is:inline")) {
186
- inlineScripts.push({
187
- attrs: cleanedAttrs,
188
- content: content.trim(),
189
- passDataExpr: passData
190
- });
191
- edits.push({
192
- start,
193
- end,
194
- newContent: `<script${cleanedAttrs ? " " + cleanedAttrs : ""}>${content.trim()}</script>`
195
- });
196
- } else if (scriptEl.hasAttribute("is:blocking")) {
197
- scriptsToRemove.push({
198
- start,
199
- end,
200
- type: "blocking",
201
- content: content.trim(),
202
- attrs: cleanedAttrs,
203
- passDataExpr: passData
204
- });
205
- edits.push({ start, end });
206
- } else if (scriptEl.hasAttribute("src")) {
207
- const src = scriptEl.getAttribute("src") || "";
208
- const isLocalScript = !src.startsWith("http://") && !src.startsWith("https://");
209
- const hasType = cleanedAttrs.includes("type=");
210
- if (isLocalScript && !hasType) {
211
- const attrsForModule = cleanedAttrs.replace(/\bdefer\s*=\s*["'][^"']*["']/gi, "").replace(/\bdefer\b/gi, "").replace(/\bsrc\s*=\s*["'][^"']*["']/gi, "").replace(/\s+/g, " ").trim();
212
- const newAttrs = attrsForModule ? attrsForModule + ' type="module"' : 'type="module"';
213
- edits.push({
214
- start,
215
- end,
216
- newContent: `<script ${newAttrs} src="${src}"></script>`
217
- });
218
- }
219
- } else if (!scriptEl.hasAttribute("is:inline") && inHead) {
220
- } else {
221
- scriptsToRemove.push({
222
- start,
223
- end,
224
- type: "client",
225
- content: content.trim(),
226
- attrs: cleanedAttrs,
227
- passDataExpr: passData
228
- });
229
- edits.push({ start, end });
230
- }
231
- }
232
- match = SCRIPT_REGEX.exec(html);
233
- }
234
- edits.sort((a, b) => a.start - b.start);
235
- let template = "";
236
- let last = 0;
237
- for (const e of edits) {
238
- template += html.slice(last, e.start);
239
- if (e.newContent !== void 0) template += e.newContent;
240
- last = e.end;
241
- }
242
- template += html.slice(last);
243
- for (const s of scriptsToRemove) {
244
- if (s.type === "build") buildContent.push(s.content);
245
- if (s.type === "client")
246
- clientScripts.push({
247
- attrs: s.attrs,
248
- content: s.content,
249
- passDataExpr: s.passDataExpr,
250
- injectInHead: s.injectInHead
251
- });
252
- if (s.type === "blocking")
253
- blockingScripts.push({
254
- attrs: s.attrs,
255
- content: s.content,
256
- passDataExpr: s.passDataExpr
257
- });
258
- }
259
- const buildScript = buildContent.length > 0 ? { content: buildContent.join("\n") } : null;
260
- return {
261
- buildScript,
262
- clientScripts,
263
- inlineScripts,
264
- blockingScripts,
265
- template: template.trim()
266
- };
267
- }
268
106
 
269
107
  // src/compiler/constants.ts
270
108
  var ATTR_PREFIX = "data-";
@@ -275,17 +113,17 @@ var ATTR_ELSE_IF = "else-if";
275
113
  var ATTR_ELSE = "else";
276
114
  var ATTR_NAME = "name";
277
115
  var ATTR_SLOT = "slot";
116
+ var ATTR_IS_BUILD = "is:build";
278
117
  var ATTR_IS_INLINE = "is:inline";
118
+ var ATTR_IS_BLOCKING = "is:blocking";
279
119
  var ATTR_PASS_DATA = "pass:data";
120
+ var ATTR_SRC = "src";
280
121
  var TAG_SLOT = "slot";
281
122
  var SLOT_NAME_DEFAULT = "default";
282
123
  var EACH_REGEX = /^(\w+)\s+in\s+(.+)$/;
283
- var CURLY_INTERPOLATION_REGEX = /{([\s\S]+?)}/g;
284
124
  var COMPONENT_SUFFIX_REGEX = /-(component|layout)$/;
285
- var IMPORT_REGEX = /((?:^|[\r\n;])\s*)import\s+(?:(\w+)|\{([^}]+)\}|\*\s+as\s+(\w+))\s+from\s+(['"])(.+?)\5/g;
286
125
  var SELF_CLOSING_TAG_REGEX = /<([a-z0-9-]+)([^>]*?)\/>/gi;
287
126
  var SELF_CLOSING_TAIL_REGEX = /\/>$/;
288
- var ALPINE_ATTR_REGEX = /^(x-|[@:.]).*/;
289
127
  var VOID_TAGS = /* @__PURE__ */ new Set([
290
128
  "area",
291
129
  "base",
@@ -303,22 +141,157 @@ var VOID_TAGS = /* @__PURE__ */ new Set([
303
141
  "wbr"
304
142
  ]);
305
143
 
144
+ // src/compiler/parser.ts
145
+ function getAttrsString(element, exclude) {
146
+ const parts = [];
147
+ const attrs = element.attributes;
148
+ if (!attrs) return "";
149
+ const excludeLower = new Set([...exclude].map((s) => s.toLowerCase()));
150
+ for (let i = 0; i < attrs.length; i++) {
151
+ const a = attrs[i];
152
+ if (!a || excludeLower.has(a.name.toLowerCase())) continue;
153
+ const value = a.value;
154
+ const escaped = value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
155
+ parts.push(`${a.name}="${escaped}"`);
156
+ }
157
+ return parts.join(" ").trim();
158
+ }
159
+ function isInForeignNamespace(el) {
160
+ let parent = el.parentElement;
161
+ while (parent) {
162
+ const tag = parent.tagName?.toLowerCase();
163
+ if (tag === "svg" || tag === "math") return true;
164
+ parent = parent.parentElement;
165
+ }
166
+ return false;
167
+ }
168
+ function isInHead(el) {
169
+ let parent = el.parentElement;
170
+ while (parent) {
171
+ if (parent.tagName?.toLowerCase() === "head") return true;
172
+ parent = parent.parentElement;
173
+ }
174
+ return false;
175
+ }
176
+ function collectScriptElements(doc) {
177
+ const scripts = [];
178
+ const walk = (node) => {
179
+ if (node.nodeType === 1) {
180
+ const el = node;
181
+ if (el.tagName?.toLowerCase() === "script") {
182
+ if (!isInForeignNamespace(el)) scripts.push(el);
183
+ return;
184
+ }
185
+ }
186
+ for (let i = 0; i < node.childNodes.length; i++) walk(node.childNodes[i]);
187
+ };
188
+ walk(doc);
189
+ return scripts;
190
+ }
191
+ function parse(html) {
192
+ html = html.replace(/^\uFEFF/, "");
193
+ html = html.replace(SELF_CLOSING_TAG_REGEX, (match, tagName, attrs) => {
194
+ const tag = String(tagName).toLowerCase();
195
+ if (VOID_TAGS.has(tag)) return match;
196
+ return `<${tagName}${attrs}></${tagName}>`;
197
+ });
198
+ const isFullDocument = /<\s*html[\s>]/i.test(html);
199
+ let doc;
200
+ if (isFullDocument) {
201
+ const parsed = parseHTML(html);
202
+ doc = parsed.document;
203
+ } else {
204
+ const parsed = parseHTML(`<html><head></head><body>${html}</body></html>`);
205
+ doc = parsed.document;
206
+ }
207
+ const buildContent = [];
208
+ const clientScripts = [];
209
+ const inlineScripts = [];
210
+ const blockingScripts = [];
211
+ const scriptElements = collectScriptElements(doc);
212
+ const toRemove = [];
213
+ for (const scriptEl of scriptElements) {
214
+ const inHead = isInHead(scriptEl);
215
+ const hasBuild = scriptEl.hasAttribute(ATTR_IS_BUILD);
216
+ const hasInline = scriptEl.hasAttribute(ATTR_IS_INLINE);
217
+ const hasBlocking = scriptEl.hasAttribute(ATTR_IS_BLOCKING);
218
+ const src = scriptEl.getAttribute(ATTR_SRC) ?? "";
219
+ const passData = scriptEl.getAttribute(ATTR_PASS_DATA) ?? void 0;
220
+ const attrsExcludeTaxonomy = /* @__PURE__ */ new Set([ATTR_IS_BUILD, ATTR_IS_INLINE, ATTR_IS_BLOCKING]);
221
+ let cleanedAttrs = getAttrsString(scriptEl, attrsExcludeTaxonomy);
222
+ if (!hasInline && !inHead) {
223
+ cleanedAttrs = getAttrsString(scriptEl, /* @__PURE__ */ new Set([...attrsExcludeTaxonomy, ATTR_PASS_DATA]));
224
+ }
225
+ cleanedAttrs = cleanedAttrs.replace(/\s+/g, " ").trim();
226
+ const content = (scriptEl.textContent ?? "").trim();
227
+ if (hasBuild) {
228
+ buildContent.push(content);
229
+ toRemove.push(scriptEl);
230
+ continue;
231
+ }
232
+ if (hasInline) {
233
+ inlineScripts.push({
234
+ attrs: cleanedAttrs,
235
+ content,
236
+ passDataExpr: passData
237
+ });
238
+ scriptEl.removeAttribute(ATTR_IS_INLINE);
239
+ continue;
240
+ }
241
+ if (hasBlocking) {
242
+ blockingScripts.push({
243
+ attrs: cleanedAttrs,
244
+ content,
245
+ passDataExpr: passData
246
+ });
247
+ toRemove.push(scriptEl);
248
+ continue;
249
+ }
250
+ if (src) {
251
+ const isLocal = !src.startsWith("http://") && !src.startsWith("https://");
252
+ const hasType = scriptEl.hasAttribute("type");
253
+ if (isLocal && !hasType) {
254
+ scriptEl.setAttribute("type", "module");
255
+ scriptEl.removeAttribute("defer");
256
+ }
257
+ continue;
258
+ }
259
+ if (inHead && scriptEl.attributes.length > 0) continue;
260
+ clientScripts.push({
261
+ attrs: cleanedAttrs,
262
+ content,
263
+ passDataExpr: passData,
264
+ injectInHead: inHead
265
+ });
266
+ toRemove.push(scriptEl);
267
+ }
268
+ for (const el of toRemove) el.remove();
269
+ const buildScript = buildContent.length > 0 ? { content: buildContent.join("\n") } : null;
270
+ let template;
271
+ if (isFullDocument) {
272
+ template = doc.documentElement ? doc.documentElement.outerHTML : String(doc);
273
+ } else {
274
+ template = doc.body ? doc.body.innerHTML : "";
275
+ }
276
+ return {
277
+ buildScript,
278
+ clientScripts,
279
+ inlineScripts,
280
+ blockingScripts,
281
+ template: template.trim()
282
+ };
283
+ }
284
+
306
285
  // src/compiler/helpers.ts
307
286
  function compileInterpolation(text) {
308
287
  if (!text) return "";
309
- let compiled = text.replace(/`/g, "\\`");
310
- compiled = compiled.replace(/{([\s\S]+?)}/g, "${$1}");
311
- return compiled;
288
+ const segments = tokenizeCurlyInterpolation(text, { attributeMode: false });
289
+ return compileInterpolationFromSegments(segments);
312
290
  }
313
291
  function compileAttributeInterpolation(text) {
314
292
  if (!text) return "";
315
- const openSentinel = "__AERO_ESC_OPEN__";
316
- const closeSentinel = "__AERO_ESC_CLOSE__";
317
- let compiled = text.replace(/`/g, "\\`");
318
- compiled = compiled.replace(/{{/g, openSentinel).replace(/}}/g, closeSentinel);
319
- compiled = compiled.replace(/{([\s\S]+?)}/g, "${$1}");
320
- compiled = compiled.replace(new RegExp(openSentinel, "g"), "{").replace(new RegExp(closeSentinel, "g"), "}");
321
- return compiled;
293
+ const segments = tokenizeCurlyInterpolation(text, { attributeMode: true });
294
+ return compileInterpolationFromSegments(segments);
322
295
  }
323
296
  function isAttr(name, attr, prefix) {
324
297
  return name === attr || name === prefix + attr;
@@ -346,55 +319,6 @@ function emitSlotsObjectVars(slotsMap) {
346
319
  const entries = Object.entries(slotsMap).map(([k, varName]) => `"${k}": ${varName}`).join(", ");
347
320
  return "{ " + entries + " }";
348
321
  }
349
- function extractGetStaticPaths(script) {
350
- const regex = /export\s+(async\s+)?function\s+getStaticPaths\s*\([^)]*\)\s*\{/;
351
- const match = regex.exec(script);
352
- if (!match) return { fnText: null, remaining: script };
353
- const start = match.index;
354
- const braceStart = start + match[0].length - 1;
355
- let depth = 1;
356
- let i = braceStart + 1;
357
- let inString = null;
358
- let inComment = null;
359
- while (i < script.length && depth > 0) {
360
- const char = script[i];
361
- const next = script[i + 1];
362
- if (inComment) {
363
- if (inComment === "//" && char === "\n") inComment = null;
364
- else if (inComment === "/*" && char === "*" && next === "/") {
365
- inComment = null;
366
- i++;
367
- }
368
- } else if (inString) {
369
- if (char === "\\") {
370
- i++;
371
- } else if (char === inString) {
372
- inString = null;
373
- }
374
- } else {
375
- if (char === "/" && next === "/") {
376
- inComment = "//";
377
- i++;
378
- } else if (char === "/" && next === "*") {
379
- inComment = "/*";
380
- i++;
381
- } else if (char === '"' || char === "'" || char === "`") {
382
- inString = char;
383
- } else if (char === "{") {
384
- depth++;
385
- } else if (char === "}") {
386
- depth--;
387
- }
388
- }
389
- i++;
390
- }
391
- if (depth !== 0) {
392
- return { fnText: null, remaining: script };
393
- }
394
- const fnText = script.slice(start, i);
395
- const remaining = (script.slice(0, start) + script.slice(i)).trim();
396
- return { fnText, remaining };
397
- }
398
322
  function emitRenderFunction(script, body, options = {}) {
399
323
  const {
400
324
  getStaticPathsFn,
@@ -762,11 +686,9 @@ var Lowerer = class {
762
686
  if (attr.name === ATTR_IS_INLINE) {
763
687
  continue;
764
688
  }
765
- let val = escapeBackticks(attr.value);
766
- val = this.resolver.resolveAttrValue(val);
767
- const isAlpine = ALPINE_ATTR_REGEX.test(attr.name);
768
- if (!isAlpine) {
769
- val = val.replace(CURLY_INTERPOLATION_REGEX, "${$1}");
689
+ let val = this.resolver.resolveAttrValue(attr.value ?? "");
690
+ if (!isDirectiveAttr(attr.name)) {
691
+ val = compileAttributeInterpolation(val);
770
692
  }
771
693
  attributes.push(`${attr.name}="${val}"`);
772
694
  }
@@ -1018,21 +940,24 @@ function compile(parsed, options) {
1018
940
  });
1019
941
  const lowerer = new Lowerer(resolver);
1020
942
  let script = parsed.buildScript ? parsed.buildScript.content : "";
1021
- const imports = [];
1022
- script = script.replace(IMPORT_REGEX, (m, prefix, name, names, starName, q, p) => {
1023
- const resolved = resolver.resolveImport(p);
1024
- if (name) {
1025
- imports.push(`const ${name} = (await import(${q}${resolved}${q})).default`);
1026
- } else if (names) {
1027
- imports.push(`const {${names}} = await import(${q}${resolved}${q})`);
1028
- } else if (starName) {
1029
- imports.push(`const ${starName} = await import(${q}${resolved}${q})`);
943
+ const analysis = analyzeBuildScript(script);
944
+ script = analysis.scriptWithoutImportsAndGetStaticPaths;
945
+ const getStaticPathsFn = analysis.getStaticPathsFn;
946
+ const importsLines = [];
947
+ const quote = '"';
948
+ for (const imp of analysis.imports) {
949
+ const resolved = resolver.resolveImport(imp.specifier);
950
+ const modExpr = `await import(${quote}${resolved}${quote})`;
951
+ if (imp.defaultBinding) {
952
+ importsLines.push(`const ${imp.defaultBinding} = (${modExpr}).default`);
953
+ } else if (imp.namedBindings.length > 0) {
954
+ const names = imp.namedBindings.map((b) => b.imported === b.local ? b.local : `${b.imported} as ${b.local}`).join(", ");
955
+ importsLines.push(`const {${names}} = ${modExpr}`);
956
+ } else if (imp.namespaceBinding) {
957
+ importsLines.push(`const ${imp.namespaceBinding} = ${modExpr}`);
1030
958
  }
1031
- return prefix;
1032
- });
1033
- const importsCode = imports.join("\n");
1034
- const { fnText: getStaticPathsFn, remaining: scriptWithoutPaths } = extractGetStaticPaths(script);
1035
- script = scriptWithoutPaths;
959
+ }
960
+ const importsCode = importsLines.join("\n");
1036
961
  const expandedTemplate = parsed.template.replace(
1037
962
  SELF_CLOSING_TAG_REGEX,
1038
963
  (match, tagName, attrs) => {
@@ -1185,17 +1110,10 @@ function registerClientScriptsToMap(parsed, baseName, target) {
1185
1110
  }
1186
1111
  }
1187
1112
  function isDynamicPage(page) {
1188
- return /\[.+?\]/.test(page.pageName);
1113
+ return isDynamicRoutePattern(page.pageName);
1189
1114
  }
1190
1115
  function expandPattern(pattern, params) {
1191
- return pattern.replace(/\[(.+?)\]/g, (_, key) => {
1192
- if (!(key in params)) {
1193
- throw new Error(
1194
- `[aero] getStaticPaths: missing param "${key}" for pattern "${pattern}". Provided params: ${JSON.stringify(params)}`
1195
- );
1196
- }
1197
- return params[key];
1198
- });
1116
+ return expandRoutePattern(pattern, params);
1199
1117
  }
1200
1118
  function walkHtmlFiles(dir) {
1201
1119
  if (!fs.existsSync(dir)) return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerobuilt/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Jamie Wilson",
@@ -40,6 +40,10 @@
40
40
  "./utils/redirects": {
41
41
  "types": "./dist/utils/redirects.d.ts",
42
42
  "default": "./dist/utils/redirects.js"
43
+ },
44
+ "./editor": {
45
+ "types": "./dist/entry-editor.d.ts",
46
+ "default": "./dist/entry-editor.js"
43
47
  }
44
48
  },
45
49
  "dependencies": {
@@ -47,13 +51,20 @@
47
51
  "html-minifier-next": "^5.1.1",
48
52
  "linkedom": "^0.18.12",
49
53
  "nitro": "^3.0.1-alpha.2",
54
+ "oxc-parser": "^0.115.0",
50
55
  "sharp": "^0.34.5",
51
56
  "svgo": "^4.0.0",
52
- "vite-plugin-image-optimizer": "^2.0.3"
57
+ "vite-plugin-image-optimizer": "^2.0.3",
58
+ "@aerobuilt/interpolation": "0.2.3"
53
59
  },
54
60
  "peerDependencies": {
55
61
  "vite": "8.0.0-beta.15"
56
62
  },
63
+ "peerDependenciesMeta": {
64
+ "vite": {
65
+ "optional": true
66
+ }
67
+ },
57
68
  "devDependencies": {
58
69
  "@types/node": "^25.3.0",
59
70
  "tsup": "^8.5.1",
@@ -61,7 +72,7 @@
61
72
  "vitest": "^4.0.18"
62
73
  },
63
74
  "scripts": {
64
- "build": "tsup src/entry-dev.ts src/entry-prod.ts src/types.ts src/vite/index.ts src/utils/redirects.ts src/runtime/index.ts src/runtime/instance.ts --format esm --dts --clean --out-dir dist --external @content/site",
75
+ "build": "tsup src/entry-dev.ts src/entry-prod.ts src/entry-editor.ts src/types.ts src/vite/index.ts src/utils/redirects.ts src/runtime/index.ts src/runtime/instance.ts --format esm --dts --clean --out-dir dist --external @content/site",
65
76
  "typecheck": "tsc --noEmit",
66
77
  "test": "vitest run"
67
78
  }