@apex-stack/core 0.1.1 → 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.
@@ -20,11 +20,18 @@ async function renderPage(opts) {
20
20
  body: html,
21
21
  island: stateIsland(mod.componentId, loaderData),
22
22
  css: mod.css + (opts.componentCss ?? ""),
23
- pageId: opts.pageId
23
+ pageId: opts.pageId,
24
+ clientHref: opts.clientHref
24
25
  });
25
26
  return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
26
27
  }
27
- function shell({ body, island, css, pageId }) {
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>`;
28
35
  return `<!DOCTYPE html>
29
36
  <html lang="en">
30
37
  <head>
@@ -36,12 +43,7 @@ function shell({ body, island, css, pageId }) {
36
43
  <body>
37
44
  ${body}
38
45
  ${island}
39
- <script type="module">
40
- import Alpine from 'alpinejs'
41
- import ${JSON.stringify(pageId)}
42
- window.Alpine = Alpine
43
- Alpine.start()
44
- </script>
46
+ ${clientScript}
45
47
  </body>
46
48
  </html>`;
47
49
  }
@@ -80,6 +82,18 @@ function sanitizeName(name) {
80
82
  function entryFor(pattern, method, mcpName, route) {
81
83
  return { pattern, segments: toSegments(pattern), method, mcpName, route };
82
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
+ }
83
97
  async function loadApiRoutes(root, loadModule) {
84
98
  const dir = join(root, "server", "api");
85
99
  if (!existsSync(dir)) return [];
@@ -87,14 +101,7 @@ async function loadApiRoutes(root, loadModule) {
87
101
  for (const file of readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f))) {
88
102
  const name = file.replace(/\.(ts|js|mjs)$/, "");
89
103
  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
- }
104
+ entries.push(...expandApiModule(name, def));
98
105
  }
99
106
  return entries;
100
107
  }
@@ -408,6 +415,14 @@ function notFoundPage(url, routes) {
408
415
 
409
416
  export {
410
417
  isApexResource,
418
+ expandApiModule,
419
+ createApiHandler,
420
+ hasMcpRoutes,
421
+ createMcpHandler,
422
+ loadComponents,
423
+ renderIslandsPage,
424
+ scanPages,
425
+ matchRoute,
411
426
  renderPage,
412
427
  startDevServer
413
428
  };
package/dist/cli.js CHANGED
@@ -1,15 +1,248 @@
1
1
  import {
2
+ createApiHandler,
3
+ createMcpHandler,
4
+ expandApiModule,
5
+ hasMcpRoutes,
6
+ loadComponents,
7
+ matchRoute,
8
+ renderIslandsPage,
9
+ renderPage,
10
+ scanPages,
2
11
  startDevServer
3
- } from "./chunk-XB5ZYPPE.js";
12
+ } from "./chunk-SGEM3N42.js";
4
13
 
5
14
  // src/cli.ts
6
- import { resolve as resolve3 } from "path";
7
- import { defineCommand as defineCommand4, runMain } from "citty";
15
+ import { resolve as resolve5 } from "path";
16
+ import { defineCommand as defineCommand6, runMain } from "citty";
8
17
 
9
- // src/commands/make.ts
10
- import { existsSync, mkdirSync, writeFileSync } from "fs";
11
- import { dirname, join, resolve } from "path";
18
+ // src/commands/build.ts
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";
12
22
  import { defineCommand } from "citty";
23
+ import { createServer as createViteServer } from "vite";
24
+
25
+ // src/build/buildClient.ts
26
+ import { readFileSync } from "fs";
27
+ import { join } from "path";
28
+ import { apex } from "@apex-stack/vite";
29
+ import { build } from "vite";
30
+ var VIRT = "virtual:apex-client:";
31
+ function entryName(pageId) {
32
+ return pageId.replace(/^\/pages\//, "").replace(/\.alpine$/, "").replace(/[^a-zA-Z0-9]+/g, "_");
33
+ }
34
+ async function buildClient(root, routes, outDir) {
35
+ const input = {};
36
+ for (const r of routes) input[entryName(r.pageId)] = `${VIRT}${r.pageId}`;
37
+ const entryPlugin = {
38
+ name: "apex:client-entries",
39
+ resolveId(id) {
40
+ if (id.startsWith(VIRT)) return `\0${id}`;
41
+ },
42
+ load(id) {
43
+ if (id.startsWith(`\0${VIRT}`)) {
44
+ const pageId = id.slice(`\0${VIRT}`.length);
45
+ return [
46
+ `import Alpine from 'alpinejs'`,
47
+ `import ${JSON.stringify(pageId)}`,
48
+ `window.Alpine = Alpine`,
49
+ `Alpine.start()`
50
+ ].join("\n");
51
+ }
52
+ }
53
+ };
54
+ await build({
55
+ root,
56
+ logLevel: "warn",
57
+ plugins: [apex({ clientRuntime: "@apex-stack/core/client" }), entryPlugin],
58
+ build: {
59
+ outDir,
60
+ emptyOutDir: false,
61
+ manifest: true,
62
+ rollupOptions: { input }
63
+ }
64
+ });
65
+ const manifest = JSON.parse(readFileSync(join(outDir, ".vite", "manifest.json"), "utf8"));
66
+ const hrefs = /* @__PURE__ */ new Map();
67
+ for (const r of routes) {
68
+ const virt = `${VIRT}${r.pageId}`;
69
+ const entry = Object.values(manifest).find(
70
+ (m) => m.isEntry && (m.src === virt || m.src === `\0${virt}`)
71
+ );
72
+ if (entry) hrefs.set(r.pageId, `/${entry.file}`);
73
+ }
74
+ return hrefs;
75
+ }
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
+
136
+ // src/commands/build.ts
137
+ function outFile(pattern) {
138
+ const clean = pattern.replace(/^\//, "");
139
+ return clean === "" ? "index.html" : `${clean}/index.html`;
140
+ }
141
+ var buildCommand = defineCommand({
142
+ meta: { name: "build", description: "Prerender pages to deployable HTML + client bundles" },
143
+ args: {
144
+ root: { type: "positional", required: false, description: "Project root", default: "." },
145
+ outDir: { type: "string", description: "Output directory", default: "dist" },
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 }
148
+ },
149
+ async run({ args }) {
150
+ const root = resolve(process.cwd(), args.root);
151
+ const outDir = resolve(root, args.outDir);
152
+ rmSync(outDir, { recursive: true, force: true });
153
+ const routes = scanPages(root);
154
+ const staticRoutes = routes.filter((r) => !r.isDynamic);
155
+ const dynamic = routes.filter((r) => r.isDynamic);
156
+ if (args.server) {
157
+ return buildServerTarget(root, outDir, args.outDir, routes);
158
+ }
159
+ const hrefs = args.islands ? /* @__PURE__ */ new Map() : await buildClient(root, staticRoutes, outDir);
160
+ const vite = await createViteServer({
161
+ root,
162
+ appType: "custom",
163
+ server: { middlewareMode: true },
164
+ plugins: [apex3({ clientRuntime: "@apex-stack/core/client" })]
165
+ });
166
+ try {
167
+ const { registry, css: componentCss } = await loadComponents(
168
+ root,
169
+ (id) => vite.ssrLoadModule(id)
170
+ );
171
+ for (const route of staticRoutes) {
172
+ const common = {
173
+ loadModule: (id) => vite.ssrLoadModule(id),
174
+ pageId: route.pageId,
175
+ url: route.pattern,
176
+ registry,
177
+ componentCss
178
+ };
179
+ const html = args.islands ? await renderIslandsPage(common) : await renderPage({ ...common, clientHref: hrefs.get(route.pageId) });
180
+ const dest = join3(outDir, outFile(route.pattern));
181
+ mkdirSync(dirname(dest), { recursive: true });
182
+ writeFileSync(dest, html);
183
+ console.log(` \u2713 ${route.pattern} \u2192 ${outFile(route.pattern)}`);
184
+ }
185
+ const pub = join3(root, "public");
186
+ if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
187
+ console.log(
188
+ `
189
+ Built ${staticRoutes.length} page(s) \u2192 ${args.outDir}/` + (args.islands ? " (islands / static-first)" : " (prerendered + hydrated)")
190
+ );
191
+ if (dynamic.length) {
192
+ console.log(
193
+ ` Skipped ${dynamic.length} dynamic route(s): ${dynamic.map((r) => r.pattern).join(", ")} (server target on the roadmap).`
194
+ );
195
+ }
196
+ console.log();
197
+ } finally {
198
+ await vite.close();
199
+ }
200
+ }
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
+ }
241
+
242
+ // src/commands/make.ts
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";
245
+ import { defineCommand as defineCommand2 } from "citty";
13
246
  function pageTemplate(name) {
14
247
  return `<script server lang="ts">
15
248
  export function loader() {
@@ -55,14 +288,14 @@ export default defineApexRoute({
55
288
  function plan(kind, name, root) {
56
289
  switch (kind) {
57
290
  case "page":
58
- return { path: join(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
291
+ return { path: join4(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
59
292
  case "component":
60
- return { path: join(root, "components", `${name}.alpine`), contents: componentTemplate() };
293
+ return { path: join4(root, "components", `${name}.alpine`), contents: componentTemplate() };
61
294
  case "api":
62
- return { path: join(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
295
+ return { path: join4(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
63
296
  }
64
297
  }
65
- var makeCommand = defineCommand({
298
+ var makeCommand = defineCommand2({
66
299
  meta: { name: "make", description: "Generate a page, component, or API route" },
67
300
  args: {
68
301
  kind: { type: "positional", required: true, description: "page | component | api" },
@@ -77,16 +310,16 @@ var makeCommand = defineCommand({
77
310
  `);
78
311
  process.exit(1);
79
312
  }
80
- const root = resolve(process.cwd(), args.root);
313
+ const root = resolve2(process.cwd(), args.root);
81
314
  const { path, contents } = plan(kind, args.name, root);
82
- if (existsSync(path)) {
315
+ if (existsSync3(path)) {
83
316
  console.error(`
84
317
  \u2717 Already exists: ${path}
85
318
  `);
86
319
  process.exit(1);
87
320
  }
88
- mkdirSync(dirname(path), { recursive: true });
89
- writeFileSync(path, contents);
321
+ mkdirSync2(dirname2(path), { recursive: true });
322
+ writeFileSync2(path, contents);
90
323
  console.log(`
91
324
  \u2713 Created ${path.replace(`${root}/`, "")}
92
325
  `);
@@ -94,8 +327,8 @@ var makeCommand = defineCommand({
94
327
  });
95
328
 
96
329
  // src/commands/mcp.ts
97
- import { defineCommand as defineCommand2 } from "citty";
98
- var mcpCommand = defineCommand2({
330
+ import { defineCommand as defineCommand3 } from "citty";
331
+ var mcpCommand = defineCommand3({
99
332
  meta: { name: "mcp", description: "Inspect the local MCP server (list or call tools)" },
100
333
  args: {
101
334
  url: { type: "string", description: "MCP endpoint URL", default: "http://localhost:3000/mcp" },
@@ -149,28 +382,32 @@ var mcpCommand = defineCommand2({
149
382
 
150
383
  // src/commands/migrate.ts
151
384
  import { createRequire } from "module";
152
- import { join as join2, resolve as resolve2 } from "path";
385
+ import { join as join5, resolve as resolve3 } from "path";
153
386
  import { pathToFileURL } from "url";
154
- import { defineCommand as defineCommand3 } from "citty";
155
- var migrateCommand = defineCommand3({
387
+ import { defineCommand as defineCommand4 } from "citty";
388
+ var migrateCommand = defineCommand4({
156
389
  meta: { name: "migrate", description: "Apply pending SQL migrations (db/migrations/*.sql)" },
157
390
  args: {
158
- 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" },
159
394
  dir: { type: "string", description: "Migrations directory", default: "db/migrations" },
160
395
  root: { type: "string", description: "Project root", default: "." }
161
396
  },
162
397
  async run({ args }) {
163
- const root = resolve2(process.cwd(), args.root);
398
+ const root = resolve3(process.cwd(), args.root);
164
399
  let data;
165
400
  try {
166
- const require2 = createRequire(join2(root, "package.json"));
401
+ const require2 = createRequire(join5(root, "package.json"));
167
402
  data = await import(pathToFileURL(require2.resolve("@apex-stack/data")).href);
168
403
  } catch {
169
404
  console.error("\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n");
170
405
  process.exit(1);
171
406
  }
172
- const { sqlite } = data.createDb(resolve2(root, args.db));
173
- const applied = data.applyMigrations(sqlite, resolve2(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();
174
411
  console.log(
175
412
  applied.length ? `
176
413
  \u2713 Applied ${applied.length} migration(s): ${applied.join(", ")}
@@ -179,8 +416,126 @@ var migrateCommand = defineCommand3({
179
416
  }
180
417
  });
181
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
+
182
537
  // src/cli.ts
183
- var dev = defineCommand4({
538
+ var dev = defineCommand6({
184
539
  meta: { name: "dev", description: "Start the Apex JS development server" },
185
540
  args: {
186
541
  root: { type: "positional", required: false, description: "Project root", default: "." },
@@ -188,7 +543,7 @@ var dev = defineCommand4({
188
543
  islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
189
544
  },
190
545
  async run({ args }) {
191
- const root = resolve3(process.cwd(), args.root);
546
+ const root = resolve5(process.cwd(), args.root);
192
547
  const port = Number(args.port);
193
548
  const { port: actual } = await startDevServer({ root, port, islands: args.islands });
194
549
  console.log(`
@@ -197,11 +552,18 @@ var dev = defineCommand4({
197
552
  `);
198
553
  }
199
554
  });
200
- var main = defineCommand4({
555
+ var main = defineCommand6({
201
556
  meta: {
202
557
  name: "apex",
203
558
  description: "The full-stack meta-framework for Alpine.js"
204
559
  },
205
- subCommands: { dev, make: makeCommand, migrate: migrateCommand, mcp: mcpCommand }
560
+ subCommands: {
561
+ dev,
562
+ build: buildCommand,
563
+ start: startCommand,
564
+ make: makeCommand,
565
+ migrate: migrateCommand,
566
+ mcp: mcpCommand
567
+ }
206
568
  });
207
569
  runMain(main);
package/dist/index.d.ts CHANGED
@@ -121,6 +121,9 @@ interface RenderPageOptions {
121
121
  componentCss?: string;
122
122
  /** Post-process the shell HTML (dev: vite.transformIndexHtml). */
123
123
  transformHtml?: (url: string, html: string) => string | Promise<string>;
124
+ /** In a production build, the href of the built client bundle for this page.
125
+ * When set, the shell references it instead of the inline dev module. */
126
+ clientHref?: string;
124
127
  }
125
128
  /**
126
129
  * The framework's render seam — deliberately dev-server-agnostic so it can move
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isApexResource,
3
3
  renderPage,
4
4
  startDevServer
5
- } from "./chunk-XB5ZYPPE.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.1",
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",
@@ -40,8 +40,8 @@
40
40
  "h3": "^1.13.0",
41
41
  "vite": "^6.0.7",
42
42
  "zod": "^4.4.3",
43
- "@apex-stack/vite": "0.1.1",
44
- "@apex-stack/kit": "0.1.1"
43
+ "@apex-stack/kit": "0.1.1",
44
+ "@apex-stack/vite": "0.1.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "alpinejs": "^3.14.0"