@apex-stack/core 0.1.9 → 0.1.11

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.
@@ -0,0 +1,235 @@
1
+ import {
2
+ loadComponents
3
+ } from "./chunk-4VG3CZ6H.js";
4
+ import {
5
+ renderIslandsPage,
6
+ renderPage,
7
+ scanPages
8
+ } from "./chunk-PAMD24NK.js";
9
+
10
+ // src/commands/build.ts
11
+ import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "fs";
12
+ import { dirname, join as join3, resolve } from "path";
13
+ import { apex as apex3 } from "@apex-stack/vite";
14
+ import { defineCommand } from "citty";
15
+ import { createServer as createViteServer } from "vite";
16
+
17
+ // src/build/buildClient.ts
18
+ import { readFileSync } from "fs";
19
+ import { join } from "path";
20
+ import { apex } from "@apex-stack/vite";
21
+ import { build } from "vite";
22
+ var VIRT = "virtual:apex-client:";
23
+ function entryName(pageId) {
24
+ return pageId.replace(/^\/pages\//, "").replace(/\.alpine$/, "").replace(/[^a-zA-Z0-9]+/g, "_");
25
+ }
26
+ async function buildClient(root, routes, outDir) {
27
+ const input = {};
28
+ for (const r of routes) input[entryName(r.pageId)] = `${VIRT}${r.pageId}`;
29
+ const entryPlugin = {
30
+ name: "apex:client-entries",
31
+ resolveId(id) {
32
+ if (id.startsWith(VIRT)) return `\0${id}`;
33
+ },
34
+ load(id) {
35
+ if (id.startsWith(`\0${VIRT}`)) {
36
+ const pageId = id.slice(`\0${VIRT}`.length);
37
+ return [
38
+ `import Alpine from 'alpinejs'`,
39
+ `import ${JSON.stringify(pageId)}`,
40
+ `window.Alpine = Alpine`,
41
+ `Alpine.start()`
42
+ ].join("\n");
43
+ }
44
+ }
45
+ };
46
+ await build({
47
+ root,
48
+ logLevel: "warn",
49
+ plugins: [apex({ clientRuntime: "@apex-stack/core/client" }), entryPlugin],
50
+ build: {
51
+ outDir,
52
+ emptyOutDir: false,
53
+ manifest: true,
54
+ rollupOptions: { input }
55
+ }
56
+ });
57
+ const manifest = JSON.parse(readFileSync(join(outDir, ".vite", "manifest.json"), "utf8"));
58
+ const hrefs = /* @__PURE__ */ new Map();
59
+ for (const r of routes) {
60
+ const virt = `${VIRT}${r.pageId}`;
61
+ const entry = Object.values(manifest).find(
62
+ (m) => m.isEntry && (m.src === virt || m.src === `\0${virt}`)
63
+ );
64
+ if (entry) hrefs.set(r.pageId, `/${entry.file}`);
65
+ }
66
+ return hrefs;
67
+ }
68
+
69
+ // src/build/buildServer.ts
70
+ import { existsSync, readdirSync } from "fs";
71
+ import { isAbsolute, join as join2 } from "path";
72
+ import { apex as apex2 } from "@apex-stack/vite";
73
+ import { build as build2 } from "vite";
74
+ async function buildServer(root, routes, outDir) {
75
+ const ids = routes.map((r) => r.pageId);
76
+ const compDir = join2(root, "components");
77
+ if (existsSync(compDir)) {
78
+ for (const f of readdirSync(compDir).filter((f2) => f2.endsWith(".alpine"))) {
79
+ ids.push(`/components/${f}`);
80
+ }
81
+ }
82
+ const apiDir = join2(root, "server", "api");
83
+ if (existsSync(apiDir)) {
84
+ for (const f of readdirSync(apiDir).filter((f2) => /\.(ts|js|mjs)$/.test(f2))) {
85
+ ids.push(`/server/api/${f}`);
86
+ }
87
+ }
88
+ const input = {};
89
+ for (const id of ids) input[entryName2(id)] = join2(root, id.slice(1));
90
+ const result = await build2({
91
+ root,
92
+ logLevel: "warn",
93
+ plugins: [apex2({ clientRuntime: "@apex-stack/core/client" })],
94
+ build: {
95
+ ssr: true,
96
+ target: "esnext",
97
+ // Node target — allow top-level await in server modules
98
+ outDir: join2(outDir, "server"),
99
+ emptyOutDir: false,
100
+ rollupOptions: {
101
+ input,
102
+ // Externalize every package import (bare specifier) — deps are resolved at
103
+ // runtime from node_modules. Only the app's own relative/absolute files are
104
+ // bundled. This keeps native/workspace deps (@libsql/client, drizzle, …) out.
105
+ external: (id) => !id.startsWith(".") && !isAbsolute(id),
106
+ output: { format: "esm", entryFileNames: "[name].mjs" }
107
+ }
108
+ }
109
+ });
110
+ const byFacade = /* @__PURE__ */ new Map();
111
+ for (const chunk of result.output) {
112
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
113
+ byFacade.set(chunk.facadeModuleId, chunk.fileName);
114
+ }
115
+ }
116
+ const modules = {};
117
+ for (const id of ids) {
118
+ const abs = join2(root, id.slice(1));
119
+ const file = byFacade.get(abs);
120
+ if (file) modules[id] = file;
121
+ }
122
+ return { modules };
123
+ }
124
+ function entryName2(id) {
125
+ return id.replace(/^\//, "").replace(/\.(alpine|ts|js|mjs)$/, "").replace(/[^a-zA-Z0-9]+/g, "_");
126
+ }
127
+
128
+ // src/commands/build.ts
129
+ function outFile(pattern) {
130
+ const clean = pattern.replace(/^\//, "");
131
+ return clean === "" ? "index.html" : `${clean}/index.html`;
132
+ }
133
+ var buildCommand = defineCommand({
134
+ meta: { name: "build", description: "Prerender pages to deployable HTML + client bundles" },
135
+ args: {
136
+ root: { type: "positional", required: false, description: "Project root", default: "." },
137
+ outDir: { type: "string", description: "Output directory", default: "dist" },
138
+ islands: { type: "boolean", description: "Static-first islands mode (zero-JS static)", default: false },
139
+ server: { type: "boolean", description: "Build a Node server (dynamic routes + API/MCP)", default: false }
140
+ },
141
+ async run({ args }) {
142
+ const root = resolve(process.cwd(), args.root);
143
+ const outDir = resolve(root, args.outDir);
144
+ rmSync(outDir, { recursive: true, force: true });
145
+ const routes = scanPages(root);
146
+ const staticRoutes = routes.filter((r) => !r.isDynamic);
147
+ const dynamic = routes.filter((r) => r.isDynamic);
148
+ if (args.server) {
149
+ return buildServerTarget(root, outDir, args.outDir, routes);
150
+ }
151
+ const hrefs = args.islands ? /* @__PURE__ */ new Map() : await buildClient(root, staticRoutes, outDir);
152
+ const vite = await createViteServer({
153
+ root,
154
+ appType: "custom",
155
+ server: { middlewareMode: true },
156
+ plugins: [apex3({ clientRuntime: "@apex-stack/core/client" })]
157
+ });
158
+ try {
159
+ const { registry, css: componentCss } = await loadComponents(
160
+ root,
161
+ (id) => vite.ssrLoadModule(id)
162
+ );
163
+ for (const route of staticRoutes) {
164
+ const common = {
165
+ loadModule: (id) => vite.ssrLoadModule(id),
166
+ pageId: route.pageId,
167
+ url: route.pattern,
168
+ registry,
169
+ componentCss
170
+ };
171
+ const html = args.islands ? await renderIslandsPage(common) : await renderPage({ ...common, clientHref: hrefs.get(route.pageId) });
172
+ const dest = join3(outDir, outFile(route.pattern));
173
+ mkdirSync(dirname(dest), { recursive: true });
174
+ writeFileSync(dest, html);
175
+ console.log(` \u2713 ${route.pattern} \u2192 ${outFile(route.pattern)}`);
176
+ }
177
+ const pub = join3(root, "public");
178
+ if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
179
+ console.log(
180
+ `
181
+ Built ${staticRoutes.length} page(s) \u2192 ${args.outDir}/` + (args.islands ? " (islands / static-first)" : " (prerendered + hydrated)")
182
+ );
183
+ if (dynamic.length) {
184
+ console.log(
185
+ ` Skipped ${dynamic.length} dynamic route(s): ${dynamic.map((r) => r.pattern).join(", ")} (server target on the roadmap).`
186
+ );
187
+ }
188
+ console.log();
189
+ } finally {
190
+ await vite.close();
191
+ }
192
+ }
193
+ });
194
+ async function buildServerTarget(root, outDir, outLabel, routes) {
195
+ const clientHrefs = await buildClient(root, routes, outDir);
196
+ const server = await buildServer(root, routes, outDir);
197
+ const components = {};
198
+ const compDir = join3(root, "components");
199
+ if (existsSync2(compDir)) {
200
+ for (const f of readdirSync2(compDir).filter((f2) => f2.endsWith(".alpine"))) {
201
+ const sf = server.modules[`/components/${f}`];
202
+ if (sf) components[f.replace(/\.alpine$/, "")] = sf;
203
+ }
204
+ }
205
+ const api = [];
206
+ const apiDir = join3(root, "server", "api");
207
+ if (existsSync2(apiDir)) {
208
+ for (const f of readdirSync2(apiDir).filter((f2) => /\.(ts|js|mjs)$/.test(f2))) {
209
+ const sf = server.modules[`/server/api/${f}`];
210
+ if (sf) api.push({ name: f.replace(/\.(ts|js|mjs)$/, ""), serverFile: sf });
211
+ }
212
+ }
213
+ const manifest = {
214
+ islands: false,
215
+ routes: routes.map((r) => ({
216
+ ...r,
217
+ serverFile: server.modules[r.pageId],
218
+ clientHref: clientHrefs.get(r.pageId)
219
+ })),
220
+ components,
221
+ api
222
+ };
223
+ writeFileSync(join3(outDir, "apex-manifest.json"), JSON.stringify(manifest, null, 2));
224
+ const pub = join3(root, "public");
225
+ if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
226
+ console.log(
227
+ `
228
+ Built server target \u2192 ${outLabel}/ (${routes.length} route(s), ${api.length} API module(s))
229
+ Run it: apex start
230
+ `
231
+ );
232
+ }
233
+ export {
234
+ buildCommand
235
+ };
@@ -0,0 +1,93 @@
1
+ // src/ui.ts
2
+ var TTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
3
+ var RESET = "\x1B[0m";
4
+ function truecolor(r, g, b, s) {
5
+ return TTY ? `\x1B[38;2;${r};${g};${b}m${s}${RESET}` : s;
6
+ }
7
+ function style(code, s) {
8
+ return TTY ? `\x1B[${code}m${s}${RESET}` : s;
9
+ }
10
+ var color = {
11
+ cyan: (s) => truecolor(34, 211, 238, s),
12
+ indigo: (s) => truecolor(129, 140, 248, s),
13
+ green: (s) => truecolor(52, 211, 153, s),
14
+ red: (s) => truecolor(248, 113, 113, s),
15
+ gray: (s) => truecolor(154, 166, 196, s),
16
+ bold: (s) => style("1", s),
17
+ dim: (s) => style("2", s)
18
+ };
19
+ var LOGO = [
20
+ " \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
21
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
22
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D ",
23
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 ",
24
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
25
+ "\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
26
+ ];
27
+ var FROM = [99, 102, 241];
28
+ var TO = [34, 211, 238];
29
+ function banner(subtitle = "The full-stack, AI-native meta-framework for Alpine.js") {
30
+ const width = LOGO[0].length;
31
+ const rows = LOGO.map((line) => {
32
+ if (!TTY) return ` ${line}`;
33
+ let out = " ";
34
+ for (let i = 0; i < line.length; i++) {
35
+ const t = width > 1 ? i / (width - 1) : 0;
36
+ const r = Math.round(FROM[0] + (TO[0] - FROM[0]) * t);
37
+ const g = Math.round(FROM[1] + (TO[1] - FROM[1]) * t);
38
+ const b = Math.round(FROM[2] + (TO[2] - FROM[2]) * t);
39
+ out += `\x1B[38;2;${r};${g};${b}m${line[i]}`;
40
+ }
41
+ return out + RESET;
42
+ });
43
+ return `
44
+ ${rows.join("\n")}
45
+ ${color.gray(subtitle)}
46
+ `;
47
+ }
48
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
49
+ function spinner(text) {
50
+ if (!TTY) {
51
+ process.stdout.write(` ${text}
52
+ `);
53
+ return {
54
+ succeed: (t) => console.log(` ${color.green("\u2713")} ${t || text}`),
55
+ fail: (t) => console.log(` ${color.red("\u2717")} ${t || text}`),
56
+ stop: () => {
57
+ }
58
+ };
59
+ }
60
+ let i = 0;
61
+ process.stdout.write("\x1B[?25l");
62
+ const id = setInterval(() => {
63
+ process.stdout.write(`\r\x1B[2K ${color.cyan(FRAMES[i++ % FRAMES.length])} ${text}`);
64
+ }, 80);
65
+ const end = (symbol, t) => {
66
+ clearInterval(id);
67
+ process.stdout.write(`\r\x1B[2K ${symbol} ${t || text}
68
+ \x1B[?25h`);
69
+ };
70
+ return {
71
+ succeed: (t) => end(color.green("\u2713"), t),
72
+ fail: (t) => end(color.red("\u2717"), t),
73
+ stop: () => {
74
+ clearInterval(id);
75
+ process.stdout.write("\r\x1B[2K\x1B[?25h");
76
+ }
77
+ };
78
+ }
79
+ function ready(rows) {
80
+ const w = Math.max(...rows.map(([l]) => l.length));
81
+ console.log();
82
+ for (const [label, value] of rows) {
83
+ console.log(` ${color.cyan("\u279C")} ${color.bold(label.padEnd(w))} ${color.cyan(value)}`);
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ export {
89
+ color,
90
+ banner,
91
+ spinner,
92
+ ready
93
+ };
@@ -0,0 +1,21 @@
1
+ // src/components/registry.ts
2
+ import { existsSync, readdirSync } from "fs";
3
+ import { join } from "path";
4
+ async function loadComponents(root, loadModule) {
5
+ const dir = join(root, "components");
6
+ if (!existsSync(dir)) return { registry: {}, css: "" };
7
+ const registry = {};
8
+ let css = "";
9
+ for (const file of readdirSync(dir).filter((f) => f.endsWith(".alpine"))) {
10
+ const name = file.replace(/\.alpine$/, "");
11
+ const mod = await loadModule(`/components/${file}`);
12
+ registry[name] = { template: mod.template, rootXData: mod.rootXData, scopeId: mod.scopeId };
13
+ if (mod.css) css += `${mod.css}
14
+ `;
15
+ }
16
+ return { registry, css };
17
+ }
18
+
19
+ export {
20
+ loadComponents
21
+ };
@@ -0,0 +1,144 @@
1
+ // src/api/resource.ts
2
+ function isApexResource(x) {
3
+ return typeof x === "object" && x !== null && x.__apexResource === true;
4
+ }
5
+
6
+ // src/api/routes.ts
7
+ import { existsSync, readdirSync } from "fs";
8
+ import { join } from "path";
9
+ import {
10
+ defineEventHandler,
11
+ getQuery,
12
+ getRequestURL,
13
+ readBody,
14
+ setResponseHeader,
15
+ setResponseStatus
16
+ } from "h3";
17
+ import { z } from "zod";
18
+ function toSegments(pattern) {
19
+ return pattern.split("/").filter(Boolean).map((p) => p.startsWith(":") ? { param: p.slice(1) } : { literal: p });
20
+ }
21
+ function sanitizeName(name) {
22
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
23
+ }
24
+ function entryFor(pattern, method, mcpName, route) {
25
+ return { pattern, segments: toSegments(pattern), method, mcpName, route };
26
+ }
27
+ function expandApiModule(name, def) {
28
+ if (!def) return [];
29
+ if (isApexResource(def)) {
30
+ return def.routes.map(
31
+ (r) => entryFor(`/api/${def.name}${r.pathSuffix}`, r.route.method, r.mcpName, r.route)
32
+ );
33
+ }
34
+ if (typeof def.handler === "function") {
35
+ return [entryFor(`/api/${name}`, def.method, sanitizeName(name), def)];
36
+ }
37
+ return [];
38
+ }
39
+ async function loadApiRoutes(root, loadModule) {
40
+ const dir = join(root, "server", "api");
41
+ if (!existsSync(dir)) return [];
42
+ const entries = [];
43
+ for (const file of readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f))) {
44
+ const name = file.replace(/\.(ts|js|mjs)$/, "");
45
+ const def = (await loadModule(`/server/api/${file}`)).default;
46
+ entries.push(...expandApiModule(name, def));
47
+ }
48
+ return entries;
49
+ }
50
+ function matchApi(entries, path, method) {
51
+ const segs = (path.split("?")[0] ?? "/").split("/").filter(Boolean);
52
+ for (const entry of entries) {
53
+ if (entry.method !== method) continue;
54
+ if (entry.segments.length !== segs.length) continue;
55
+ const params = {};
56
+ let ok = true;
57
+ for (let i = 0; i < entry.segments.length; i++) {
58
+ const s = entry.segments[i];
59
+ const v = segs[i];
60
+ if (s.param) params[s.param] = decodeURIComponent(v);
61
+ else if (s.literal !== v) {
62
+ ok = false;
63
+ break;
64
+ }
65
+ }
66
+ if (ok) return { entry, params };
67
+ }
68
+ return null;
69
+ }
70
+ function createApiHandler(entries) {
71
+ return defineEventHandler(async (event) => {
72
+ const url = getRequestURL(event);
73
+ const matched = matchApi(entries, url.pathname, event.method);
74
+ if (!matched) {
75
+ setResponseStatus(event, 404);
76
+ return { error: `No API route for ${event.method} ${url.pathname}` };
77
+ }
78
+ const { entry, params } = matched;
79
+ const raw = {
80
+ ...entry.method === "GET" ? getQuery(event) : await readBody(event) ?? {},
81
+ ...params
82
+ };
83
+ let input = raw;
84
+ if (entry.route.inputShape) {
85
+ const parsed = z.object(entry.route.inputShape).safeParse(raw);
86
+ if (!parsed.success) {
87
+ setResponseStatus(event, 400);
88
+ return { error: "Invalid input", issues: parsed.error.issues };
89
+ }
90
+ input = parsed.data;
91
+ }
92
+ const result = await entry.route.handler({ input, url: url.toString() });
93
+ setResponseHeader(event, "Content-Type", "application/json");
94
+ return result;
95
+ });
96
+ }
97
+
98
+ // src/mcp/server.ts
99
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
100
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
101
+ import { defineEventHandler as defineEventHandler2, toWebRequest } from "h3";
102
+ function hasMcpRoutes(entries) {
103
+ return entries.some((e) => e.route.mcp);
104
+ }
105
+ function buildServer(entries) {
106
+ const server = new McpServer({ name: "apexjs", version: "0.0.0" });
107
+ for (const entry of entries) {
108
+ server.registerTool(
109
+ entry.mcpName,
110
+ {
111
+ description: entry.route.description ?? `Apex route ${entry.mcpName}`,
112
+ inputSchema: entry.route.inputShape ?? {}
113
+ },
114
+ async (args) => {
115
+ const result = await entry.route.handler({ input: args ?? {}, url: `mcp://${entry.mcpName}` });
116
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
117
+ }
118
+ );
119
+ }
120
+ return server;
121
+ }
122
+ function createMcpHandler(entries) {
123
+ const mcpEntries = entries.filter((e) => e.route.mcp);
124
+ return defineEventHandler2(async (event) => {
125
+ const server = buildServer(mcpEntries);
126
+ const transport = new WebStandardStreamableHTTPServerTransport({
127
+ sessionIdGenerator: void 0,
128
+ enableJsonResponse: true
129
+ });
130
+ await server.connect(transport);
131
+ const response = await transport.handleRequest(toWebRequest(event));
132
+ void server.close();
133
+ return response;
134
+ });
135
+ }
136
+
137
+ export {
138
+ isApexResource,
139
+ expandApiModule,
140
+ loadApiRoutes,
141
+ createApiHandler,
142
+ hasMcpRoutes,
143
+ createMcpHandler
144
+ };
@@ -0,0 +1,171 @@
1
+ // src/islands/render.ts
2
+ import { renderIslands } from "@apex-stack/kit";
3
+ var ISLAND_LOADER = (
4
+ /* js */
5
+ `
6
+ let __alpine
7
+ function __ensureAlpine() {
8
+ return __alpine ??= import('alpinejs').then(function (m) {
9
+ const Alpine = m.default
10
+ window.Alpine = Alpine
11
+ Alpine.start() // islands are x-ignore'd, so this hydrates nothing on its own
12
+ return Alpine
13
+ })
14
+ }
15
+ async function __hydrate(el) {
16
+ const Alpine = await __ensureAlpine()
17
+ // Global Alpine.start() marked this island with the internal _x_ignore
18
+ // property (from the x-ignore attribute). Clear BOTH so initTree will descend
19
+ // and initialize the island's own x-data instead of early-returning.
20
+ el.removeAttribute('x-ignore')
21
+ delete el._x_ignore
22
+ Alpine.initTree(el)
23
+ el.setAttribute('data-apex-hydrated', '')
24
+ }
25
+ document.querySelectorAll('[data-apex-island]').forEach(function (el) {
26
+ const mode = el.getAttribute('data-apex-client')
27
+ if (mode === 'load') {
28
+ __hydrate(el)
29
+ } else if (mode === 'idle') {
30
+ (window.requestIdleCallback || function (cb) { return setTimeout(cb, 200) })(function () { __hydrate(el) })
31
+ } else if (mode === 'visible') {
32
+ const io = new IntersectionObserver(function (entries, obs) {
33
+ entries.forEach(function (e) { if (e.isIntersecting) { obs.unobserve(e.target); __hydrate(e.target) } })
34
+ })
35
+ io.observe(el)
36
+ }
37
+ // 'none' \u2192 do nothing; the SSR HTML is the final, static output.
38
+ })
39
+ `.trim()
40
+ );
41
+ async function renderIslandsPage(opts) {
42
+ const mod = await opts.loadModule(opts.pageId);
43
+ const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
44
+ const { html, hydratingCount } = renderIslands(
45
+ mod.template,
46
+ loaderData,
47
+ mod.scopeId,
48
+ opts.registry
49
+ );
50
+ const loaderScript = hydratingCount > 0 ? `
51
+ <script type="module">${ISLAND_LOADER}</script>` : "";
52
+ const doc = `<!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="utf-8" />
56
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
57
+ <title>Apex JS \u2014 Islands</title>
58
+ <style>${mod.css}${opts.componentCss ?? ""}</style>
59
+ </head>
60
+ <body>
61
+ ${html}${loaderScript}
62
+ </body>
63
+ </html>`;
64
+ return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
65
+ }
66
+
67
+ // src/routing/router.ts
68
+ import { existsSync, readdirSync, statSync } from "fs";
69
+ import { join, relative, sep } from "path";
70
+ function walkAlpine(dir) {
71
+ const out = [];
72
+ for (const entry of readdirSync(dir)) {
73
+ const abs = join(dir, entry);
74
+ if (statSync(abs).isDirectory()) out.push(...walkAlpine(abs));
75
+ else if (entry.endsWith(".alpine")) out.push(abs);
76
+ }
77
+ return out;
78
+ }
79
+ function scanPages(root) {
80
+ const dir = join(root, "pages");
81
+ if (!existsSync(dir)) return [];
82
+ const routes = walkAlpine(dir).map((abs) => {
83
+ const rel = relative(dir, abs).split(sep).join("/");
84
+ const pageId = `/pages/${rel}`;
85
+ const parts = rel.replace(/\.alpine$/, "").split("/");
86
+ if (parts[parts.length - 1] === "index") parts.pop();
87
+ const segments = parts.map((p) => {
88
+ const m = /^\[(.+)\]$/.exec(p);
89
+ return m ? { param: m[1] } : { literal: p };
90
+ });
91
+ const isDynamic = segments.some((s) => s.param !== void 0);
92
+ const pattern = `/${segments.map((s) => s.param ? `:${s.param}` : s.literal).join("/")}`;
93
+ return { pageId, pattern, segments, isDynamic };
94
+ });
95
+ return routes.sort((a, b) => Number(a.isDynamic) - Number(b.isDynamic));
96
+ }
97
+ function pathSegments(url) {
98
+ const path = url.split("?")[0] ?? "/";
99
+ return path.split("/").filter(Boolean);
100
+ }
101
+ function matchRoute(routes, url) {
102
+ const segs = pathSegments(url);
103
+ for (const route of routes) {
104
+ if (route.segments.length !== segs.length) continue;
105
+ const params = {};
106
+ let ok = true;
107
+ for (let i = 0; i < route.segments.length; i++) {
108
+ const rs = route.segments[i];
109
+ const value = segs[i];
110
+ if (rs.param) params[rs.param] = decodeURIComponent(value);
111
+ else if (rs.literal !== value) {
112
+ ok = false;
113
+ break;
114
+ }
115
+ }
116
+ if (ok) return { pageId: route.pageId, params };
117
+ }
118
+ return null;
119
+ }
120
+
121
+ // src/dev/renderPage.ts
122
+ import { renderComponent, stateIsland } from "@apex-stack/kit";
123
+ async function renderPage(opts) {
124
+ const mod = await opts.loadModule(opts.pageId);
125
+ const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
126
+ const { html } = renderComponent({
127
+ template: mod.template,
128
+ rootXData: mod.rootXData,
129
+ componentId: mod.componentId,
130
+ scopeId: mod.scopeId,
131
+ loaderData,
132
+ registry: opts.registry
133
+ });
134
+ const doc = shell({
135
+ body: html,
136
+ island: stateIsland(mod.componentId, loaderData),
137
+ css: mod.css + (opts.componentCss ?? ""),
138
+ pageId: opts.pageId,
139
+ clientHref: opts.clientHref
140
+ });
141
+ return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
142
+ }
143
+ function shell({ body, island, css, pageId, clientHref }) {
144
+ const clientScript = clientHref ? `<script type="module" src="${clientHref}"></script>` : `<script type="module">
145
+ import Alpine from 'alpinejs'
146
+ import ${JSON.stringify(pageId)}
147
+ window.Alpine = Alpine
148
+ Alpine.start()
149
+ </script>`;
150
+ return `<!DOCTYPE html>
151
+ <html lang="en">
152
+ <head>
153
+ <meta charset="utf-8" />
154
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
155
+ <title>Apex JS</title>
156
+ <style>${css}</style>
157
+ </head>
158
+ <body>
159
+ ${body}
160
+ ${island}
161
+ ${clientScript}
162
+ </body>
163
+ </html>`;
164
+ }
165
+
166
+ export {
167
+ renderIslandsPage,
168
+ scanPages,
169
+ matchRoute,
170
+ renderPage
171
+ };