@apex-stack/core 0.1.2 → 0.1.3

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.
@@ -82,6 +82,18 @@ function sanitizeName(name) {
82
82
  function entryFor(pattern, method, mcpName, route) {
83
83
  return { pattern, segments: toSegments(pattern), method, mcpName, route };
84
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
+ }
85
97
  async function loadApiRoutes(root, loadModule) {
86
98
  const dir = join(root, "server", "api");
87
99
  if (!existsSync(dir)) return [];
@@ -89,14 +101,7 @@ async function loadApiRoutes(root, loadModule) {
89
101
  for (const file of readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f))) {
90
102
  const name = file.replace(/\.(ts|js|mjs)$/, "");
91
103
  const def = (await loadModule(`/server/api/${file}`)).default;
92
- if (!def) continue;
93
- if (isApexResource(def)) {
94
- for (const r of def.routes) {
95
- entries.push(entryFor(`/api/${def.name}${r.pathSuffix}`, r.route.method, r.mcpName, r.route));
96
- }
97
- } else if (typeof def.handler === "function") {
98
- entries.push(entryFor(`/api/${name}`, def.method, sanitizeName(name), def));
99
- }
104
+ entries.push(...expandApiModule(name, def));
100
105
  }
101
106
  return entries;
102
107
  }
@@ -410,9 +415,14 @@ function notFoundPage(url, routes) {
410
415
 
411
416
  export {
412
417
  isApexResource,
418
+ expandApiModule,
419
+ createApiHandler,
420
+ hasMcpRoutes,
421
+ createMcpHandler,
413
422
  loadComponents,
414
423
  renderIslandsPage,
415
424
  scanPages,
425
+ matchRoute,
416
426
  renderPage,
417
427
  startDevServer
418
428
  };
package/dist/cli.js CHANGED
@@ -1,19 +1,24 @@
1
1
  import {
2
+ createApiHandler,
3
+ createMcpHandler,
4
+ expandApiModule,
5
+ hasMcpRoutes,
2
6
  loadComponents,
7
+ matchRoute,
3
8
  renderIslandsPage,
4
9
  renderPage,
5
10
  scanPages,
6
11
  startDevServer
7
- } from "./chunk-GC3WNE6M.js";
12
+ } from "./chunk-SGEM3N42.js";
8
13
 
9
14
  // src/cli.ts
10
- import { resolve as resolve4 } from "path";
11
- import { defineCommand as defineCommand5, runMain } from "citty";
15
+ import { resolve as resolve5 } from "path";
16
+ import { defineCommand as defineCommand6, runMain } from "citty";
12
17
 
13
18
  // src/commands/build.ts
14
- import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
15
- import { dirname, join as join2, resolve } from "path";
16
- import { apex as apex2 } from "@apex-stack/vite";
19
+ import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "fs";
20
+ import { dirname, join as join3, resolve } from "path";
21
+ import { apex as apex3 } from "@apex-stack/vite";
17
22
  import { defineCommand } from "citty";
18
23
  import { createServer as createViteServer } from "vite";
19
24
 
@@ -69,6 +74,65 @@ async function buildClient(root, routes, outDir) {
69
74
  return hrefs;
70
75
  }
71
76
 
77
+ // src/build/buildServer.ts
78
+ import { existsSync, readdirSync } from "fs";
79
+ import { isAbsolute, join as join2 } from "path";
80
+ import { apex as apex2 } from "@apex-stack/vite";
81
+ import { build as build2 } from "vite";
82
+ async function buildServer(root, routes, outDir) {
83
+ const ids = routes.map((r) => r.pageId);
84
+ const compDir = join2(root, "components");
85
+ if (existsSync(compDir)) {
86
+ for (const f of readdirSync(compDir).filter((f2) => f2.endsWith(".alpine"))) {
87
+ ids.push(`/components/${f}`);
88
+ }
89
+ }
90
+ const apiDir = join2(root, "server", "api");
91
+ if (existsSync(apiDir)) {
92
+ for (const f of readdirSync(apiDir).filter((f2) => /\.(ts|js|mjs)$/.test(f2))) {
93
+ ids.push(`/server/api/${f}`);
94
+ }
95
+ }
96
+ const input = {};
97
+ for (const id of ids) input[entryName2(id)] = join2(root, id.slice(1));
98
+ const result = await build2({
99
+ root,
100
+ logLevel: "warn",
101
+ plugins: [apex2({ clientRuntime: "@apex-stack/core/client" })],
102
+ build: {
103
+ ssr: true,
104
+ target: "esnext",
105
+ // Node target — allow top-level await in server modules
106
+ outDir: join2(outDir, "server"),
107
+ emptyOutDir: false,
108
+ rollupOptions: {
109
+ input,
110
+ // Externalize every package import (bare specifier) — deps are resolved at
111
+ // runtime from node_modules. Only the app's own relative/absolute files are
112
+ // bundled. This keeps native/workspace deps (@libsql/client, drizzle, …) out.
113
+ external: (id) => !id.startsWith(".") && !isAbsolute(id),
114
+ output: { format: "esm", entryFileNames: "[name].mjs" }
115
+ }
116
+ }
117
+ });
118
+ const byFacade = /* @__PURE__ */ new Map();
119
+ for (const chunk of result.output) {
120
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
121
+ byFacade.set(chunk.facadeModuleId, chunk.fileName);
122
+ }
123
+ }
124
+ const modules = {};
125
+ for (const id of ids) {
126
+ const abs = join2(root, id.slice(1));
127
+ const file = byFacade.get(abs);
128
+ if (file) modules[id] = file;
129
+ }
130
+ return { modules };
131
+ }
132
+ function entryName2(id) {
133
+ return id.replace(/^\//, "").replace(/\.(alpine|ts|js|mjs)$/, "").replace(/[^a-zA-Z0-9]+/g, "_");
134
+ }
135
+
72
136
  // src/commands/build.ts
73
137
  function outFile(pattern) {
74
138
  const clean = pattern.replace(/^\//, "");
@@ -79,7 +143,8 @@ var buildCommand = defineCommand({
79
143
  args: {
80
144
  root: { type: "positional", required: false, description: "Project root", default: "." },
81
145
  outDir: { type: "string", description: "Output directory", default: "dist" },
82
- islands: { type: "boolean", description: "Static-first islands mode (zero-JS static)", default: false }
146
+ islands: { type: "boolean", description: "Static-first islands mode (zero-JS static)", default: false },
147
+ server: { type: "boolean", description: "Build a Node server (dynamic routes + API/MCP)", default: false }
83
148
  },
84
149
  async run({ args }) {
85
150
  const root = resolve(process.cwd(), args.root);
@@ -88,12 +153,15 @@ var buildCommand = defineCommand({
88
153
  const routes = scanPages(root);
89
154
  const staticRoutes = routes.filter((r) => !r.isDynamic);
90
155
  const dynamic = routes.filter((r) => r.isDynamic);
156
+ if (args.server) {
157
+ return buildServerTarget(root, outDir, args.outDir, routes);
158
+ }
91
159
  const hrefs = args.islands ? /* @__PURE__ */ new Map() : await buildClient(root, staticRoutes, outDir);
92
160
  const vite = await createViteServer({
93
161
  root,
94
162
  appType: "custom",
95
163
  server: { middlewareMode: true },
96
- plugins: [apex2({ clientRuntime: "@apex-stack/core/client" })]
164
+ plugins: [apex3({ clientRuntime: "@apex-stack/core/client" })]
97
165
  });
98
166
  try {
99
167
  const { registry, css: componentCss } = await loadComponents(
@@ -109,13 +177,13 @@ var buildCommand = defineCommand({
109
177
  componentCss
110
178
  };
111
179
  const html = args.islands ? await renderIslandsPage(common) : await renderPage({ ...common, clientHref: hrefs.get(route.pageId) });
112
- const dest = join2(outDir, outFile(route.pattern));
180
+ const dest = join3(outDir, outFile(route.pattern));
113
181
  mkdirSync(dirname(dest), { recursive: true });
114
182
  writeFileSync(dest, html);
115
183
  console.log(` \u2713 ${route.pattern} \u2192 ${outFile(route.pattern)}`);
116
184
  }
117
- const pub = join2(root, "public");
118
- if (existsSync(pub)) cpSync(pub, outDir, { recursive: true });
185
+ const pub = join3(root, "public");
186
+ if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
119
187
  console.log(
120
188
  `
121
189
  Built ${staticRoutes.length} page(s) \u2192 ${args.outDir}/` + (args.islands ? " (islands / static-first)" : " (prerendered + hydrated)")
@@ -131,10 +199,49 @@ var buildCommand = defineCommand({
131
199
  }
132
200
  }
133
201
  });
202
+ async function buildServerTarget(root, outDir, outLabel, routes) {
203
+ const clientHrefs = await buildClient(root, routes, outDir);
204
+ const server = await buildServer(root, routes, outDir);
205
+ const components = {};
206
+ const compDir = join3(root, "components");
207
+ if (existsSync2(compDir)) {
208
+ for (const f of readdirSync2(compDir).filter((f2) => f2.endsWith(".alpine"))) {
209
+ const sf = server.modules[`/components/${f}`];
210
+ if (sf) components[f.replace(/\.alpine$/, "")] = sf;
211
+ }
212
+ }
213
+ const api = [];
214
+ const apiDir = join3(root, "server", "api");
215
+ if (existsSync2(apiDir)) {
216
+ for (const f of readdirSync2(apiDir).filter((f2) => /\.(ts|js|mjs)$/.test(f2))) {
217
+ const sf = server.modules[`/server/api/${f}`];
218
+ if (sf) api.push({ name: f.replace(/\.(ts|js|mjs)$/, ""), serverFile: sf });
219
+ }
220
+ }
221
+ const manifest = {
222
+ islands: false,
223
+ routes: routes.map((r) => ({
224
+ ...r,
225
+ serverFile: server.modules[r.pageId],
226
+ clientHref: clientHrefs.get(r.pageId)
227
+ })),
228
+ components,
229
+ api
230
+ };
231
+ writeFileSync(join3(outDir, "apex-manifest.json"), JSON.stringify(manifest, null, 2));
232
+ const pub = join3(root, "public");
233
+ if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
234
+ console.log(
235
+ `
236
+ Built server target \u2192 ${outLabel}/ (${routes.length} route(s), ${api.length} API module(s))
237
+ Run it: apex start
238
+ `
239
+ );
240
+ }
134
241
 
135
242
  // src/commands/make.ts
136
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
137
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
243
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
244
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
138
245
  import { defineCommand as defineCommand2 } from "citty";
139
246
  function pageTemplate(name) {
140
247
  return `<script server lang="ts">
@@ -181,11 +288,11 @@ export default defineApexRoute({
181
288
  function plan(kind, name, root) {
182
289
  switch (kind) {
183
290
  case "page":
184
- return { path: join3(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
291
+ return { path: join4(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
185
292
  case "component":
186
- return { path: join3(root, "components", `${name}.alpine`), contents: componentTemplate() };
293
+ return { path: join4(root, "components", `${name}.alpine`), contents: componentTemplate() };
187
294
  case "api":
188
- return { path: join3(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
295
+ return { path: join4(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
189
296
  }
190
297
  }
191
298
  var makeCommand = defineCommand2({
@@ -205,7 +312,7 @@ var makeCommand = defineCommand2({
205
312
  }
206
313
  const root = resolve2(process.cwd(), args.root);
207
314
  const { path, contents } = plan(kind, args.name, root);
208
- if (existsSync2(path)) {
315
+ if (existsSync3(path)) {
209
316
  console.error(`
210
317
  \u2717 Already exists: ${path}
211
318
  `);
@@ -275,13 +382,15 @@ var mcpCommand = defineCommand3({
275
382
 
276
383
  // src/commands/migrate.ts
277
384
  import { createRequire } from "module";
278
- import { join as join4, resolve as resolve3 } from "path";
385
+ import { join as join5, resolve as resolve3 } from "path";
279
386
  import { pathToFileURL } from "url";
280
387
  import { defineCommand as defineCommand4 } from "citty";
281
388
  var migrateCommand = defineCommand4({
282
389
  meta: { name: "migrate", description: "Apply pending SQL migrations (db/migrations/*.sql)" },
283
390
  args: {
284
- db: { type: "string", description: "SQLite file path", default: "data.db" },
391
+ db: { type: "string", description: "SQLite file path (libSQL)", default: "data.db" },
392
+ driver: { type: "string", description: "sqlite | postgres | pglite", default: "sqlite" },
393
+ url: { type: "string", description: "Connection URL (postgres) \u2014 overrides --db" },
285
394
  dir: { type: "string", description: "Migrations directory", default: "db/migrations" },
286
395
  root: { type: "string", description: "Project root", default: "." }
287
396
  },
@@ -289,14 +398,16 @@ var migrateCommand = defineCommand4({
289
398
  const root = resolve3(process.cwd(), args.root);
290
399
  let data;
291
400
  try {
292
- const require2 = createRequire(join4(root, "package.json"));
401
+ const require2 = createRequire(join5(root, "package.json"));
293
402
  data = await import(pathToFileURL(require2.resolve("@apex-stack/data")).href);
294
403
  } catch {
295
404
  console.error("\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n");
296
405
  process.exit(1);
297
406
  }
298
- const { sqlite } = data.createDb(resolve3(root, args.db));
299
- const applied = data.applyMigrations(sqlite, resolve3(root, args.dir));
407
+ const config = args.driver === "postgres" ? { driver: "postgres", url: args.url } : args.driver === "pglite" ? { driver: "pglite", dir: args.url } : resolve3(root, args.db);
408
+ const handle = await data.createDb(config);
409
+ const applied = await data.applyMigrations(handle, resolve3(root, args.dir));
410
+ await handle.close();
300
411
  console.log(
301
412
  applied.length ? `
302
413
  \u2713 Applied ${applied.length} migration(s): ${applied.join(", ")}
@@ -305,8 +416,126 @@ var migrateCommand = defineCommand4({
305
416
  }
306
417
  });
307
418
 
419
+ // src/commands/start.ts
420
+ import { existsSync as existsSync5 } from "fs";
421
+ import { join as join7, resolve as resolve4 } from "path";
422
+ import { defineCommand as defineCommand5 } from "citty";
423
+
424
+ // src/prod/server.ts
425
+ import { createServer as createHttpServer } from "http";
426
+ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "fs";
427
+ import { join as join6 } from "path";
428
+ import { pathToFileURL as pathToFileURL2 } from "url";
429
+ import {
430
+ createApp,
431
+ defineEventHandler,
432
+ getRequestURL,
433
+ setResponseHeader,
434
+ setResponseStatus,
435
+ toNodeListener
436
+ } from "h3";
437
+ var MIME = {
438
+ ".js": "text/javascript",
439
+ ".mjs": "text/javascript",
440
+ ".css": "text/css",
441
+ ".html": "text/html",
442
+ ".json": "application/json",
443
+ ".svg": "image/svg+xml",
444
+ ".png": "image/png",
445
+ ".jpg": "image/jpeg",
446
+ ".ico": "image/x-icon",
447
+ ".woff2": "font/woff2"
448
+ };
449
+ async function startProdServer(options) {
450
+ const dir = options.dir;
451
+ const port = options.port ?? 3e3;
452
+ const manifest = JSON.parse(readFileSync2(join6(dir, "apex-manifest.json"), "utf8"));
453
+ const importServer = (relFile) => import(pathToFileURL2(join6(dir, "server", relFile)).href);
454
+ const registry = {};
455
+ let componentCss = "";
456
+ for (const [name, file] of Object.entries(manifest.components)) {
457
+ const mod = await importServer(file);
458
+ registry[name] = { template: mod.template, rootXData: mod.rootXData, scopeId: mod.scopeId };
459
+ if (mod.css) componentCss += `${mod.css}
460
+ `;
461
+ }
462
+ const apiEntries = [];
463
+ for (const { name, serverFile } of manifest.api) {
464
+ const mod = await importServer(serverFile);
465
+ apiEntries.push(...expandApiModule(name, mod.default));
466
+ }
467
+ const serverFileFor = new Map(manifest.routes.map((r) => [r.pageId, r.serverFile]));
468
+ const loadModule = (id) => importServer(serverFileFor.get(id));
469
+ const app = createApp();
470
+ app.use(
471
+ defineEventHandler((event) => {
472
+ if (event.method !== "GET") return;
473
+ const path = decodeURIComponent(getRequestURL(event).pathname);
474
+ if (path === "/" || path.startsWith("/api") || path === "/mcp") return;
475
+ const file = join6(dir, path);
476
+ if (!file.startsWith(dir) || !existsSync4(file) || !statSync(file).isFile()) return;
477
+ const ext = path.slice(path.lastIndexOf("."));
478
+ setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
479
+ if (path.startsWith("/assets/")) setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
480
+ return readFileSync2(file);
481
+ })
482
+ );
483
+ if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
484
+ if (hasMcpRoutes(apiEntries)) app.use("/mcp", createMcpHandler(apiEntries));
485
+ app.use(
486
+ defineEventHandler(async (event) => {
487
+ const url = getRequestURL(event).pathname;
488
+ const matched = matchRoute(manifest.routes, url);
489
+ if (!matched) {
490
+ setResponseStatus(event, 404);
491
+ setResponseHeader(event, "Content-Type", "text/html");
492
+ return `<!DOCTYPE html><h1>404 \u2014 ${url}</h1>`;
493
+ }
494
+ const route = manifest.routes.find((r) => r.pageId === matched.pageId);
495
+ const render = manifest.islands ? renderIslandsPage : renderPage;
496
+ const html = await render({
497
+ loadModule,
498
+ pageId: matched.pageId,
499
+ params: matched.params,
500
+ url,
501
+ registry,
502
+ componentCss,
503
+ clientHref: route?.clientHref
504
+ });
505
+ setResponseHeader(event, "Content-Type", "text/html");
506
+ return html;
507
+ })
508
+ );
509
+ const server = createHttpServer(toNodeListener(app));
510
+ await new Promise((resolve6) => server.listen(port, resolve6));
511
+ return { server, port };
512
+ }
513
+
514
+ // src/commands/start.ts
515
+ var startCommand = defineCommand5({
516
+ meta: { name: "start", description: "Run a production build (from `apex build --server`)" },
517
+ args: {
518
+ dir: { type: "positional", required: false, description: "Build directory", default: "dist" },
519
+ port: { type: "string", description: "Port to listen on", default: "3000" }
520
+ },
521
+ async run({ args }) {
522
+ const dir = resolve4(process.cwd(), args.dir);
523
+ if (!existsSync5(join7(dir, "apex-manifest.json"))) {
524
+ console.error(`
525
+ No build found in ${args.dir}/. Run \`apex build --server\` first.
526
+ `);
527
+ process.exit(1);
528
+ }
529
+ const { port } = await startProdServer({ dir, port: Number(args.port) });
530
+ console.log(`
531
+ \x1B[36mApex JS\x1B[0m production server
532
+ \u2192 http://localhost:${port}
533
+ `);
534
+ }
535
+ });
536
+
308
537
  // src/cli.ts
309
- var dev = defineCommand5({
538
+ var dev = defineCommand6({
310
539
  meta: { name: "dev", description: "Start the Apex JS development server" },
311
540
  args: {
312
541
  root: { type: "positional", required: false, description: "Project root", default: "." },
@@ -314,7 +543,7 @@ var dev = defineCommand5({
314
543
  islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
315
544
  },
316
545
  async run({ args }) {
317
- const root = resolve4(process.cwd(), args.root);
546
+ const root = resolve5(process.cwd(), args.root);
318
547
  const port = Number(args.port);
319
548
  const { port: actual } = await startDevServer({ root, port, islands: args.islands });
320
549
  console.log(`
@@ -323,7 +552,7 @@ var dev = defineCommand5({
323
552
  `);
324
553
  }
325
554
  });
326
- var main = defineCommand5({
555
+ var main = defineCommand6({
327
556
  meta: {
328
557
  name: "apex",
329
558
  description: "The full-stack meta-framework for Alpine.js"
@@ -331,6 +560,7 @@ var main = defineCommand5({
331
560
  subCommands: {
332
561
  dev,
333
562
  build: buildCommand,
563
+ start: startCommand,
334
564
  make: makeCommand,
335
565
  migrate: migrateCommand,
336
566
  mcp: mcpCommand
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isApexResource,
3
3
  renderPage,
4
4
  startDevServer
5
- } from "./chunk-GC3WNE6M.js";
5
+ } from "./chunk-SGEM3N42.js";
6
6
 
7
7
  // src/api/defineRoute.ts
8
8
  function defineApexRoute(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",