@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.
- package/dist/build-QRHQUUZC.js +235 -0
- package/dist/chunk-2L2T47AH.js +93 -0
- package/dist/chunk-4VG3CZ6H.js +21 -0
- package/dist/chunk-DSUIB3JH.js +144 -0
- package/dist/chunk-PAMD24NK.js +171 -0
- package/dist/chunk-QZTDKUXK.js +127 -0
- package/dist/cli.js +78 -607
- package/dist/dev-HDRJEPPN.js +38 -0
- package/dist/index.js +8 -3
- package/dist/make-4LINTKZH.js +89 -0
- package/dist/mcp-DL4J6JFJ.js +56 -0
- package/dist/migrate-NOGFOFV2.js +38 -0
- package/dist/server-VOIXBL4I.js +9 -0
- package/dist/start-VJJXI4W3.js +132 -0
- package/package.json +1 -1
- package/dist/chunk-IEXQ7E5C.js +0 -426
|
@@ -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
|
+
};
|