@apex-stack/core 0.1.10 → 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.
@@ -1,426 +0,0 @@
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
- clientHref: opts.clientHref
25
- });
26
- return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
27
- }
28
- function shell({ body, island, css, pageId, clientHref }) {
29
- const clientScript = clientHref ? `<script type="module" src="${clientHref}"></script>` : `<script type="module">
30
- import Alpine from 'alpinejs'
31
- import ${JSON.stringify(pageId)}
32
- window.Alpine = Alpine
33
- Alpine.start()
34
- </script>`;
35
- return `<!DOCTYPE html>
36
- <html lang="en">
37
- <head>
38
- <meta charset="utf-8" />
39
- <meta name="viewport" content="width=device-width, initial-scale=1" />
40
- <title>Apex JS</title>
41
- <style>${css}</style>
42
- </head>
43
- <body>
44
- ${body}
45
- ${island}
46
- ${clientScript}
47
- </body>
48
- </html>`;
49
- }
50
-
51
- // src/dev/server.ts
52
- import { createServer as createHttpServer } from "http";
53
- import { apex } from "@apex-stack/vite";
54
- import {
55
- createApp,
56
- defineEventHandler as defineEventHandler3,
57
- fromNodeMiddleware,
58
- setResponseHeader as setResponseHeader2,
59
- setResponseStatus as setResponseStatus2,
60
- toNodeListener
61
- } from "h3";
62
- import { createServer as createViteServer } from "vite";
63
-
64
- // src/api/routes.ts
65
- import { existsSync, readdirSync } from "fs";
66
- import { join } from "path";
67
- import {
68
- defineEventHandler,
69
- getQuery,
70
- getRequestURL,
71
- readBody,
72
- setResponseHeader,
73
- setResponseStatus
74
- } from "h3";
75
- import { z } from "zod";
76
- function toSegments(pattern) {
77
- return pattern.split("/").filter(Boolean).map((p) => p.startsWith(":") ? { param: p.slice(1) } : { literal: p });
78
- }
79
- function sanitizeName(name) {
80
- return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
81
- }
82
- function entryFor(pattern, method, mcpName, route) {
83
- return { pattern, segments: toSegments(pattern), method, mcpName, route };
84
- }
85
- function expandApiModule(name, def) {
86
- if (!def) return [];
87
- if (isApexResource(def)) {
88
- return def.routes.map(
89
- (r) => entryFor(`/api/${def.name}${r.pathSuffix}`, r.route.method, r.mcpName, r.route)
90
- );
91
- }
92
- if (typeof def.handler === "function") {
93
- return [entryFor(`/api/${name}`, def.method, sanitizeName(name), def)];
94
- }
95
- return [];
96
- }
97
- async function loadApiRoutes(root, loadModule) {
98
- const dir = join(root, "server", "api");
99
- if (!existsSync(dir)) return [];
100
- const entries = [];
101
- for (const file of readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f))) {
102
- const name = file.replace(/\.(ts|js|mjs)$/, "");
103
- const def = (await loadModule(`/server/api/${file}`)).default;
104
- entries.push(...expandApiModule(name, def));
105
- }
106
- return entries;
107
- }
108
- function matchApi(entries, path, method) {
109
- const segs = (path.split("?")[0] ?? "/").split("/").filter(Boolean);
110
- for (const entry of entries) {
111
- if (entry.method !== method) continue;
112
- if (entry.segments.length !== segs.length) continue;
113
- const params = {};
114
- let ok = true;
115
- for (let i = 0; i < entry.segments.length; i++) {
116
- const s = entry.segments[i];
117
- const v = segs[i];
118
- if (s.param) params[s.param] = decodeURIComponent(v);
119
- else if (s.literal !== v) {
120
- ok = false;
121
- break;
122
- }
123
- }
124
- if (ok) return { entry, params };
125
- }
126
- return null;
127
- }
128
- function createApiHandler(entries) {
129
- return defineEventHandler(async (event) => {
130
- const url = getRequestURL(event);
131
- const matched = matchApi(entries, url.pathname, event.method);
132
- if (!matched) {
133
- setResponseStatus(event, 404);
134
- return { error: `No API route for ${event.method} ${url.pathname}` };
135
- }
136
- const { entry, params } = matched;
137
- const raw = {
138
- ...entry.method === "GET" ? getQuery(event) : await readBody(event) ?? {},
139
- ...params
140
- };
141
- let input = raw;
142
- if (entry.route.inputShape) {
143
- const parsed = z.object(entry.route.inputShape).safeParse(raw);
144
- if (!parsed.success) {
145
- setResponseStatus(event, 400);
146
- return { error: "Invalid input", issues: parsed.error.issues };
147
- }
148
- input = parsed.data;
149
- }
150
- const result = await entry.route.handler({ input, url: url.toString() });
151
- setResponseHeader(event, "Content-Type", "application/json");
152
- return result;
153
- });
154
- }
155
-
156
- // src/mcp/server.ts
157
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
158
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
159
- import { defineEventHandler as defineEventHandler2, toWebRequest } from "h3";
160
- function hasMcpRoutes(entries) {
161
- return entries.some((e) => e.route.mcp);
162
- }
163
- function buildServer(entries) {
164
- const server = new McpServer({ name: "apexjs", version: "0.0.0" });
165
- for (const entry of entries) {
166
- server.registerTool(
167
- entry.mcpName,
168
- {
169
- description: entry.route.description ?? `Apex route ${entry.mcpName}`,
170
- inputSchema: entry.route.inputShape ?? {}
171
- },
172
- async (args) => {
173
- const result = await entry.route.handler({ input: args ?? {}, url: `mcp://${entry.mcpName}` });
174
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
175
- }
176
- );
177
- }
178
- return server;
179
- }
180
- function createMcpHandler(entries) {
181
- const mcpEntries = entries.filter((e) => e.route.mcp);
182
- return defineEventHandler2(async (event) => {
183
- const server = buildServer(mcpEntries);
184
- const transport = new WebStandardStreamableHTTPServerTransport({
185
- sessionIdGenerator: void 0,
186
- enableJsonResponse: true
187
- });
188
- await server.connect(transport);
189
- const response = await transport.handleRequest(toWebRequest(event));
190
- void server.close();
191
- return response;
192
- });
193
- }
194
-
195
- // src/components/registry.ts
196
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
197
- import { join as join2 } from "path";
198
- async function loadComponents(root, loadModule) {
199
- const dir = join2(root, "components");
200
- if (!existsSync2(dir)) return { registry: {}, css: "" };
201
- const registry = {};
202
- let css = "";
203
- for (const file of readdirSync2(dir).filter((f) => f.endsWith(".alpine"))) {
204
- const name = file.replace(/\.alpine$/, "");
205
- const mod = await loadModule(`/components/${file}`);
206
- registry[name] = { template: mod.template, rootXData: mod.rootXData, scopeId: mod.scopeId };
207
- if (mod.css) css += `${mod.css}
208
- `;
209
- }
210
- return { registry, css };
211
- }
212
-
213
- // src/islands/render.ts
214
- import { renderIslands } from "@apex-stack/kit";
215
- var ISLAND_LOADER = (
216
- /* js */
217
- `
218
- let __alpine
219
- function __ensureAlpine() {
220
- return __alpine ??= import('alpinejs').then(function (m) {
221
- const Alpine = m.default
222
- window.Alpine = Alpine
223
- Alpine.start() // islands are x-ignore'd, so this hydrates nothing on its own
224
- return Alpine
225
- })
226
- }
227
- async function __hydrate(el) {
228
- const Alpine = await __ensureAlpine()
229
- // Global Alpine.start() marked this island with the internal _x_ignore
230
- // property (from the x-ignore attribute). Clear BOTH so initTree will descend
231
- // and initialize the island's own x-data instead of early-returning.
232
- el.removeAttribute('x-ignore')
233
- delete el._x_ignore
234
- Alpine.initTree(el)
235
- el.setAttribute('data-apex-hydrated', '')
236
- }
237
- document.querySelectorAll('[data-apex-island]').forEach(function (el) {
238
- const mode = el.getAttribute('data-apex-client')
239
- if (mode === 'load') {
240
- __hydrate(el)
241
- } else if (mode === 'idle') {
242
- (window.requestIdleCallback || function (cb) { return setTimeout(cb, 200) })(function () { __hydrate(el) })
243
- } else if (mode === 'visible') {
244
- const io = new IntersectionObserver(function (entries, obs) {
245
- entries.forEach(function (e) { if (e.isIntersecting) { obs.unobserve(e.target); __hydrate(e.target) } })
246
- })
247
- io.observe(el)
248
- }
249
- // 'none' \u2192 do nothing; the SSR HTML is the final, static output.
250
- })
251
- `.trim()
252
- );
253
- async function renderIslandsPage(opts) {
254
- const mod = await opts.loadModule(opts.pageId);
255
- const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
256
- const { html, hydratingCount } = renderIslands(
257
- mod.template,
258
- loaderData,
259
- mod.scopeId,
260
- opts.registry
261
- );
262
- const loaderScript = hydratingCount > 0 ? `
263
- <script type="module">${ISLAND_LOADER}</script>` : "";
264
- const doc = `<!DOCTYPE html>
265
- <html lang="en">
266
- <head>
267
- <meta charset="utf-8" />
268
- <meta name="viewport" content="width=device-width, initial-scale=1" />
269
- <title>Apex JS \u2014 Islands</title>
270
- <style>${mod.css}${opts.componentCss ?? ""}</style>
271
- </head>
272
- <body>
273
- ${html}${loaderScript}
274
- </body>
275
- </html>`;
276
- return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
277
- }
278
-
279
- // src/routing/router.ts
280
- import { existsSync as existsSync3, readdirSync as readdirSync3, statSync } from "fs";
281
- import { join as join3, relative, sep } from "path";
282
- function walkAlpine(dir) {
283
- const out = [];
284
- for (const entry of readdirSync3(dir)) {
285
- const abs = join3(dir, entry);
286
- if (statSync(abs).isDirectory()) out.push(...walkAlpine(abs));
287
- else if (entry.endsWith(".alpine")) out.push(abs);
288
- }
289
- return out;
290
- }
291
- function scanPages(root) {
292
- const dir = join3(root, "pages");
293
- if (!existsSync3(dir)) return [];
294
- const routes = walkAlpine(dir).map((abs) => {
295
- const rel = relative(dir, abs).split(sep).join("/");
296
- const pageId = `/pages/${rel}`;
297
- const parts = rel.replace(/\.alpine$/, "").split("/");
298
- if (parts[parts.length - 1] === "index") parts.pop();
299
- const segments = parts.map((p) => {
300
- const m = /^\[(.+)\]$/.exec(p);
301
- return m ? { param: m[1] } : { literal: p };
302
- });
303
- const isDynamic = segments.some((s) => s.param !== void 0);
304
- const pattern = `/${segments.map((s) => s.param ? `:${s.param}` : s.literal).join("/")}`;
305
- return { pageId, pattern, segments, isDynamic };
306
- });
307
- return routes.sort((a, b) => Number(a.isDynamic) - Number(b.isDynamic));
308
- }
309
- function pathSegments(url) {
310
- const path = url.split("?")[0] ?? "/";
311
- return path.split("/").filter(Boolean);
312
- }
313
- function matchRoute(routes, url) {
314
- const segs = pathSegments(url);
315
- for (const route of routes) {
316
- if (route.segments.length !== segs.length) continue;
317
- const params = {};
318
- let ok = true;
319
- for (let i = 0; i < route.segments.length; i++) {
320
- const rs = route.segments[i];
321
- const value = segs[i];
322
- if (rs.param) params[rs.param] = decodeURIComponent(value);
323
- else if (rs.literal !== value) {
324
- ok = false;
325
- break;
326
- }
327
- }
328
- if (ok) return { pageId: route.pageId, params };
329
- }
330
- return null;
331
- }
332
-
333
- // src/dev/server.ts
334
- async function startDevServer(options) {
335
- const port = options.port ?? 3e3;
336
- const pageId = options.pageId ?? "/pages/index.alpine";
337
- const vite = await createViteServer({
338
- root: options.root,
339
- appType: "custom",
340
- server: { middlewareMode: true },
341
- // User apps depend on `@apex-stack/core`, so the client module imports the runtime
342
- // from `@apex-stack/core/client` (a re-export) rather than the internal kit package.
343
- plugins: [apex({ clientRuntime: "@apex-stack/core/client" })],
344
- optimizeDeps: { include: ["alpinejs"] }
345
- });
346
- const app = createApp();
347
- app.use(fromNodeMiddleware(vite.middlewares));
348
- const loadEntries = () => loadApiRoutes(options.root, (id) => vite.ssrLoadModule(id));
349
- app.use("/api", defineEventHandler3((event) => loadEntries().then((e) => createApiHandler(e)(event))));
350
- app.use("/mcp", defineEventHandler3((event) => loadEntries().then((e) => createMcpHandler(e)(event))));
351
- app.use(
352
- defineEventHandler3(async (event) => {
353
- const url = event.path || "/";
354
- try {
355
- const routes = scanPages(options.root);
356
- const matched = routes.length ? matchRoute(routes, url) : { pageId, params: {} };
357
- if (!matched) {
358
- setResponseStatus2(event, 404);
359
- setResponseHeader2(event, "Content-Type", "text/html");
360
- return notFoundPage(url, routes);
361
- }
362
- const { registry, css: componentCss } = await loadComponents(
363
- options.root,
364
- (id) => vite.ssrLoadModule(id)
365
- );
366
- const render = options.islands ? renderIslandsPage : renderPage;
367
- const html = await render({
368
- loadModule: (id) => vite.ssrLoadModule(id),
369
- pageId: matched.pageId,
370
- params: matched.params,
371
- url,
372
- registry,
373
- componentCss,
374
- transformHtml: (u, doc) => vite.transformIndexHtml(u, doc)
375
- });
376
- setResponseHeader2(event, "Content-Type", "text/html");
377
- return html;
378
- } catch (err) {
379
- const error = err;
380
- vite.ssrFixStacktrace(error);
381
- setResponseStatus2(event, 500);
382
- setResponseHeader2(event, "Content-Type", "text/html");
383
- return `<pre>${escapeHtml(error.stack ?? error.message)}</pre>`;
384
- }
385
- })
386
- );
387
- const server = createHttpServer(toNodeListener(app));
388
- await new Promise((resolve) => server.listen(port, resolve));
389
- return {
390
- vite,
391
- server,
392
- port,
393
- close: async () => {
394
- await vite.close();
395
- await new Promise(
396
- (resolve, reject) => server.close((e) => e ? reject(e) : resolve())
397
- );
398
- }
399
- };
400
- }
401
- function escapeHtml(s) {
402
- return s.replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
403
- }
404
- function notFoundPage(url, routes) {
405
- const list = routes.map((r) => `<li><code>${escapeHtml(r.pattern)}</code></li>`).join("");
406
- return `<!DOCTYPE html><html><head><title>404 \u2014 Apex JS</title></head>
407
- <body style="font-family: system-ui, sans-serif; max-width: 40rem; margin: 3rem auto;">
408
- <h1>404 \u2014 no route for <code>${escapeHtml(url)}</code></h1>
409
- <p>Available routes:</p>
410
- <ul>${list || "<li>(no pages found \u2014 add <code>pages/index.alpine</code>)</li>"}</ul>
411
- </body></html>`;
412
- }
413
-
414
- export {
415
- isApexResource,
416
- expandApiModule,
417
- createApiHandler,
418
- hasMcpRoutes,
419
- createMcpHandler,
420
- loadComponents,
421
- renderIslandsPage,
422
- scanPages,
423
- matchRoute,
424
- renderPage,
425
- startDevServer
426
- };