@apex-stack/core 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andre Corugda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,413 @@
1
+ // src/api/resource.ts
2
+ function isApexResource(x) {
3
+ return typeof x === "object" && x !== null && x.__apexResource === true;
4
+ }
5
+
6
+ // src/dev/renderPage.ts
7
+ import { renderComponent, stateIsland } from "@apex-stack/kit";
8
+ async function renderPage(opts) {
9
+ const mod = await opts.loadModule(opts.pageId);
10
+ const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
11
+ const { html } = renderComponent({
12
+ template: mod.template,
13
+ rootXData: mod.rootXData,
14
+ componentId: mod.componentId,
15
+ scopeId: mod.scopeId,
16
+ loaderData,
17
+ registry: opts.registry
18
+ });
19
+ const doc = shell({
20
+ body: html,
21
+ island: stateIsland(mod.componentId, loaderData),
22
+ css: mod.css + (opts.componentCss ?? ""),
23
+ pageId: opts.pageId
24
+ });
25
+ return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
26
+ }
27
+ function shell({ body, island, css, pageId }) {
28
+ return `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8" />
32
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <title>Apex JS</title>
34
+ <style>${css}</style>
35
+ </head>
36
+ <body>
37
+ ${body}
38
+ ${island}
39
+ <script type="module">
40
+ import Alpine from 'alpinejs'
41
+ import ${JSON.stringify(pageId)}
42
+ window.Alpine = Alpine
43
+ Alpine.start()
44
+ </script>
45
+ </body>
46
+ </html>`;
47
+ }
48
+
49
+ // src/dev/server.ts
50
+ import { createServer as createHttpServer } from "http";
51
+ import { apex } from "@apex-stack/vite";
52
+ import {
53
+ createApp,
54
+ defineEventHandler as defineEventHandler3,
55
+ fromNodeMiddleware,
56
+ setResponseHeader as setResponseHeader2,
57
+ setResponseStatus as setResponseStatus2,
58
+ toNodeListener
59
+ } from "h3";
60
+ import { createServer as createViteServer } from "vite";
61
+
62
+ // src/api/routes.ts
63
+ import { existsSync, readdirSync } from "fs";
64
+ import { join } from "path";
65
+ import {
66
+ defineEventHandler,
67
+ getQuery,
68
+ getRequestURL,
69
+ readBody,
70
+ setResponseHeader,
71
+ setResponseStatus
72
+ } from "h3";
73
+ import { z } from "zod";
74
+ function toSegments(pattern) {
75
+ return pattern.split("/").filter(Boolean).map((p) => p.startsWith(":") ? { param: p.slice(1) } : { literal: p });
76
+ }
77
+ function sanitizeName(name) {
78
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
79
+ }
80
+ function entryFor(pattern, method, mcpName, route) {
81
+ return { pattern, segments: toSegments(pattern), method, mcpName, route };
82
+ }
83
+ async function loadApiRoutes(root, loadModule) {
84
+ const dir = join(root, "server", "api");
85
+ if (!existsSync(dir)) return [];
86
+ const entries = [];
87
+ for (const file of readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f))) {
88
+ const name = file.replace(/\.(ts|js|mjs)$/, "");
89
+ const def = (await loadModule(`/server/api/${file}`)).default;
90
+ if (!def) continue;
91
+ if (isApexResource(def)) {
92
+ for (const r of def.routes) {
93
+ entries.push(entryFor(`/api/${def.name}${r.pathSuffix}`, r.route.method, r.mcpName, r.route));
94
+ }
95
+ } else if (typeof def.handler === "function") {
96
+ entries.push(entryFor(`/api/${name}`, def.method, sanitizeName(name), def));
97
+ }
98
+ }
99
+ return entries;
100
+ }
101
+ function matchApi(entries, path, method) {
102
+ const segs = (path.split("?")[0] ?? "/").split("/").filter(Boolean);
103
+ for (const entry of entries) {
104
+ if (entry.method !== method) continue;
105
+ if (entry.segments.length !== segs.length) continue;
106
+ const params = {};
107
+ let ok = true;
108
+ for (let i = 0; i < entry.segments.length; i++) {
109
+ const s = entry.segments[i];
110
+ const v = segs[i];
111
+ if (s.param) params[s.param] = decodeURIComponent(v);
112
+ else if (s.literal !== v) {
113
+ ok = false;
114
+ break;
115
+ }
116
+ }
117
+ if (ok) return { entry, params };
118
+ }
119
+ return null;
120
+ }
121
+ function createApiHandler(entries) {
122
+ return defineEventHandler(async (event) => {
123
+ const url = getRequestURL(event);
124
+ const matched = matchApi(entries, url.pathname, event.method);
125
+ if (!matched) {
126
+ setResponseStatus(event, 404);
127
+ return { error: `No API route for ${event.method} ${url.pathname}` };
128
+ }
129
+ const { entry, params } = matched;
130
+ const raw = {
131
+ ...entry.method === "GET" ? getQuery(event) : await readBody(event) ?? {},
132
+ ...params
133
+ };
134
+ let input = raw;
135
+ if (entry.route.inputShape) {
136
+ const parsed = z.object(entry.route.inputShape).safeParse(raw);
137
+ if (!parsed.success) {
138
+ setResponseStatus(event, 400);
139
+ return { error: "Invalid input", issues: parsed.error.issues };
140
+ }
141
+ input = parsed.data;
142
+ }
143
+ const result = await entry.route.handler({ input, url: url.toString() });
144
+ setResponseHeader(event, "Content-Type", "application/json");
145
+ return result;
146
+ });
147
+ }
148
+
149
+ // src/mcp/server.ts
150
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
151
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
152
+ import { defineEventHandler as defineEventHandler2, toWebRequest } from "h3";
153
+ function hasMcpRoutes(entries) {
154
+ return entries.some((e) => e.route.mcp);
155
+ }
156
+ function buildServer(entries) {
157
+ const server = new McpServer({ name: "apexjs", version: "0.0.0" });
158
+ for (const entry of entries) {
159
+ server.registerTool(
160
+ entry.mcpName,
161
+ {
162
+ description: entry.route.description ?? `Apex route ${entry.mcpName}`,
163
+ inputSchema: entry.route.inputShape ?? {}
164
+ },
165
+ async (args) => {
166
+ const result = await entry.route.handler({ input: args ?? {}, url: `mcp://${entry.mcpName}` });
167
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
168
+ }
169
+ );
170
+ }
171
+ return server;
172
+ }
173
+ function createMcpHandler(entries) {
174
+ const mcpEntries = entries.filter((e) => e.route.mcp);
175
+ return defineEventHandler2(async (event) => {
176
+ const server = buildServer(mcpEntries);
177
+ const transport = new WebStandardStreamableHTTPServerTransport({
178
+ sessionIdGenerator: void 0,
179
+ enableJsonResponse: true
180
+ });
181
+ await server.connect(transport);
182
+ const response = await transport.handleRequest(toWebRequest(event));
183
+ void server.close();
184
+ return response;
185
+ });
186
+ }
187
+
188
+ // src/components/registry.ts
189
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
190
+ import { join as join2 } from "path";
191
+ async function loadComponents(root, loadModule) {
192
+ const dir = join2(root, "components");
193
+ if (!existsSync2(dir)) return { registry: {}, css: "" };
194
+ const registry = {};
195
+ let css = "";
196
+ for (const file of readdirSync2(dir).filter((f) => f.endsWith(".alpine"))) {
197
+ const name = file.replace(/\.alpine$/, "");
198
+ const mod = await loadModule(`/components/${file}`);
199
+ registry[name] = { template: mod.template, rootXData: mod.rootXData, scopeId: mod.scopeId };
200
+ if (mod.css) css += `${mod.css}
201
+ `;
202
+ }
203
+ return { registry, css };
204
+ }
205
+
206
+ // src/islands/render.ts
207
+ import { renderIslands } from "@apex-stack/kit";
208
+ var ISLAND_LOADER = (
209
+ /* js */
210
+ `
211
+ let __alpine
212
+ function __ensureAlpine() {
213
+ return __alpine ??= import('alpinejs').then(function (m) {
214
+ const Alpine = m.default
215
+ window.Alpine = Alpine
216
+ Alpine.start() // islands are x-ignore'd, so this hydrates nothing on its own
217
+ return Alpine
218
+ })
219
+ }
220
+ async function __hydrate(el) {
221
+ const Alpine = await __ensureAlpine()
222
+ // Global Alpine.start() marked this island with the internal _x_ignore
223
+ // property (from the x-ignore attribute). Clear BOTH so initTree will descend
224
+ // and initialize the island's own x-data instead of early-returning.
225
+ el.removeAttribute('x-ignore')
226
+ delete el._x_ignore
227
+ Alpine.initTree(el)
228
+ el.setAttribute('data-apex-hydrated', '')
229
+ }
230
+ document.querySelectorAll('[data-apex-island]').forEach(function (el) {
231
+ const mode = el.getAttribute('data-apex-client')
232
+ if (mode === 'load') {
233
+ __hydrate(el)
234
+ } else if (mode === 'idle') {
235
+ (window.requestIdleCallback || function (cb) { return setTimeout(cb, 200) })(function () { __hydrate(el) })
236
+ } else if (mode === 'visible') {
237
+ const io = new IntersectionObserver(function (entries, obs) {
238
+ entries.forEach(function (e) { if (e.isIntersecting) { obs.unobserve(e.target); __hydrate(e.target) } })
239
+ })
240
+ io.observe(el)
241
+ }
242
+ // 'none' \u2192 do nothing; the SSR HTML is the final, static output.
243
+ })
244
+ `.trim()
245
+ );
246
+ async function renderIslandsPage(opts) {
247
+ const mod = await opts.loadModule(opts.pageId);
248
+ const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
249
+ const { html, hydratingCount } = renderIslands(
250
+ mod.template,
251
+ loaderData,
252
+ mod.scopeId,
253
+ opts.registry
254
+ );
255
+ const loaderScript = hydratingCount > 0 ? `
256
+ <script type="module">${ISLAND_LOADER}</script>` : "";
257
+ const doc = `<!DOCTYPE html>
258
+ <html lang="en">
259
+ <head>
260
+ <meta charset="utf-8" />
261
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
262
+ <title>Apex JS \u2014 Islands</title>
263
+ <style>${mod.css}${opts.componentCss ?? ""}</style>
264
+ </head>
265
+ <body>
266
+ ${html}${loaderScript}
267
+ </body>
268
+ </html>`;
269
+ return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
270
+ }
271
+
272
+ // src/routing/router.ts
273
+ import { existsSync as existsSync3, readdirSync as readdirSync3, statSync } from "fs";
274
+ import { join as join3, relative, sep } from "path";
275
+ function walkAlpine(dir) {
276
+ const out = [];
277
+ for (const entry of readdirSync3(dir)) {
278
+ const abs = join3(dir, entry);
279
+ if (statSync(abs).isDirectory()) out.push(...walkAlpine(abs));
280
+ else if (entry.endsWith(".alpine")) out.push(abs);
281
+ }
282
+ return out;
283
+ }
284
+ function scanPages(root) {
285
+ const dir = join3(root, "pages");
286
+ if (!existsSync3(dir)) return [];
287
+ const routes = walkAlpine(dir).map((abs) => {
288
+ const rel = relative(dir, abs).split(sep).join("/");
289
+ const pageId = `/pages/${rel}`;
290
+ const parts = rel.replace(/\.alpine$/, "").split("/");
291
+ if (parts[parts.length - 1] === "index") parts.pop();
292
+ const segments = parts.map((p) => {
293
+ const m = /^\[(.+)\]$/.exec(p);
294
+ return m ? { param: m[1] } : { literal: p };
295
+ });
296
+ const isDynamic = segments.some((s) => s.param !== void 0);
297
+ const pattern = `/${segments.map((s) => s.param ? `:${s.param}` : s.literal).join("/")}`;
298
+ return { pageId, pattern, segments, isDynamic };
299
+ });
300
+ return routes.sort((a, b) => Number(a.isDynamic) - Number(b.isDynamic));
301
+ }
302
+ function pathSegments(url) {
303
+ const path = url.split("?")[0] ?? "/";
304
+ return path.split("/").filter(Boolean);
305
+ }
306
+ function matchRoute(routes, url) {
307
+ const segs = pathSegments(url);
308
+ for (const route of routes) {
309
+ if (route.segments.length !== segs.length) continue;
310
+ const params = {};
311
+ let ok = true;
312
+ for (let i = 0; i < route.segments.length; i++) {
313
+ const rs = route.segments[i];
314
+ const value = segs[i];
315
+ if (rs.param) params[rs.param] = decodeURIComponent(value);
316
+ else if (rs.literal !== value) {
317
+ ok = false;
318
+ break;
319
+ }
320
+ }
321
+ if (ok) return { pageId: route.pageId, params };
322
+ }
323
+ return null;
324
+ }
325
+
326
+ // src/dev/server.ts
327
+ async function startDevServer(options) {
328
+ const port = options.port ?? 3e3;
329
+ const pageId = options.pageId ?? "/pages/index.alpine";
330
+ const vite = await createViteServer({
331
+ root: options.root,
332
+ appType: "custom",
333
+ server: { middlewareMode: true },
334
+ // User apps depend on `@apex-stack/core`, so the client module imports the runtime
335
+ // from `@apex-stack/core/client` (a re-export) rather than the internal kit package.
336
+ plugins: [apex({ clientRuntime: "@apex-stack/core/client" })],
337
+ optimizeDeps: { include: ["alpinejs"] }
338
+ });
339
+ const app = createApp();
340
+ app.use(fromNodeMiddleware(vite.middlewares));
341
+ const apiEntries = await loadApiRoutes(options.root, (id) => vite.ssrLoadModule(id));
342
+ if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
343
+ if (hasMcpRoutes(apiEntries)) {
344
+ app.use("/mcp", createMcpHandler(apiEntries));
345
+ }
346
+ app.use(
347
+ defineEventHandler3(async (event) => {
348
+ const url = event.path || "/";
349
+ try {
350
+ const routes = scanPages(options.root);
351
+ const matched = routes.length ? matchRoute(routes, url) : { pageId, params: {} };
352
+ if (!matched) {
353
+ setResponseStatus2(event, 404);
354
+ setResponseHeader2(event, "Content-Type", "text/html");
355
+ return notFoundPage(url, routes);
356
+ }
357
+ const { registry, css: componentCss } = await loadComponents(
358
+ options.root,
359
+ (id) => vite.ssrLoadModule(id)
360
+ );
361
+ const render = options.islands ? renderIslandsPage : renderPage;
362
+ const html = await render({
363
+ loadModule: (id) => vite.ssrLoadModule(id),
364
+ pageId: matched.pageId,
365
+ params: matched.params,
366
+ url,
367
+ registry,
368
+ componentCss,
369
+ transformHtml: (u, doc) => vite.transformIndexHtml(u, doc)
370
+ });
371
+ setResponseHeader2(event, "Content-Type", "text/html");
372
+ return html;
373
+ } catch (err) {
374
+ const error = err;
375
+ vite.ssrFixStacktrace(error);
376
+ setResponseStatus2(event, 500);
377
+ setResponseHeader2(event, "Content-Type", "text/html");
378
+ return `<pre>${escapeHtml(error.stack ?? error.message)}</pre>`;
379
+ }
380
+ })
381
+ );
382
+ const server = createHttpServer(toNodeListener(app));
383
+ await new Promise((resolve) => server.listen(port, resolve));
384
+ return {
385
+ vite,
386
+ server,
387
+ port,
388
+ close: async () => {
389
+ await vite.close();
390
+ await new Promise(
391
+ (resolve, reject) => server.close((e) => e ? reject(e) : resolve())
392
+ );
393
+ }
394
+ };
395
+ }
396
+ function escapeHtml(s) {
397
+ return s.replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
398
+ }
399
+ function notFoundPage(url, routes) {
400
+ const list = routes.map((r) => `<li><code>${escapeHtml(r.pattern)}</code></li>`).join("");
401
+ return `<!DOCTYPE html><html><head><title>404 \u2014 Apex JS</title></head>
402
+ <body style="font-family: system-ui, sans-serif; max-width: 40rem; margin: 3rem auto;">
403
+ <h1>404 \u2014 no route for <code>${escapeHtml(url)}</code></h1>
404
+ <p>Available routes:</p>
405
+ <ul>${list || "<li>(no pages found \u2014 add <code>pages/index.alpine</code>)</li>"}</ul>
406
+ </body></html>`;
407
+ }
408
+
409
+ export {
410
+ isApexResource,
411
+ renderPage,
412
+ startDevServer
413
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ import '@apex-stack/kit/client';
package/dist/cli.js ADDED
@@ -0,0 +1,207 @@
1
+ import {
2
+ startDevServer
3
+ } from "./chunk-XB5ZYPPE.js";
4
+
5
+ // src/cli.ts
6
+ import { resolve as resolve3 } from "path";
7
+ import { defineCommand as defineCommand4, runMain } from "citty";
8
+
9
+ // src/commands/make.ts
10
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
11
+ import { dirname, join, resolve } from "path";
12
+ import { defineCommand } from "citty";
13
+ function pageTemplate(name) {
14
+ return `<script server lang="ts">
15
+ export function loader() {
16
+ return { title: '${name}' }
17
+ }
18
+ </script>
19
+
20
+ <template x-data>
21
+ <main>
22
+ <h1 x-text="title"></h1>
23
+ </main>
24
+ </template>
25
+
26
+ <style scoped>
27
+ main { max-width: 40rem; margin: 3rem auto; font-family: system-ui, sans-serif; }
28
+ </style>
29
+ `;
30
+ }
31
+ function componentTemplate() {
32
+ return `<template x-data="{ count: 0 }">
33
+ <button @click="count++" x-text="count"></button>
34
+ </template>
35
+
36
+ <style scoped>
37
+ button { cursor: pointer; }
38
+ </style>
39
+ `;
40
+ }
41
+ function apiTemplate(name) {
42
+ return `import { defineApexRoute } from '@apex-stack/core'
43
+ import { z } from 'zod'
44
+
45
+ // GET /api/${name} \xB7 MCP tool "${name}"
46
+ export default defineApexRoute({
47
+ method: 'GET',
48
+ description: 'Describe what ${name} does',
49
+ input: { name: z.string() },
50
+ mcp: true,
51
+ handler: ({ input }) => ({ message: \`Hello, \${input.name}!\` }),
52
+ })
53
+ `;
54
+ }
55
+ function plan(kind, name, root) {
56
+ switch (kind) {
57
+ case "page":
58
+ return { path: join(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
59
+ case "component":
60
+ return { path: join(root, "components", `${name}.alpine`), contents: componentTemplate() };
61
+ case "api":
62
+ return { path: join(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
63
+ }
64
+ }
65
+ var makeCommand = defineCommand({
66
+ meta: { name: "make", description: "Generate a page, component, or API route" },
67
+ args: {
68
+ kind: { type: "positional", required: true, description: "page | component | api" },
69
+ name: { type: "positional", required: true, description: "Name (about, Counter, todos, \u2026)" },
70
+ root: { type: "string", description: "Project root", default: "." }
71
+ },
72
+ run({ args }) {
73
+ const kind = args.kind;
74
+ if (kind !== "page" && kind !== "component" && kind !== "api") {
75
+ console.error(`
76
+ Unknown type "${args.kind}". Use: page | component | api
77
+ `);
78
+ process.exit(1);
79
+ }
80
+ const root = resolve(process.cwd(), args.root);
81
+ const { path, contents } = plan(kind, args.name, root);
82
+ if (existsSync(path)) {
83
+ console.error(`
84
+ \u2717 Already exists: ${path}
85
+ `);
86
+ process.exit(1);
87
+ }
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ writeFileSync(path, contents);
90
+ console.log(`
91
+ \u2713 Created ${path.replace(`${root}/`, "")}
92
+ `);
93
+ }
94
+ });
95
+
96
+ // src/commands/mcp.ts
97
+ import { defineCommand as defineCommand2 } from "citty";
98
+ var mcpCommand = defineCommand2({
99
+ meta: { name: "mcp", description: "Inspect the local MCP server (list or call tools)" },
100
+ args: {
101
+ url: { type: "string", description: "MCP endpoint URL", default: "http://localhost:3000/mcp" },
102
+ call: { type: "string", description: "Name of a tool to call" },
103
+ args: { type: "string", description: "JSON arguments for --call", default: "{}" }
104
+ },
105
+ async run({ args }) {
106
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
107
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
108
+ const client = new Client({ name: "apex-mcp-cli", version: "0.0.0" });
109
+ try {
110
+ await client.connect(new StreamableHTTPClientTransport(new URL(args.url)));
111
+ } catch (err) {
112
+ console.error(
113
+ `
114
+ Could not reach an MCP server at ${args.url}
115
+ Is \`apex dev\` running? ${err.message}
116
+ `
117
+ );
118
+ process.exit(1);
119
+ }
120
+ if (args.call) {
121
+ const result = await client.callTool({
122
+ name: args.call,
123
+ arguments: JSON.parse(args.args)
124
+ });
125
+ console.log(`
126
+ \x1B[36m${args.call}\x1B[0m(${args.args}) \u2192`);
127
+ for (const part of result.content) {
128
+ console.log(" " + (part.text ?? JSON.stringify(part)));
129
+ }
130
+ console.log();
131
+ } else {
132
+ const { tools } = await client.listTools();
133
+ console.log(`
134
+ \x1B[36mMCP tools\x1B[0m at ${args.url} (${tools.length})
135
+ `);
136
+ for (const t of tools) {
137
+ const props = t.inputSchema?.properties;
138
+ const sig = props ? Object.entries(props).map(([k, v]) => `${k}: ${v.type ?? "any"}`).join(", ") : "";
139
+ console.log(` \u2022 \x1B[1m${t.name}\x1B[0m(${sig})`);
140
+ if (t.description) console.log(` ${t.description}`);
141
+ }
142
+ console.log(`
143
+ Call one: apex mcp --call <name> --args '{...}'
144
+ `);
145
+ }
146
+ await client.close();
147
+ }
148
+ });
149
+
150
+ // src/commands/migrate.ts
151
+ import { createRequire } from "module";
152
+ import { join as join2, resolve as resolve2 } from "path";
153
+ import { pathToFileURL } from "url";
154
+ import { defineCommand as defineCommand3 } from "citty";
155
+ var migrateCommand = defineCommand3({
156
+ meta: { name: "migrate", description: "Apply pending SQL migrations (db/migrations/*.sql)" },
157
+ args: {
158
+ db: { type: "string", description: "SQLite file path", default: "data.db" },
159
+ dir: { type: "string", description: "Migrations directory", default: "db/migrations" },
160
+ root: { type: "string", description: "Project root", default: "." }
161
+ },
162
+ async run({ args }) {
163
+ const root = resolve2(process.cwd(), args.root);
164
+ let data;
165
+ try {
166
+ const require2 = createRequire(join2(root, "package.json"));
167
+ data = await import(pathToFileURL(require2.resolve("@apex-stack/data")).href);
168
+ } catch {
169
+ console.error("\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n");
170
+ process.exit(1);
171
+ }
172
+ const { sqlite } = data.createDb(resolve2(root, args.db));
173
+ const applied = data.applyMigrations(sqlite, resolve2(root, args.dir));
174
+ console.log(
175
+ applied.length ? `
176
+ \u2713 Applied ${applied.length} migration(s): ${applied.join(", ")}
177
+ ` : "\n \u2713 Up to date \u2014 no pending migrations.\n"
178
+ );
179
+ }
180
+ });
181
+
182
+ // src/cli.ts
183
+ var dev = defineCommand4({
184
+ meta: { name: "dev", description: "Start the Apex JS development server" },
185
+ args: {
186
+ root: { type: "positional", required: false, description: "Project root", default: "." },
187
+ port: { type: "string", description: "Port to listen on", default: "3000" },
188
+ islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
189
+ },
190
+ async run({ args }) {
191
+ const root = resolve3(process.cwd(), args.root);
192
+ const port = Number(args.port);
193
+ const { port: actual } = await startDevServer({ root, port, islands: args.islands });
194
+ console.log(`
195
+ \x1B[36mApex JS\x1B[0m dev server ready
196
+ \u2192 http://localhost:${actual}
197
+ `);
198
+ }
199
+ });
200
+ var main = defineCommand4({
201
+ meta: {
202
+ name: "apex",
203
+ description: "The full-stack meta-framework for Alpine.js"
204
+ },
205
+ subCommands: { dev, make: makeCommand, migrate: migrateCommand, mcp: mcpCommand }
206
+ });
207
+ runMain(main);
@@ -0,0 +1 @@
1
+ export { registerApexComponent } from '@apex-stack/kit/client';
package/dist/client.js ADDED
@@ -0,0 +1,5 @@
1
+ // src/client.ts
2
+ import { registerApexComponent } from "@apex-stack/kit/client";
3
+ export {
4
+ registerApexComponent
5
+ };
@@ -0,0 +1,133 @@
1
+ import { ZodRawShape, z } from 'zod';
2
+ import { Server } from 'node:http';
3
+ import { ViteDevServer } from 'vite';
4
+ import { ComponentRegistry } from '@apex-stack/kit';
5
+
6
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
7
+ /** Inferred, validated input object for a route's handler. */
8
+ type InferInput<Shape extends ZodRawShape | undefined> = Shape extends ZodRawShape ? z.infer<z.ZodObject<Shape>> : Record<string, never>;
9
+ interface ApexRouteHandlerContext<Shape extends ZodRawShape | undefined> {
10
+ /** The validated input (query for GET, JSON body otherwise). */
11
+ input: InferInput<Shape>;
12
+ /** The raw request URL. */
13
+ url: string;
14
+ }
15
+ interface ApexRouteConfig<Shape extends ZodRawShape | undefined, Output> {
16
+ /** HTTP method. Defaults to GET. */
17
+ method?: HttpMethod;
18
+ /** Human + AI-readable description. Becomes the MCP tool description. */
19
+ description?: string;
20
+ /**
21
+ * Input contract as a Zod raw shape (an object of Zod validators). Drives both
22
+ * REST validation AND the MCP tool's inputSchema — one definition, both worlds.
23
+ */
24
+ input?: Shape;
25
+ /** Opt in to exposing this route as an MCP tool. Defaults to false. */
26
+ mcp?: boolean;
27
+ /**
28
+ * Override the MCP tool name (must match ^[a-zA-Z0-9_-]{1,64}$). Defaults to a
29
+ * slug derived from the file path.
30
+ */
31
+ mcpName?: string;
32
+ /** The route implementation. Business logic should live in a service; this is the adapter. */
33
+ handler: (ctx: ApexRouteHandlerContext<Shape>) => Output | Promise<Output>;
34
+ }
35
+ /** The normalized, framework-facing route object produced by defineApexRoute. */
36
+ interface ApexRoute {
37
+ method: HttpMethod;
38
+ description?: string;
39
+ inputShape?: ZodRawShape;
40
+ mcp: boolean;
41
+ mcpName?: string;
42
+ handler: (ctx: {
43
+ input: unknown;
44
+ url: string;
45
+ }) => unknown | Promise<unknown>;
46
+ }
47
+ /**
48
+ * Define a typed API route. A single definition serves as:
49
+ * - a validated REST endpoint, and
50
+ * - (when `mcp: true`) an MCP tool whose inputSchema is derived from `input`.
51
+ *
52
+ * The strict, schema-carrying contract is what makes "any Apex API can be MCP"
53
+ * possible with no extra library on the user's side.
54
+ */
55
+ declare function defineApexRoute<Shape extends ZodRawShape | undefined, Output>(config: ApexRouteConfig<Shape, Output>): ApexRoute;
56
+
57
+ /** One route within a resource, mounted at `/api/<name><pathSuffix>`. */
58
+ interface ResourceRoute {
59
+ /** Path suffix after the resource name, e.g. '' or '/:id'. */
60
+ pathSuffix: string;
61
+ /** MCP tool name for this route (e.g. `todos_list`). */
62
+ mcpName: string;
63
+ route: ApexRoute;
64
+ }
65
+ /**
66
+ * A resource expands to several routes (list/get/create/…) from one definition.
67
+ * Built by `defineResource` in `@apex-stack/data`; recognized by the core API loader.
68
+ */
69
+ interface ApexResource {
70
+ __apexResource: true;
71
+ name: string;
72
+ routes: ResourceRoute[];
73
+ }
74
+ declare function isApexResource(x: unknown): x is ApexResource;
75
+
76
+ interface DevServerOptions {
77
+ root: string;
78
+ port?: number;
79
+ /** Page module rendered for every route in the spike (single-route). */
80
+ pageId?: string;
81
+ /** Render in islands mode (static-first, per-island hydration) instead of one page component. */
82
+ islands?: boolean;
83
+ }
84
+ interface DevServer {
85
+ vite: ViteDevServer;
86
+ server: Server;
87
+ port: number;
88
+ close: () => Promise<void>;
89
+ }
90
+ /**
91
+ * Start the Apex dev server: Vite in middleware mode (build/HMR/asset serving)
92
+ * fronted by an h3 app whose catch-all handler SSRs the page. Written as h3
93
+ * event handlers so the render path can migrate to Nitro unchanged.
94
+ */
95
+ declare function startDevServer(options: DevServerOptions): Promise<DevServer>;
96
+
97
+ /** The shape a compiled `.alpine` SSR module exports (see @apex-stack/vite). */
98
+ interface PageModule {
99
+ loader: (ctx: {
100
+ params: Record<string, string>;
101
+ url: string;
102
+ }) => unknown | Promise<unknown>;
103
+ template: string;
104
+ rootXData: string | null;
105
+ componentId: string;
106
+ scopeId: string;
107
+ css: string;
108
+ }
109
+ interface RenderPageOptions {
110
+ /** Load a page's SSR module (dev: vite.ssrLoadModule; prod: static import). */
111
+ loadModule: (id: string) => Promise<PageModule>;
112
+ /** The page module id to render, e.g. `/pages/index.alpine`. */
113
+ pageId: string;
114
+ /** The incoming request path. */
115
+ url: string;
116
+ /** Route params captured from a dynamic segment (e.g. { slug: '...' }). */
117
+ params?: Record<string, string>;
118
+ /** Registry of embeddable components. */
119
+ registry?: ComponentRegistry;
120
+ /** Aggregated component CSS to include in the shell. */
121
+ componentCss?: string;
122
+ /** Post-process the shell HTML (dev: vite.transformIndexHtml). */
123
+ transformHtml?: (url: string, html: string) => string | Promise<string>;
124
+ }
125
+ /**
126
+ * The framework's render seam — deliberately dev-server-agnostic so it can move
127
+ * verbatim into a Nitro route handler post-spike. Loads the page module, runs
128
+ * its loader, renders the component to hydration-safe HTML, and assembles the
129
+ * document shell (SSR body + state island + client entry).
130
+ */
131
+ declare function renderPage(opts: RenderPageOptions): Promise<string>;
132
+
133
+ export { type ApexResource, type ApexRoute, type ApexRouteConfig, type ApexRouteHandlerContext, type DevServer, type DevServerOptions, type HttpMethod, type PageModule, type RenderPageOptions, type ResourceRoute, defineApexRoute, isApexResource, renderPage, startDevServer };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ isApexResource,
3
+ renderPage,
4
+ startDevServer
5
+ } from "./chunk-XB5ZYPPE.js";
6
+
7
+ // src/api/defineRoute.ts
8
+ function defineApexRoute(config) {
9
+ return {
10
+ method: config.method ?? "GET",
11
+ description: config.description,
12
+ inputShape: config.input,
13
+ mcp: config.mcp ?? false,
14
+ mcpName: config.mcpName,
15
+ handler: config.handler
16
+ };
17
+ }
18
+ export {
19
+ defineApexRoute,
20
+ isApexResource,
21
+ renderPage,
22
+ startDevServer
23
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@apex-stack/core",
3
+ "version": "0.1.1",
4
+ "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Andre Corugda",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/andrecorugda/apexjs.git",
11
+ "directory": "packages/apexjs"
12
+ },
13
+ "keywords": [
14
+ "alpine",
15
+ "alpinejs",
16
+ "framework",
17
+ "ssr",
18
+ "apex",
19
+ "apexjs"
20
+ ],
21
+ "bin": {
22
+ "apex": "./dist/cli.js"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./client": {
30
+ "types": "./dist/client.d.ts",
31
+ "import": "./dist/client.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "citty": "^0.1.6",
40
+ "h3": "^1.13.0",
41
+ "vite": "^6.0.7",
42
+ "zod": "^4.4.3",
43
+ "@apex-stack/vite": "0.1.1",
44
+ "@apex-stack/kit": "0.1.1"
45
+ },
46
+ "peerDependencies": {
47
+ "alpinejs": "^3.14.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=20.19"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "dev": "tsup --watch",
55
+ "typecheck": "tsc --noEmit"
56
+ }
57
+ }