@aerobuilt/core 0.1.0
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.
- package/README.md +122 -0
- package/dist/chunk-3OZCI7DL.js +238 -0
- package/dist/chunk-5GK7XRII.js +36 -0
- package/dist/chunk-7A3WBPH4.js +97 -0
- package/dist/chunk-F7MXQXLM.js +15 -0
- package/dist/entry-dev.d.ts +34 -0
- package/dist/entry-dev.js +87 -0
- package/dist/entry-prod.d.ts +19 -0
- package/dist/entry-prod.js +19 -0
- package/dist/runtime/index.d.ts +74 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/instance.d.ts +31 -0
- package/dist/runtime/instance.js +10 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.js +0 -0
- package/dist/utils/redirects.d.ts +24 -0
- package/dist/utils/redirects.js +6 -0
- package/dist/vite/index.d.ts +23 -0
- package/dist/vite/index.js +2049 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @aerobuilt/core
|
|
2
|
+
|
|
3
|
+
The core package of the Aero static site generator. It provides the compiler, runtime, and Vite plugin that power Aero’s HTML-first template engine, component system, and build pipeline.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- **Compiler** — Parses Aero HTML templates, extracts script blocks, and compiles templates into async render functions (DOM → IR → JS).
|
|
8
|
+
- **Runtime** — Renders pages and components with context; supports props, slots, globals, and 404 handling.
|
|
9
|
+
- **Vite plugin** — Integrates templates into the Vite build: virtual client modules, HMR, static generation, optional Nitro and image optimization.
|
|
10
|
+
|
|
11
|
+
## Exports
|
|
12
|
+
|
|
13
|
+
| Export | Description |
|
|
14
|
+
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
15
|
+
| `@aerobuilt/core` | Default: shared `aero` instance with `mount()` for the client entry. |
|
|
16
|
+
| `aerobuilt/vite` | `aero()` Vite plugin for build and dev. |
|
|
17
|
+
| `@aerobuilt/core/runtime` | `Aero` class for programmatic rendering. |
|
|
18
|
+
| `@aerobuilt/core/runtime/instance` | Shared `aero` instance and `onUpdate` for HMR. |
|
|
19
|
+
| `@aerobuilt/core/types` | Shared TypeScript types. |
|
|
20
|
+
|
|
21
|
+
## Script taxonomy
|
|
22
|
+
|
|
23
|
+
Script blocks are classified by attributes (see [docs/script-taxonomy.md](https://github.com/jamiewilson/aero/blob/main/docs/script-taxonomy.md) in the repo):
|
|
24
|
+
|
|
25
|
+
| Script type | Attribute | When it runs | Notes |
|
|
26
|
+
| ----------- | ---------------------- | ----------------- | ----------------------------------------------------------------------------------------- |
|
|
27
|
+
| Build | `<script is:build>` | Build time (Node) | One per template; compiles into the render module. Access `aero.props`, globals, imports. |
|
|
28
|
+
| Client | Plain `<script>` | Browser | Bundled as a Vite virtual module; HMR. Use `pass:data` to inject build-time data. |
|
|
29
|
+
| Inline | `<script is:inline>` | Browser | Left in place; not bundled. For critical inline scripts (e.g. theme FOUC prevention). |
|
|
30
|
+
| Blocking | `<script is:blocking>` | Browser | Extracted and emitted in `<head>`. |
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### Template compiler
|
|
35
|
+
|
|
36
|
+
- Parses templates and extracts `<script is:build>`, client (plain `<script>`), `<script is:inline>`, and `<script is:blocking>` blocks.
|
|
37
|
+
- Lowers template DOM to an **IR** (intermediate representation), then emits a single async render function with `{ }` interpolation.
|
|
38
|
+
- Supports components, slots, `each`, `if` / `else-if` / `else`, and `pass:data` on scripts and styles.
|
|
39
|
+
|
|
40
|
+
**Example**
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script is:build>
|
|
44
|
+
import header from '@components/header'
|
|
45
|
+
const { title } = aero.props
|
|
46
|
+
</script>
|
|
47
|
+
<header-component title="{ title }" />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Runtime
|
|
51
|
+
|
|
52
|
+
- **Aero** class: `global()`, `registerPages()`, `render()`, `renderComponent()`.
|
|
53
|
+
- Context includes globals (e.g. from content), props, slots, request, url, params.
|
|
54
|
+
- Resolves pages by name with fallbacks (index, trailing slash, `getStaticPaths`); returns `null` for 404 when no static path matches.
|
|
55
|
+
|
|
56
|
+
**Example**
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { Aero } from '@aerobuilt/core/runtime'
|
|
60
|
+
const aero = new Aero()
|
|
61
|
+
aero.global('site', { title: 'My Site' })
|
|
62
|
+
// … registerPages, then:
|
|
63
|
+
const html = await aero.render('index', { props: { title: 'Home' } })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Vite plugin
|
|
67
|
+
|
|
68
|
+
- **Plugin** from `aerobuilt/vite`: `aero(options?)`. Options: `nitro`, `apiPrefix`, `dirs`, `site` (canonical URL; exposed as `import.meta.env.SITE` and `Aero.site`; when set, generates `dist/sitemap.xml` after build).
|
|
69
|
+
- Sub-plugins: config resolution, virtual client modules (`\0`-prefixed), HTML transform, SSR middleware, HMR.
|
|
70
|
+
- Build: page discovery, static render, optional Nitro build, optional image optimizer (sharp/svgo).
|
|
71
|
+
|
|
72
|
+
**Example**
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { aero } from 'aerobuilt/vite'
|
|
76
|
+
export default {
|
|
77
|
+
plugins: [aero({ nitro: true })],
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Apps typically use `@aerobuilt/config` and `createViteConfig(aeroConfig)`, which wires the Aero plugin for them.
|
|
82
|
+
|
|
83
|
+
### Client entry
|
|
84
|
+
|
|
85
|
+
The default export of `@aerobuilt/core` is the shared `aero` instance with `mount(options?)` attached. Use it as the browser entry (e.g. in your main script). It does not perform an initial render; it attaches to a root element and subscribes to HMR re-renders in dev.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import aero from 'aerobuilt'
|
|
89
|
+
aero.mount({
|
|
90
|
+
target: '#app',
|
|
91
|
+
onRender: el => {
|
|
92
|
+
/* optional */
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Components and layouts
|
|
98
|
+
|
|
99
|
+
- Components: use `-component` suffix in markup; import without suffix (e.g. `@components/header` → `header.html`).
|
|
100
|
+
- Layouts: use `-layout` and `<slot>` (with `name` and optional `slot` attribute).
|
|
101
|
+
- Props: attributes or `props` / `props="{ ... }"`. In build script, read via `aero.props`.
|
|
102
|
+
|
|
103
|
+
### Path aliases and client stack
|
|
104
|
+
|
|
105
|
+
- Path aliases (e.g. `@components/*`, `@layouts/*`, `@content/*`) are resolved from the project tsconfig.
|
|
106
|
+
- Alpine.js and HTMX attributes (e.g. `x-data`, `:disabled`, `hx-post`) are preserved; attributes matching `^(x-|[@:.]).*` are not interpolated.
|
|
107
|
+
|
|
108
|
+
## File structure
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
src/
|
|
112
|
+
compiler/ # parser.ts, codegen.ts, ir.ts, emit.ts, resolver.ts, helpers.ts, constants.ts
|
|
113
|
+
runtime/ # index.ts (Aero), instance.ts, client.ts
|
|
114
|
+
vite/ # index.ts (plugin), build.ts, defaults.ts
|
|
115
|
+
utils/ # aliases.ts, routing.ts
|
|
116
|
+
types.ts
|
|
117
|
+
index.ts # client entry (aero + mount)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
Vitest in `packages/core`: `compiler/__tests__/`, `runtime/__tests__/`, `vite/__tests__/`. Run from repo root: `pnpm test`.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pagePathToKey,
|
|
3
|
+
resolvePageTarget
|
|
4
|
+
} from "./chunk-7A3WBPH4.js";
|
|
5
|
+
|
|
6
|
+
// src/runtime/index.ts
|
|
7
|
+
var Aero = class {
|
|
8
|
+
/** Global values merged into template context (e.g. from content modules). */
|
|
9
|
+
globals = {};
|
|
10
|
+
/** Map from page name (or path) to module. Keys include both canonical name and full path for lookup. */
|
|
11
|
+
pagesMap = {};
|
|
12
|
+
/** Set by client entry when running in the browser; used to attach the app to a DOM root. */
|
|
13
|
+
mount;
|
|
14
|
+
/**
|
|
15
|
+
* Register a global value available in all templates as `name`.
|
|
16
|
+
*
|
|
17
|
+
* @param name - Key used in templates (e.g. `site`).
|
|
18
|
+
* @param value - Any value (object, string, etc.).
|
|
19
|
+
*/
|
|
20
|
+
global(name, value) {
|
|
21
|
+
this.globals[name] = value;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register page/layout modules from a Vite glob (e.g. `import.meta.glob('@pages/**\/*.html')`).
|
|
25
|
+
* Derives a lookup key from each path via pagePathToKey; also stores by full path for resolution.
|
|
26
|
+
*
|
|
27
|
+
* @param pages - Record of resolved path → module (default export is the render function).
|
|
28
|
+
*/
|
|
29
|
+
registerPages(pages) {
|
|
30
|
+
for (const [path, mod] of Object.entries(pages)) {
|
|
31
|
+
const key = pagePathToKey(path);
|
|
32
|
+
this.pagesMap[key] = mod;
|
|
33
|
+
this.pagesMap[path] = mod;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Type guard: true if value looks like an `AeroRenderInput` (has at least one of props, slots, request, url, params, routePath). */
|
|
37
|
+
isRenderInput(value) {
|
|
38
|
+
if (!value || typeof value !== "object") return false;
|
|
39
|
+
return ["props", "slots", "request", "url", "params", "routePath"].some(
|
|
40
|
+
(key) => key in value
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
/** Coerce various call signatures into a single `AeroRenderInput` (e.g. plain object → `{ props }`). */
|
|
44
|
+
normalizeRenderInput(input) {
|
|
45
|
+
if (!input) return {};
|
|
46
|
+
if (this.isRenderInput(input)) return input;
|
|
47
|
+
if (typeof input === "object") return { props: input };
|
|
48
|
+
return { props: {} };
|
|
49
|
+
}
|
|
50
|
+
/** Convert a page name to a route path (e.g. `index` → `'/'`, `about` → `'/about'`). */
|
|
51
|
+
toRoutePath(pageName = "index") {
|
|
52
|
+
if (!pageName || pageName === "index" || pageName === "home") return "/";
|
|
53
|
+
if (pageName.endsWith("/index")) {
|
|
54
|
+
return "/" + pageName.slice(0, -"/index".length);
|
|
55
|
+
}
|
|
56
|
+
return pageName.startsWith("/") ? pageName : "/" + pageName;
|
|
57
|
+
}
|
|
58
|
+
/** Build a URL from route path and optional raw URL. Uses `http://localhost` as base when only a path is given. */
|
|
59
|
+
toURL(routePath, rawUrl) {
|
|
60
|
+
if (rawUrl instanceof URL) return rawUrl;
|
|
61
|
+
if (typeof rawUrl === "string" && rawUrl.length > 0) {
|
|
62
|
+
return new URL(rawUrl, "http://localhost");
|
|
63
|
+
}
|
|
64
|
+
return new URL(routePath, "http://localhost");
|
|
65
|
+
}
|
|
66
|
+
/** Build template context: globals, props, slots, request, url, params, site, and `renderComponent` / `nextPassDataId`. */
|
|
67
|
+
createContext(input) {
|
|
68
|
+
const routePath = input.routePath || "/";
|
|
69
|
+
const url = this.toURL(routePath, input.url);
|
|
70
|
+
const request = input.request || new Request(url.toString(), { method: "GET" });
|
|
71
|
+
let _passDataId = 0;
|
|
72
|
+
const context = {
|
|
73
|
+
...this.globals,
|
|
74
|
+
props: input.props || {},
|
|
75
|
+
slots: input.slots || {},
|
|
76
|
+
request,
|
|
77
|
+
url,
|
|
78
|
+
params: input.params || {},
|
|
79
|
+
site: input.site ?? "",
|
|
80
|
+
styles: input.styles,
|
|
81
|
+
scripts: input.scripts,
|
|
82
|
+
headScripts: input.headScripts,
|
|
83
|
+
nextPassDataId: () => `__aero_${_passDataId++}`,
|
|
84
|
+
renderComponent: this.renderComponent.bind(this)
|
|
85
|
+
};
|
|
86
|
+
return context;
|
|
87
|
+
}
|
|
88
|
+
/** True if entry params and request params have the same keys and stringified values. */
|
|
89
|
+
paramsMatch(entryParams, requestParams) {
|
|
90
|
+
const entryKeys = Object.keys(entryParams);
|
|
91
|
+
if (entryKeys.length !== Object.keys(requestParams).length) return false;
|
|
92
|
+
for (const key of entryKeys) {
|
|
93
|
+
if (String(entryParams[key]) !== String(requestParams[key])) return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Render a page or layout to HTML.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Resolves `component` (page name string or module) via `pagesMap`, with fallbacks: directory index
|
|
102
|
+
* (`foo` → `foo/index`), `index` → `home`, dynamic routes, and trailing-slash stripping. If the module
|
|
103
|
+
* exports `getStaticPaths` and no props are provided, finds the matching static path and uses its props.
|
|
104
|
+
* For root-level renders, injects accumulated styles and scripts into the document and fixes content
|
|
105
|
+
* that ends up after `</html>` when using layouts (moves it into `</body>`).
|
|
106
|
+
*
|
|
107
|
+
* @param component - Page name (e.g. `'index'`, `'about'`) or the module object.
|
|
108
|
+
* @param input - Render input (props, request, url, params, etc.). Can be a plain object (treated as props).
|
|
109
|
+
* @returns HTML string, or `null` if the page is not found or no static path match.
|
|
110
|
+
*/
|
|
111
|
+
async render(component, input = {}) {
|
|
112
|
+
const renderInput = this.normalizeRenderInput(input);
|
|
113
|
+
const isRootRender = !renderInput.styles;
|
|
114
|
+
if (isRootRender) {
|
|
115
|
+
renderInput.styles = /* @__PURE__ */ new Set();
|
|
116
|
+
renderInput.scripts = /* @__PURE__ */ new Set();
|
|
117
|
+
renderInput.headScripts = /* @__PURE__ */ new Set();
|
|
118
|
+
}
|
|
119
|
+
const resolved = resolvePageTarget(component, this.pagesMap);
|
|
120
|
+
if (!resolved) return null;
|
|
121
|
+
let target = resolved.module;
|
|
122
|
+
const matchedPageName = resolved.pageName;
|
|
123
|
+
const dynamicParams = resolved.params;
|
|
124
|
+
if (typeof target === "function" && target.length === 0) {
|
|
125
|
+
target = await target();
|
|
126
|
+
}
|
|
127
|
+
if (typeof target.getStaticPaths === "function" && Object.keys(renderInput.props || {}).length === 0) {
|
|
128
|
+
const staticPaths = await target.getStaticPaths();
|
|
129
|
+
const combinedParams = { ...dynamicParams, ...renderInput.params || {} };
|
|
130
|
+
const match = staticPaths.find((entry) => this.paramsMatch(entry.params, combinedParams));
|
|
131
|
+
if (!match) {
|
|
132
|
+
console.warn(
|
|
133
|
+
`[aero] 404: Route params ${JSON.stringify(combinedParams)} not found in getStaticPaths for ${matchedPageName}`
|
|
134
|
+
);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (match.props) {
|
|
138
|
+
renderInput.props = match.props;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const routePath = renderInput.routePath || this.toRoutePath(matchedPageName);
|
|
142
|
+
const context = this.createContext({
|
|
143
|
+
props: renderInput.props || {},
|
|
144
|
+
slots: renderInput.slots || {},
|
|
145
|
+
request: renderInput.request,
|
|
146
|
+
url: renderInput.url,
|
|
147
|
+
params: { ...dynamicParams, ...renderInput.params || {} },
|
|
148
|
+
routePath,
|
|
149
|
+
site: renderInput.site,
|
|
150
|
+
styles: renderInput.styles,
|
|
151
|
+
scripts: renderInput.scripts,
|
|
152
|
+
headScripts: renderInput.headScripts
|
|
153
|
+
});
|
|
154
|
+
let renderFn = target;
|
|
155
|
+
if (target.default) renderFn = target.default;
|
|
156
|
+
if (typeof renderFn === "function") {
|
|
157
|
+
let html = await renderFn(context);
|
|
158
|
+
if (isRootRender) {
|
|
159
|
+
if (html.includes("</html>")) {
|
|
160
|
+
const afterHtml = html.split("</html>")[1]?.trim();
|
|
161
|
+
if (afterHtml && html.includes("</body>")) {
|
|
162
|
+
html = html.split("</html>")[0] + "</html>";
|
|
163
|
+
html = html.replace("</body>", `
|
|
164
|
+
${afterHtml}
|
|
165
|
+
</body>`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let headInjections = "";
|
|
169
|
+
if (context.styles && context.styles.size > 0) {
|
|
170
|
+
headInjections += Array.from(context.styles).join("\n") + "\n";
|
|
171
|
+
}
|
|
172
|
+
if (context.headScripts && context.headScripts.size > 0) {
|
|
173
|
+
headInjections += Array.from(context.headScripts).join("\n") + "\n";
|
|
174
|
+
}
|
|
175
|
+
if (headInjections) {
|
|
176
|
+
if (html.includes("</head>")) {
|
|
177
|
+
html = html.replace("</head>", `
|
|
178
|
+
${headInjections}</head>`);
|
|
179
|
+
} else if (html.includes("<body")) {
|
|
180
|
+
html = html.replace(/(<body[^>]*>)/i, `<head>
|
|
181
|
+
${headInjections}</head>
|
|
182
|
+
$1`);
|
|
183
|
+
} else {
|
|
184
|
+
html = `${headInjections}${html}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (context.scripts && context.scripts.size > 0) {
|
|
188
|
+
const scriptsHtml = Array.from(context.scripts).join("\n");
|
|
189
|
+
if (html.includes("</body>")) {
|
|
190
|
+
html = html.replace("</body>", `
|
|
191
|
+
${scriptsHtml}
|
|
192
|
+
</body>`);
|
|
193
|
+
} else {
|
|
194
|
+
html = `${html}
|
|
195
|
+
${scriptsHtml}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return html;
|
|
200
|
+
}
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Render a child component (layout or component) with the given props and slots.
|
|
205
|
+
* Used by compiled templates via context.renderComponent.
|
|
206
|
+
*
|
|
207
|
+
* @param component - Render function or module with `default` render function.
|
|
208
|
+
* @param props - Props object for the component.
|
|
209
|
+
* @param slots - Named slot content (key → HTML string).
|
|
210
|
+
* @param input - Optional request/url/params for context; `headScripts` is not passed through.
|
|
211
|
+
* @returns HTML string from the component's render function, or empty string if not invokable.
|
|
212
|
+
*/
|
|
213
|
+
async renderComponent(component, props = {}, slots = {}, input = {}) {
|
|
214
|
+
const context = this.createContext({
|
|
215
|
+
props,
|
|
216
|
+
slots,
|
|
217
|
+
request: input.request,
|
|
218
|
+
url: input.url,
|
|
219
|
+
params: input.params,
|
|
220
|
+
routePath: input.routePath || "/",
|
|
221
|
+
site: input.site,
|
|
222
|
+
styles: input.styles,
|
|
223
|
+
scripts: input.scripts,
|
|
224
|
+
headScripts: input.headScripts
|
|
225
|
+
});
|
|
226
|
+
if (typeof component === "function") {
|
|
227
|
+
return await component(context);
|
|
228
|
+
}
|
|
229
|
+
if (component && typeof component.default === "function") {
|
|
230
|
+
return await component.default(context);
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
Aero
|
|
238
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Aero
|
|
3
|
+
} from "./chunk-3OZCI7DL.js";
|
|
4
|
+
|
|
5
|
+
// src/runtime/instance.ts
|
|
6
|
+
var instance = globalThis.__AERO_INSTANCE__ || new Aero();
|
|
7
|
+
var listeners = globalThis.__AERO_LISTENERS__ || /* @__PURE__ */ new Set();
|
|
8
|
+
var aero = instance;
|
|
9
|
+
var onUpdate = (cb) => {
|
|
10
|
+
listeners.add(cb);
|
|
11
|
+
return () => listeners.delete(cb);
|
|
12
|
+
};
|
|
13
|
+
var notify = () => {
|
|
14
|
+
listeners.forEach((cb) => cb());
|
|
15
|
+
};
|
|
16
|
+
if (!globalThis.__AERO_INSTANCE__) {
|
|
17
|
+
globalThis.__AERO_INSTANCE__ = instance;
|
|
18
|
+
}
|
|
19
|
+
if (!globalThis.__AERO_LISTENERS__) {
|
|
20
|
+
globalThis.__AERO_LISTENERS__ = listeners;
|
|
21
|
+
}
|
|
22
|
+
var components = import.meta.glob("@components/**/*.html", { eager: true });
|
|
23
|
+
var layouts = import.meta.glob("@layouts/*.html", { eager: true });
|
|
24
|
+
var pages = import.meta.glob("@pages/**/*.html", { eager: true });
|
|
25
|
+
aero.registerPages(components);
|
|
26
|
+
aero.registerPages(layouts);
|
|
27
|
+
aero.registerPages(pages);
|
|
28
|
+
notify();
|
|
29
|
+
if (import.meta.hot) {
|
|
30
|
+
import.meta.hot.accept();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
aero,
|
|
35
|
+
onUpdate
|
|
36
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/utils/path.ts
|
|
2
|
+
function toPosix(value) {
|
|
3
|
+
return value.replace(/\\/g, "/");
|
|
4
|
+
}
|
|
5
|
+
function toPosixRelative(value, root) {
|
|
6
|
+
const valuePosix = toPosix(value);
|
|
7
|
+
const rootPosix = toPosix(root);
|
|
8
|
+
if (valuePosix.startsWith(rootPosix + "/")) {
|
|
9
|
+
return valuePosix.slice(rootPosix.length + 1);
|
|
10
|
+
}
|
|
11
|
+
return valuePosix;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/utils/routing.ts
|
|
15
|
+
function pagePathToKey(path) {
|
|
16
|
+
const withoutExt = toPosix(path).replace(/\.html$/i, "");
|
|
17
|
+
if (withoutExt.includes("pages/")) {
|
|
18
|
+
return withoutExt.split("pages/").pop();
|
|
19
|
+
}
|
|
20
|
+
const segments = withoutExt.split("/").filter(Boolean);
|
|
21
|
+
if (segments.length > 1) {
|
|
22
|
+
return segments.join("/");
|
|
23
|
+
}
|
|
24
|
+
return segments.pop() || path;
|
|
25
|
+
}
|
|
26
|
+
function resolvePageName(url) {
|
|
27
|
+
const [pathPart] = url.split("?");
|
|
28
|
+
let clean = pathPart || "/";
|
|
29
|
+
if (clean === "/" || clean === "") return "index";
|
|
30
|
+
if (clean.endsWith("/")) {
|
|
31
|
+
clean = clean + "index";
|
|
32
|
+
}
|
|
33
|
+
clean = clean.replace(/^\//, "");
|
|
34
|
+
clean = clean.replace(/\.html$/, "");
|
|
35
|
+
return clean || "index";
|
|
36
|
+
}
|
|
37
|
+
function resolveDynamicPage(pageName, pagesMap) {
|
|
38
|
+
const requestedSegments = pageName.split("/").filter(Boolean);
|
|
39
|
+
for (const [key, mod] of Object.entries(pagesMap)) {
|
|
40
|
+
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) {
|
|
59
|
+
return { module: mod, pageName: key, params };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function resolvePageTarget(component, pagesMap) {
|
|
65
|
+
if (typeof component !== "string") {
|
|
66
|
+
return component != null ? { module: component, pageName: "index", params: {} } : null;
|
|
67
|
+
}
|
|
68
|
+
const pageName = component;
|
|
69
|
+
let target = pagesMap[pageName];
|
|
70
|
+
if (!target) {
|
|
71
|
+
target = pagesMap[`${pageName}/index`];
|
|
72
|
+
}
|
|
73
|
+
if (!target && pageName === "index") {
|
|
74
|
+
target = pagesMap["home"];
|
|
75
|
+
}
|
|
76
|
+
if (!target) {
|
|
77
|
+
const dynamicMatch = resolveDynamicPage(pageName, pagesMap) ?? resolveDynamicPage(`${pageName}/index`, pagesMap);
|
|
78
|
+
if (dynamicMatch) return dynamicMatch;
|
|
79
|
+
}
|
|
80
|
+
if (!target && pageName.endsWith("/index")) {
|
|
81
|
+
const stripped = pageName.slice(0, -"/index".length);
|
|
82
|
+
target = pagesMap[stripped];
|
|
83
|
+
if (target) return { module: target, pageName: stripped, params: {} };
|
|
84
|
+
const dynamicMatch = resolveDynamicPage(stripped, pagesMap);
|
|
85
|
+
if (dynamicMatch) return dynamicMatch;
|
|
86
|
+
}
|
|
87
|
+
if (!target) return null;
|
|
88
|
+
return { module: target, pageName, params: {} };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
toPosix,
|
|
93
|
+
toPosixRelative,
|
|
94
|
+
pagePathToKey,
|
|
95
|
+
resolvePageName,
|
|
96
|
+
resolvePageTarget
|
|
97
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/utils/redirects.ts
|
|
2
|
+
function redirectsToRouteRules(redirects) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const rule of redirects) {
|
|
5
|
+
const status = rule.status ?? 302;
|
|
6
|
+
out[rule.from] = {
|
|
7
|
+
redirect: status === 307 ? rule.to : { to: rule.to, statusCode: status }
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
redirectsToRouteRules
|
|
15
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Attach the app to a DOM element and optionally set up HMR re-renders.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Does not perform an initial render: we assume the document already has SSR/pre-rendered
|
|
20
|
+
* HTML. Only runs `onRender` if provided, then in dev (Vite HMR) subscribes to template
|
|
21
|
+
* updates and re-renders into the same target.
|
|
22
|
+
*
|
|
23
|
+
* @param options - Mount options. Defaults to `{ target: '#app' }`.
|
|
24
|
+
* @param options.target - CSS selector (e.g. `#app`) or the root `HTMLElement`. Defaults to `#app`.
|
|
25
|
+
* @param options.onRender - Called with the root element after mount and after each HMR re-render.
|
|
26
|
+
* @returns A promise that resolves immediately. Does not wait for any async render (no initial render).
|
|
27
|
+
* @throws When `target` is a string and no matching element is found in the document.
|
|
28
|
+
*/
|
|
29
|
+
declare function mount(options?: MountOptions): Promise<void>;
|
|
30
|
+
declare const _default: Aero & {
|
|
31
|
+
mount: typeof mount;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { _default as default };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aero,
|
|
3
|
+
onUpdate
|
|
4
|
+
} from "./chunk-5GK7XRII.js";
|
|
5
|
+
import "./chunk-3OZCI7DL.js";
|
|
6
|
+
import {
|
|
7
|
+
resolvePageName
|
|
8
|
+
} from "./chunk-7A3WBPH4.js";
|
|
9
|
+
|
|
10
|
+
// src/runtime/client.ts
|
|
11
|
+
function extractDocumentParts(html) {
|
|
12
|
+
const parser = new DOMParser();
|
|
13
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
14
|
+
const head = doc.head?.innerHTML?.trim() || "";
|
|
15
|
+
const body = doc.body?.innerHTML ?? html;
|
|
16
|
+
return { head, body };
|
|
17
|
+
}
|
|
18
|
+
var PERSISTENT_SELECTORS = [
|
|
19
|
+
'script[src*="/@vite/client"]',
|
|
20
|
+
"[data-vite-dev-id]"
|
|
21
|
+
].join(", ");
|
|
22
|
+
function updateHead(headContent) {
|
|
23
|
+
const headEl = document.head;
|
|
24
|
+
const queriedNodes = headEl.querySelectorAll(PERSISTENT_SELECTORS);
|
|
25
|
+
const persistentSet = new Set(Array.from(queriedNodes));
|
|
26
|
+
for (const node of Array.from(headEl.children)) {
|
|
27
|
+
if (persistentSet.has(node)) continue;
|
|
28
|
+
headEl.removeChild(node);
|
|
29
|
+
}
|
|
30
|
+
const parser = new DOMParser();
|
|
31
|
+
const frag = parser.parseFromString(`<head>${headContent}</head>`, "text/html");
|
|
32
|
+
const nodes = Array.from(frag.head?.childNodes || []);
|
|
33
|
+
for (const node of nodes) {
|
|
34
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
35
|
+
const el = node;
|
|
36
|
+
if (el.matches(PERSISTENT_SELECTORS)) {
|
|
37
|
+
const devId = el.getAttribute("data-vite-dev-id");
|
|
38
|
+
if (devId && headEl.querySelector(`[data-vite-dev-id="${devId}"]`)) continue;
|
|
39
|
+
if (el instanceof HTMLScriptElement && el.src && headEl.querySelector(`script[src="${el.src}"]`)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
headEl.appendChild(document.importNode(node, true));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function renderPage(appEl, renderFn) {
|
|
48
|
+
const pageName = resolvePageName(window.location.pathname);
|
|
49
|
+
try {
|
|
50
|
+
const html = await renderFn(pageName);
|
|
51
|
+
const { head, body } = extractDocumentParts(html);
|
|
52
|
+
if (head) updateHead(head);
|
|
53
|
+
appEl.innerHTML = body;
|
|
54
|
+
console.log(`[aero] Rendered: ${pageName}`);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
appEl.innerHTML = `<h1>Error rendering page: ${pageName}</h1><pre>${String(err)}</pre>`;
|
|
57
|
+
console.error("[aero] Render Error:", err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/entry-dev.ts
|
|
62
|
+
var coreRender = aero.render.bind(aero);
|
|
63
|
+
var hmrState = { lastEl: null, unsubscribe: null };
|
|
64
|
+
function mount(options = {}) {
|
|
65
|
+
const { target = "#app", onRender } = options;
|
|
66
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
67
|
+
if (!el) throw new Error("Target element not found: " + target);
|
|
68
|
+
hmrState.lastEl = el;
|
|
69
|
+
if (onRender) onRender(el);
|
|
70
|
+
const done = Promise.resolve();
|
|
71
|
+
if (import.meta.hot && !hmrState.unsubscribe) {
|
|
72
|
+
hmrState.unsubscribe = onUpdate(() => {
|
|
73
|
+
const el2 = hmrState.lastEl;
|
|
74
|
+
if (el2) {
|
|
75
|
+
void renderPage(el2, coreRender).then(() => {
|
|
76
|
+
if (onRender) onRender(el2);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return done;
|
|
82
|
+
}
|
|
83
|
+
aero.mount = mount;
|
|
84
|
+
var entry_dev_default = aero;
|
|
85
|
+
export {
|
|
86
|
+
entry_dev_default as default
|
|
87
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MountOptions } from './types.js';
|
|
2
|
+
import { Aero } from './runtime/index.js';
|
|
3
|
+
import 'vite';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal client entry for static production builds.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Does not import the runtime instance (no import.meta.glob of components/layouts/pages),
|
|
10
|
+
* so the production client bundle stays small: no template chunks, only mount + onRender.
|
|
11
|
+
* Use this when building for production; dev uses the full entry (entry-dev.ts) for HMR.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare function mount(options?: MountOptions): Promise<void>;
|
|
15
|
+
declare const _default: Aero & {
|
|
16
|
+
mount: typeof mount;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { _default as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Aero
|
|
3
|
+
} from "./chunk-3OZCI7DL.js";
|
|
4
|
+
import "./chunk-7A3WBPH4.js";
|
|
5
|
+
|
|
6
|
+
// src/entry-prod.ts
|
|
7
|
+
function mount(options = {}) {
|
|
8
|
+
const { target = "#app", onRender } = options;
|
|
9
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
10
|
+
if (!el) throw new Error("Target element not found: " + target);
|
|
11
|
+
if (onRender) onRender(el);
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
var aero = new Aero();
|
|
15
|
+
aero.mount = mount;
|
|
16
|
+
var entry_prod_default = aero;
|
|
17
|
+
export {
|
|
18
|
+
entry_prod_default as default
|
|
19
|
+
};
|