@aerobuilt/core 0.3.0 → 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.
@@ -31,10 +31,14 @@ const notify = () => {
31
31
  };
32
32
  if (!globalThis.__AERO_INSTANCE__) globalThis.__AERO_INSTANCE__ = instance;
33
33
  if (!globalThis.__AERO_LISTENERS__) globalThis.__AERO_LISTENERS__ = listeners;
34
- /** Eager globs so pages, layouts, and components are available synchronously for SSR/build. */
35
- const components = import.meta.glob("@components/**/*.html", { eager: true });
36
- const layouts = import.meta.glob("@layouts/*.html", { eager: true });
37
- const pages = import.meta.glob("@pages/**/*.html", { eager: true });
34
+ /**
35
+ * Eager globs so pages, layouts, and components are available synchronously for SSR/build.
36
+ * Patterns use default client dir (./client/...) so Vite's import-glob accepts them; apps with
37
+ * custom dirs rely on path aliases mapping @components/@layouts/@pages to the same structure.
38
+ */
39
+ const components = /* @__PURE__ */ Object.assign({});
40
+ const layouts = /* @__PURE__ */ Object.assign({});
41
+ const pages = /* @__PURE__ */ Object.assign({});
38
42
  aero.registerPages(components);
39
43
  aero.registerPages(layouts);
40
44
  aero.registerPages(pages);
@@ -648,31 +648,20 @@ var Lowerer = class {
648
648
  }
649
649
  /** Gets the condition value from if/else-if attribute */
650
650
  getCondition(node, attr) {
651
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
651
652
  const plainValue = node.getAttribute(attr);
652
- if (plainValue !== null) return this.requireBracedExpression(plainValue, attr, node);
653
+ if (plainValue !== null) return stripBraces(validateSingleBracedExpression(plainValue, {
654
+ directive: attr,
655
+ tagName
656
+ }));
653
657
  const dataAttr = ATTR_PREFIX + attr;
654
658
  const dataValue = node.getAttribute(dataAttr);
655
- if (dataValue !== null) return this.requireBracedExpression(dataValue, dataAttr, node);
659
+ if (dataValue !== null) return stripBraces(validateSingleBracedExpression(dataValue, {
660
+ directive: dataAttr,
661
+ tagName
662
+ }));
656
663
  return null;
657
664
  }
658
- /**
659
- * Require directive value to be a braced expression; optionally strip outer braces.
660
- *
661
- * @param value - Raw attribute value.
662
- * @param directive - Attribute name for error message (e.g. `each`, `pass:data`).
663
- * @param node - DOM node for error message (tag name).
664
- * @param options - `strip: true` (default) returns inner expression; `strip: false` returns trimmed value including braces (e.g. for pass:data).
665
- * @returns Trimmed value, with or without outer braces per options.
666
- */
667
- requireBracedExpression(value, directive, node, options) {
668
- const strip = options?.strip !== false;
669
- const trimmed = value.trim();
670
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
671
- const tagName = node?.tagName?.toLowerCase?.() || "element";
672
- throw new Error(`Directive \`${directive}\` on <${tagName}> must use a braced expression, e.g. ${directive}="{ expression }".`);
673
- }
674
- return strip ? stripBraces(trimmed) : trimmed;
675
- }
676
665
  isSingleWrappedExpression(value) {
677
666
  const trimmed = value.trim();
678
667
  if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
@@ -702,7 +691,13 @@ var Lowerer = class {
702
691
  if (isAttr(attr.name, ATTR_PROPS, ATTR_PREFIX)) {
703
692
  const value = attr.value?.trim() || "";
704
693
  if (!value) dataPropsExpression = "...props";
705
- else dataPropsExpression = this.requireBracedExpression(value, attr.name, node);
694
+ else {
695
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
696
+ dataPropsExpression = stripBraces(validateSingleBracedExpression(value, {
697
+ directive: attr.name,
698
+ tagName
699
+ }));
700
+ }
706
701
  continue;
707
702
  }
708
703
  const rawValue = attr.value ?? "";
@@ -725,7 +720,11 @@ var Lowerer = class {
725
720
  if (node.attributes) for (let i = 0; i < node.attributes.length; i++) {
726
721
  const attr = node.attributes[i];
727
722
  if (isAttr(attr.name, ATTR_EACH, ATTR_PREFIX)) {
728
- const match = this.requireBracedExpression(attr.value || "", attr.name, node).match(EACH_REGEX);
723
+ const tagName = node?.tagName?.toLowerCase?.() || "element";
724
+ const match = stripBraces(validateSingleBracedExpression(attr.value || "", {
725
+ directive: attr.name,
726
+ tagName
727
+ })).match(EACH_REGEX);
729
728
  if (!match) {
730
729
  const tagName = node?.tagName?.toLowerCase?.() || "element";
731
730
  throw new Error(`Directive \`${attr.name}\` on <${tagName}> must match "{ item in items }".`);
@@ -1493,12 +1492,15 @@ function createBuildConfig(options = {}, root = process.cwd()) {
1493
1492
  const require = createRequire(import.meta.url);
1494
1493
  const AERO_DIR = ".aero";
1495
1494
  const NITRO_CONFIG_FILENAME = "nitro.config.mjs";
1495
+ /** Filename for the generated runtime instance (uses app dirs for globs); written under .aero so Vite treats it as a real module. */
1496
+ const RUNTIME_INSTANCE_FILENAME = "runtime-instance.mjs";
1496
1497
  /**
1497
1498
  * Generate Nitro config from Aero options and write to <projectRoot>/.aero/nitro.config.mjs.
1498
1499
  * root is the app/site directory (Vite config.root), e.g. examples/kitchen-sink or a create-aerobuilt project folder.
1500
+ * distDir is the configured output dir (e.g. 'build') so the catch-all route serves from the same path at preview time.
1499
1501
  * Returns the absolute path to .aero (Nitro cwd so it loads this file).
1500
1502
  */
1501
- function writeGeneratedNitroConfig(root, serverDir, redirects) {
1503
+ function writeGeneratedNitroConfig(root, serverDir, redirects, distDir) {
1502
1504
  const aeroDir = path.join(root, AERO_DIR);
1503
1505
  mkdirSync(aeroDir, { recursive: true });
1504
1506
  const routeRules = redirectsToRouteRules(redirects ?? []);
@@ -1507,7 +1509,8 @@ function writeGeneratedNitroConfig(root, serverDir, redirects) {
1507
1509
  output: { dir: path.join(root, ".output") },
1508
1510
  scanDirs: [path.join(root, serverDir)],
1509
1511
  routeRules,
1510
- noPublicDir: true
1512
+ noPublicDir: true,
1513
+ replace: { "process.env.AERO_DIST": JSON.stringify(distDir) }
1511
1514
  };
1512
1515
  const content = `// Generated by Aero — do not edit
1513
1516
  export default ${JSON.stringify(nitroConfig, null, 2)}
@@ -1540,9 +1543,7 @@ function createAeroConfigPlugin(state) {
1540
1543
  enforce: "pre",
1541
1544
  config(userConfig, env) {
1542
1545
  const root = userConfig.root || process.cwd();
1543
- const rawAliases = loadTsconfigAliases(root);
1544
- state.aliasResult = mergeWithDefaultAliases(rawAliases, root, state.dirs);
1545
- if (state.dirs.client !== DEFAULT_DIRS.client && rawAliases.projectRoot != null) console.warn("[aero] Custom dirs.client is set; ensure tsconfig paths for @pages, @layouts, @components match (e.g. @pages → \"" + state.dirs.client + "/pages\").");
1546
+ state.aliasResult = mergeWithDefaultAliases(loadTsconfigAliases(root), root, state.dirs);
1546
1547
  const site = state.options.site ?? "";
1547
1548
  return {
1548
1549
  base: "./",
@@ -1559,6 +1560,13 @@ function createAeroConfigPlugin(state) {
1559
1560
  },
1560
1561
  configResolved(resolvedConfig) {
1561
1562
  state.config = resolvedConfig;
1563
+ const dir = path.join(resolvedConfig.root, AERO_DIR);
1564
+ mkdirSync(dir, { recursive: true });
1565
+ const filePath = path.join(dir, RUNTIME_INSTANCE_FILENAME);
1566
+ const runtimeIndexPath = path.join(path.dirname(state.runtimeInstancePath), "index.mjs");
1567
+ const runtimeImportPath = path.relative(dir, runtimeIndexPath).replace(/\\/g, "/");
1568
+ writeFileSync(filePath, getRuntimeInstanceVirtualSource(state.dirs.client, runtimeImportPath.startsWith(".") ? runtimeImportPath : "./" + runtimeImportPath), "utf-8");
1569
+ state.generatedRuntimeInstancePath = filePath;
1562
1570
  }
1563
1571
  };
1564
1572
  }
@@ -1570,6 +1578,56 @@ function isAeroTemplateHtml(filePath, root, dirs) {
1570
1578
  const sep = path.sep;
1571
1579
  return rel.startsWith("pages" + sep) || rel.startsWith("components" + sep) || rel.startsWith("layouts" + sep);
1572
1580
  }
1581
+ /**
1582
+ * Prefix for import.meta.glob patterns. In virtual modules Vite requires globs to start with '/'
1583
+ * (absolute from project root). Uses app-configured client dir so custom dirs (e.g. frontend/) resolve correctly.
1584
+ */
1585
+ function clientGlobPrefix(clientDir) {
1586
+ const normalized = clientDir.replace(/\\/g, "/").replace(/^\.\/+/, "");
1587
+ return normalized ? `/${normalized}` : "/client";
1588
+ }
1589
+ /**
1590
+ * Virtual module source for the runtime instance with glob patterns using the app's client dir.
1591
+ * Ensures template resolution works for custom dirs (e.g. dirs.client === 'frontend').
1592
+ * runtimeImportPath: path that resolves to @aerobuilt/core/runtime from the generated file (e.g. relative to .aero/ for SSR).
1593
+ */
1594
+ function getRuntimeInstanceVirtualSource(clientDir, runtimeImportPath = "@aerobuilt/core/runtime") {
1595
+ const prefix = clientGlobPrefix(clientDir);
1596
+ const componentsPattern = `${prefix}/components/**/*.html`;
1597
+ const layoutsPattern = `${prefix}/layouts/*.html`;
1598
+ const pagesPattern = `${prefix}/pages/**/*.html`;
1599
+ return `import { Aero } from ${JSON.stringify(runtimeImportPath)}
1600
+
1601
+ const instance = globalThis.__AERO_INSTANCE__ || new Aero()
1602
+ const listeners = globalThis.__AERO_LISTENERS__ || new Set()
1603
+ const aero = instance
1604
+
1605
+ const onUpdate = (cb) => {
1606
+ listeners.add(cb)
1607
+ return () => listeners.delete(cb)
1608
+ }
1609
+ const notify = () => {
1610
+ listeners.forEach((cb) => cb())
1611
+ }
1612
+
1613
+ if (!globalThis.__AERO_INSTANCE__) globalThis.__AERO_INSTANCE__ = instance
1614
+ if (!globalThis.__AERO_LISTENERS__) globalThis.__AERO_LISTENERS__ = listeners
1615
+
1616
+ const components = import.meta.glob(${JSON.stringify(componentsPattern)}, { eager: true })
1617
+ const layouts = import.meta.glob(${JSON.stringify(layoutsPattern)}, { eager: true })
1618
+ const pages = import.meta.glob(${JSON.stringify(pagesPattern)}, { eager: true })
1619
+
1620
+ aero.registerPages(components)
1621
+ aero.registerPages(layouts)
1622
+ aero.registerPages(pages)
1623
+
1624
+ notify()
1625
+
1626
+ if (import.meta.hot) import.meta.hot.accept()
1627
+
1628
+ export { aero, onUpdate }
1629
+ `;
1630
+ }
1573
1631
  function createAeroVirtualsPlugin(state) {
1574
1632
  return {
1575
1633
  name: "vite-plugin-aero-virtuals",
@@ -1579,7 +1637,7 @@ function createAeroVirtualsPlugin(state) {
1579
1637
  discoverClientScriptContentMap(state.config.root, state.dirs.client).forEach((entry, url) => state.clientScripts.set(url, entry));
1580
1638
  },
1581
1639
  async resolveId(id, importer) {
1582
- if (id === RUNTIME_INSTANCE_MODULE_ID) return RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1640
+ if (id === RUNTIME_INSTANCE_MODULE_ID) return state.generatedRuntimeInstancePath ?? RESOLVED_RUNTIME_INSTANCE_MODULE_ID;
1583
1641
  if (id.startsWith(CLIENT_SCRIPT_PREFIX)) return "\0" + id;
1584
1642
  if (id.startsWith("\0" + CLIENT_SCRIPT_PREFIX)) return id;
1585
1643
  if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) return id;
@@ -1600,7 +1658,7 @@ function createAeroVirtualsPlugin(state) {
1600
1658
  return null;
1601
1659
  },
1602
1660
  load(id) {
1603
- if (id === RESOLVED_RUNTIME_INSTANCE_MODULE_ID) return `export { aero, onUpdate } from ${JSON.stringify(state.runtimeInstancePath)}`;
1661
+ if (id === RESOLVED_RUNTIME_INSTANCE_MODULE_ID) return getRuntimeInstanceVirtualSource(state.dirs.client);
1604
1662
  if (id.startsWith(AERO_EMPTY_INLINE_CSS_PREFIX)) return "/* aero: no inline styles */";
1605
1663
  if (id.startsWith(AERO_HTML_VIRTUAL_PREFIX)) {
1606
1664
  const filePath = id.slice(AERO_HTML_VIRTUAL_PREFIX.length).replace(/\.aero$/i, ".html");
@@ -1796,6 +1854,7 @@ function aero(options = {}) {
1796
1854
  aliasResult: null,
1797
1855
  clientScripts: /* @__PURE__ */ new Map(),
1798
1856
  runtimeInstancePath,
1857
+ generatedRuntimeInstancePath: null,
1799
1858
  dirs,
1800
1859
  apiPrefix,
1801
1860
  options
@@ -1833,7 +1892,7 @@ function aero(options = {}) {
1833
1892
  site: options.site,
1834
1893
  redirects: options.redirects
1835
1894
  }, outDir);
1836
- if (enableNitro) await runNitroBuild(root, writeGeneratedNitroConfig(root, dirs.server, options.redirects));
1895
+ if (enableNitro) await runNitroBuild(root, writeGeneratedNitroConfig(root, dirs.server, options.redirects, dirs.dist));
1837
1896
  }
1838
1897
  },
1839
1898
  ViteImageOptimizer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerobuilt/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Jamie Wilson",
@@ -60,7 +60,7 @@
60
60
  "sharp": "^0.34.5",
61
61
  "svgo": "^4.0.0",
62
62
  "vite-plugin-image-optimizer": "^2.0.3",
63
- "@aerobuilt/interpolation": "0.3.0"
63
+ "@aerobuilt/interpolation": "0.3.1"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "vite": "^8.0.0-0"
@@ -77,7 +77,7 @@
77
77
  "vitest": "^4.0.18"
78
78
  },
79
79
  "scripts": {
80
- "build": "tsdown src/entry-dev.ts src/entry-prod.ts src/entry-editor.ts src/types.ts src/vite/index.ts src/utils/aliases.ts src/utils/redirects.ts src/runtime/index.ts src/runtime/instance.ts --format esm --dts --clean --out-dir dist --external @content/site",
80
+ "build": "tsdown",
81
81
  "typecheck": "tsc --noEmit",
82
82
  "test": "vitest run"
83
83
  }