@apex-stack/core 0.1.19 → 0.2.0

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.
Files changed (40) hide show
  1. package/dist/{build-J47A3B4Y.js → build-VHS6KZBK.js} +91 -27
  2. package/dist/chunk-2C2HRLIY.js +18 -0
  3. package/dist/chunk-CHBSGOB3.js +42 -0
  4. package/dist/{chunk-JWYNLP4L.js → chunk-HCNNKT4A.js} +51 -9
  5. package/dist/chunk-JLIAISWM.js +48 -0
  6. package/dist/{chunk-XSN6NDWP.js → chunk-XDKJO6ZC.js} +159 -25
  7. package/dist/cli.js +65 -24
  8. package/dist/client.d.ts +1 -1
  9. package/dist/client.js +2 -1
  10. package/dist/{dev-OCVQRCCE.js → dev-G7HPP6KW.js} +6 -2
  11. package/dist/index.d.ts +88 -5
  12. package/dist/index.js +12 -4
  13. package/dist/{make-JAW22LQZ.js → make-VAYO5GWA.js} +71 -5
  14. package/dist/{mcp-DL4J6JFJ.js → mcp-CH7L4GF3.js} +1 -1
  15. package/dist/{migrate-NOGFOFV2.js → migrate-X6LIHMIE.js} +3 -1
  16. package/dist/{server-L3V34B5X.js → server-PTHGOE42.js} +63 -19
  17. package/dist/{start-AUJJ7HAY.js → start-3O3E43PT.js} +47 -10
  18. package/dist/upgrade-WC5F5FKY.js +168 -0
  19. package/package.json +5 -4
  20. package/templates/default/.env.example +9 -0
  21. package/templates/default/README.md +63 -17
  22. package/templates/default/_gitignore +5 -0
  23. package/templates/default/apex.config.ts +22 -0
  24. package/templates/default/components/Counter.alpine +15 -0
  25. package/templates/default/composables/useToggle.ts +14 -0
  26. package/templates/default/db/README.md +18 -0
  27. package/templates/default/layouts/default.alpine +16 -0
  28. package/templates/default/package.json +11 -2
  29. package/templates/default/pages/index.alpine +23 -92
  30. package/templates/default/public/.gitkeep +0 -0
  31. package/templates/default/server/api/hello.ts +10 -2
  32. package/templates/default/services/GreetingService.ts +12 -0
  33. package/templates/default/shared/types.ts +11 -0
  34. package/templates/default/stores/ui.ts +9 -0
  35. package/templates/default/tests/greeting.test.ts +12 -0
  36. package/templates/default/tsconfig.json +15 -0
  37. package/templates/default/vitest.config.ts +7 -0
  38. package/vscode/apex-alpine.vsix +0 -0
  39. package/dist/chunk-HRJTOSYH.js +0 -8
  40. package/dist/chunk-MZVLRU3R.js +0 -15
@@ -1,6 +1,93 @@
1
1
  import {
2
- isApexStore
3
- } from "./chunk-MZVLRU3R.js";
2
+ clientConfigScript,
3
+ isApexStore,
4
+ setRuntimeConfig
5
+ } from "./chunk-JLIAISWM.js";
6
+
7
+ // src/config/resolve.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ function parseEnvFile(text) {
11
+ const out = {};
12
+ for (const rawLine of text.split(/\r?\n/)) {
13
+ const line = rawLine.trim();
14
+ if (!line || line[0] === "#") continue;
15
+ const eq = line.indexOf("=");
16
+ if (eq < 0) continue;
17
+ const key = line.slice(0, eq).trim().replace(/^export\s+/, "");
18
+ let val = line.slice(eq + 1).trim();
19
+ if (val[0] === '"' && val.endsWith('"') || val[0] === "'" && val.endsWith("'")) {
20
+ val = val.slice(1, -1);
21
+ }
22
+ out[key] = val;
23
+ }
24
+ return out;
25
+ }
26
+ function loadDotenv(root, mode = process.env.NODE_ENV || "development") {
27
+ const merged = {};
28
+ for (const file of [".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`]) {
29
+ const p = join(root, file);
30
+ if (existsSync(p)) Object.assign(merged, parseEnvFile(readFileSync(p, "utf8")));
31
+ }
32
+ for (const [k, v] of Object.entries(merged)) {
33
+ if (process.env[k] === void 0) process.env[k] = v;
34
+ }
35
+ return { ...merged, ...process.env };
36
+ }
37
+ function screamingSnake(key) {
38
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
39
+ }
40
+ function coerce(def, raw) {
41
+ if (typeof def === "number") {
42
+ const n = Number(raw);
43
+ return Number.isNaN(n) ? def : n;
44
+ }
45
+ if (typeof def === "boolean") return raw === "true" || raw === "1";
46
+ return raw;
47
+ }
48
+ function applyOverrides(node, env, prefix) {
49
+ for (const [key, val] of Object.entries(node)) {
50
+ if (key === "public" && prefix === "APEX_" && val && typeof val === "object") {
51
+ applyOverrides(val, env, "APEX_PUBLIC_");
52
+ continue;
53
+ }
54
+ if (val && typeof val === "object" && !Array.isArray(val)) {
55
+ applyOverrides(val, env, `${prefix}${screamingSnake(key)}_`);
56
+ continue;
57
+ }
58
+ const envKey = `${prefix}${screamingSnake(key)}`;
59
+ if (env[envKey] !== void 0) node[key] = coerce(val, env[envKey]);
60
+ }
61
+ }
62
+ function applyEnvToRuntimeConfig(runtimeConfig, root) {
63
+ const env = loadDotenv(root);
64
+ if (!runtimeConfig.public) runtimeConfig.public = {};
65
+ applyOverrides(runtimeConfig, env, "APEX_");
66
+ setRuntimeConfig(runtimeConfig);
67
+ return runtimeConfig;
68
+ }
69
+ async function resolveApexConfig(root, loadModule) {
70
+ loadDotenv(root);
71
+ let config = {};
72
+ const file = ["apex.config.ts", "apex.config.js", "apex.config.mjs"].find(
73
+ (f) => existsSync(join(root, f))
74
+ );
75
+ if (file) {
76
+ try {
77
+ config = (await loadModule(`/${file}`)).default ?? {};
78
+ } catch {
79
+ config = {};
80
+ }
81
+ }
82
+ const runtimeConfig = structuredClone(config.runtimeConfig ?? {});
83
+ if (!runtimeConfig.public) runtimeConfig.public = {};
84
+ applyEnvToRuntimeConfig(runtimeConfig, root);
85
+ return {
86
+ config,
87
+ runtimeConfig,
88
+ publicConfig: runtimeConfig.public ?? {}
89
+ };
90
+ }
4
91
 
5
92
  // src/islands/render.ts
6
93
  import { renderIslands } from "@apex-stack/kit";
@@ -44,7 +131,12 @@ document.querySelectorAll('[data-apex-island]').forEach(function (el) {
44
131
  );
45
132
  async function renderIslandsPage(opts) {
46
133
  const mod = await opts.loadModule(opts.pageId);
47
- const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
134
+ const loaderData = await mod.loader({
135
+ params: opts.params ?? {},
136
+ url: opts.url,
137
+ config: opts.runtimeConfig ?? { public: {} },
138
+ locals: opts.locals ?? {}
139
+ }) ?? {};
48
140
  const { html, hydratingCount } = renderIslands(
49
141
  mod.template,
50
142
  loaderData,
@@ -53,6 +145,8 @@ async function renderIslandsPage(opts) {
53
145
  );
54
146
  const loaderScript = hydratingCount > 0 ? `
55
147
  <script type="module">${ISLAND_LOADER}</script>` : "";
148
+ const configScript = hydratingCount > 0 ? `
149
+ ${clientConfigScript(opts.publicConfig ?? {})}` : "";
56
150
  const doc = `<!DOCTYPE html>
57
151
  <html lang="en">
58
152
  <head>
@@ -62,27 +156,27 @@ async function renderIslandsPage(opts) {
62
156
  <style>${mod.css}${opts.componentCss ?? ""}</style>
63
157
  </head>
64
158
  <body>
65
- ${html}${loaderScript}
159
+ ${html}${configScript}${loaderScript}
66
160
  </body>
67
161
  </html>`;
68
162
  return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
69
163
  }
70
164
 
71
165
  // src/routing/router.ts
72
- import { existsSync, readdirSync, statSync } from "fs";
73
- import { join, relative, sep } from "path";
166
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
167
+ import { join as join2, relative, sep } from "path";
74
168
  function walkAlpine(dir) {
75
169
  const out = [];
76
170
  for (const entry of readdirSync(dir)) {
77
- const abs = join(dir, entry);
171
+ const abs = join2(dir, entry);
78
172
  if (statSync(abs).isDirectory()) out.push(...walkAlpine(abs));
79
173
  else if (entry.endsWith(".alpine")) out.push(abs);
80
174
  }
81
175
  return out;
82
176
  }
83
177
  function scanPages(root) {
84
- const dir = join(root, "pages");
85
- if (!existsSync(dir)) return [];
178
+ const dir = join2(root, "pages");
179
+ if (!existsSync2(dir)) return [];
86
180
  const routes = walkAlpine(dir).map((abs) => {
87
181
  const rel = relative(dir, abs).split(sep).join("/");
88
182
  const pageId = `/pages/${rel}`;
@@ -146,11 +240,11 @@ function matchRoute(routes, url) {
146
240
  }
147
241
 
148
242
  // src/stores/loader.ts
149
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
150
- import { join as join2 } from "path";
243
+ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
244
+ import { join as join3 } from "path";
151
245
  async function loadStores(root, loadModule) {
152
- const dir = join2(root, "stores");
153
- if (!existsSync2(dir)) return [];
246
+ const dir = join3(root, "stores");
247
+ if (!existsSync3(dir)) return [];
154
248
  const out = [];
155
249
  for (const file of readdirSync2(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"))) {
156
250
  const id = `/stores/${file}`;
@@ -176,7 +270,11 @@ function storesInitialState(stores) {
176
270
  }
177
271
 
178
272
  // src/dev/renderPage.ts
179
- import { renderComponent, renderFragment, stateIsland } from "@apex-stack/kit";
273
+ import {
274
+ renderComponent,
275
+ renderFragment,
276
+ stateIsland
277
+ } from "@apex-stack/kit";
180
278
  function escAttr(s) {
181
279
  return String(s).replace(
182
280
  /[&<>"]/g,
@@ -186,17 +284,44 @@ function escAttr(s) {
186
284
  function renderHead(head) {
187
285
  const parts = [`<title>${head?.title ? escAttr(head.title) : "Apex JS"}</title>`];
188
286
  for (const m of head?.meta ?? []) {
189
- parts.push(`<meta ${Object.entries(m).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`);
287
+ parts.push(
288
+ `<meta ${Object.entries(m).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`
289
+ );
190
290
  }
191
291
  for (const l of head?.link ?? []) {
192
- parts.push(`<link ${Object.entries(l).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`);
292
+ parts.push(
293
+ `<link ${Object.entries(l).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`
294
+ );
193
295
  }
194
296
  return parts.join("\n ");
195
297
  }
196
298
  async function renderPage(opts) {
197
- const mod = await opts.loadModule(opts.pageId);
198
- const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
199
- const head = mod.head ? await mod.head({ data: loaderData, params: opts.params ?? {}, url: opts.url }) : void 0;
299
+ let mod = await opts.loadModule(opts.pageId);
300
+ const cfg = opts.runtimeConfig ?? { public: {} };
301
+ const locals = opts.locals ?? {};
302
+ let loaderData;
303
+ try {
304
+ loaderData = await mod.loader({
305
+ params: opts.params ?? {},
306
+ url: opts.url,
307
+ config: cfg,
308
+ locals
309
+ }) ?? {};
310
+ } catch (err) {
311
+ if (!opts.errorPageId) throw err;
312
+ mod = await opts.loadModule(opts.errorPageId);
313
+ const e = err;
314
+ loaderData = {
315
+ error: { message: e.message ?? "Something went wrong", statusCode: e.statusCode ?? 500 }
316
+ };
317
+ }
318
+ const head = mod.head ? await mod.head({
319
+ data: loaderData,
320
+ params: opts.params ?? {},
321
+ url: opts.url,
322
+ config: cfg,
323
+ locals
324
+ }) : void 0;
200
325
  const stores = opts.stores ?? [];
201
326
  const { html } = renderComponent({
202
327
  template: mod.template,
@@ -212,11 +337,15 @@ async function renderPage(opts) {
212
337
  const layoutName = mod.layout === false ? null : typeof mod.layout === "string" ? mod.layout : available.includes("default") ? "default" : null;
213
338
  let body = html;
214
339
  let layoutCss = "";
215
- if (layoutName && available.includes(layoutName)) {
216
- const layoutMod = await opts.loadModule(`/layouts/${layoutName}.alpine`);
340
+ const seen = /* @__PURE__ */ new Set();
341
+ let next = layoutName;
342
+ while (typeof next === "string" && available.includes(next) && !seen.has(next)) {
343
+ seen.add(next);
344
+ const layoutMod = await opts.loadModule(`/layouts/${next}.alpine`);
217
345
  const chrome = renderFragment(layoutMod.template, {}, layoutMod.scopeId, opts.registry);
218
- body = /<slot\b[^>]*>[\s\S]*?<\/slot>/.test(chrome) ? chrome.replace(/<slot\b[^>]*>[\s\S]*?<\/slot>/, () => html) : chrome + html;
219
- layoutCss = layoutMod.css;
346
+ body = /<slot\b[^>]*>[\s\S]*?<\/slot>/.test(chrome) ? chrome.replace(/<slot\b[^>]*>[\s\S]*?<\/slot>/, () => body) : chrome + body;
347
+ layoutCss += layoutMod.css;
348
+ next = layoutMod.layout;
220
349
  }
221
350
  const doc = shell({
222
351
  body,
@@ -226,7 +355,8 @@ async function renderPage(opts) {
226
355
  clientHref: opts.clientHref,
227
356
  storeIds: stores.map((s) => s.id),
228
357
  appCss: opts.appCss,
229
- headTags: renderHead(head)
358
+ headTags: renderHead(head),
359
+ configScript: clientConfigScript(opts.publicConfig ?? {})
230
360
  });
231
361
  return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
232
362
  }
@@ -238,7 +368,8 @@ function shell({
238
368
  clientHref,
239
369
  storeIds = [],
240
370
  appCss,
241
- headTags = "<title>Apex JS</title>"
371
+ headTags = "<title>Apex JS</title>",
372
+ configScript = ""
242
373
  }) {
243
374
  const storeImports = storeIds.map((id, i) => ` import __s${i} from ${JSON.stringify(id)}`).join("\n");
244
375
  const storeRegs = storeIds.map((_, i) => ` Alpine.store(__s${i}.name, __s${i}.factory())`).join("\n");
@@ -262,12 +393,15 @@ ${storeRegs ? `${storeRegs}
262
393
  <body>
263
394
  ${body}
264
395
  ${island}
396
+ ${configScript}
265
397
  ${clientScript}
266
398
  </body>
267
399
  </html>`;
268
400
  }
269
401
 
270
402
  export {
403
+ applyEnvToRuntimeConfig,
404
+ resolveApexConfig,
271
405
  renderIslandsPage,
272
406
  scanPages,
273
407
  matchRoute,
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ offerExtension
4
+ } from "./chunk-CHBSGOB3.js";
2
5
  import {
3
6
  VERSION,
4
7
  banner,
@@ -11,11 +14,21 @@ import { defineCommand as defineCommand2, runMain } from "citty";
11
14
 
12
15
  // src/commands/new.ts
13
16
  import { spawn, spawnSync } from "child_process";
14
- import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
17
+ import { cpSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "fs";
15
18
  import { basename, join, resolve } from "path";
16
19
  import { fileURLToPath } from "url";
17
20
  import { defineCommand } from "citty";
18
21
  var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
22
+ function substituteName(dir, name) {
23
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
24
+ const p = join(dir, entry.name);
25
+ if (entry.isDirectory()) substituteName(p, name);
26
+ else {
27
+ const txt = readFileSync(p, "utf8");
28
+ if (txt.includes("{{name}}")) writeFileSync(p, txt.replaceAll("{{name}}", name));
29
+ }
30
+ }
31
+ }
19
32
  function detectPackageManager() {
20
33
  const ua = process.env.npm_config_user_agent || "";
21
34
  if (ua.startsWith("pnpm")) return "pnpm";
@@ -37,9 +50,26 @@ function installAsync(pm, cwd) {
37
50
  var newCommand = defineCommand({
38
51
  meta: { name: "new", description: "Scaffold a new Apex JS app" },
39
52
  args: {
40
- dir: { type: "positional", required: false, description: "Target directory", default: "apex-app" },
41
- install: { type: "boolean", default: true, description: "Install dependencies (use --no-install to skip)" },
42
- git: { type: "boolean", default: true, description: "Initialize a git repository (use --no-git to skip)" }
53
+ dir: {
54
+ type: "positional",
55
+ required: false,
56
+ description: "Target directory",
57
+ default: "apex-app"
58
+ },
59
+ install: {
60
+ type: "boolean",
61
+ default: true,
62
+ description: "Install dependencies (use --no-install to skip)"
63
+ },
64
+ git: {
65
+ type: "boolean",
66
+ default: true,
67
+ description: "Initialize a git repository (use --no-git to skip)"
68
+ },
69
+ vscode: {
70
+ type: "boolean",
71
+ description: "Install the Apex VS Code extension (skip the interactive prompt)"
72
+ }
43
73
  },
44
74
  async run({ args }) {
45
75
  const dir = String(args.dir);
@@ -55,10 +85,7 @@ var newCommand = defineCommand({
55
85
  cpSync(TEMPLATE_DIR, target, { recursive: true });
56
86
  const gitignore = join(target, "_gitignore");
57
87
  if (existsSync(gitignore)) renameSync(gitignore, join(target, ".gitignore"));
58
- for (const rel of ["package.json", "README.md"]) {
59
- const file = join(target, rel);
60
- if (existsSync(file)) writeFileSync(file, readFileSync(file, "utf8").replaceAll("{{name}}", name));
61
- }
88
+ substituteName(target, name);
62
89
  log(` ${color.green("\u2713")} Created ${color.bold(dir)}`);
63
90
  const pm = detectPackageManager();
64
91
  let gitOk = false;
@@ -73,21 +100,29 @@ var newCommand = defineCommand({
73
100
  if (gitOk) log(` ${color.green("\u2713")} Initialized a git repository`);
74
101
  let installed = false;
75
102
  if (args.install) {
76
- const sp = spinner(`Installing dependencies with ${pm}\u2026 ${color.dim("(first run can take a minute)")}`);
103
+ const sp = spinner(
104
+ `Installing dependencies with ${pm}\u2026 ${color.dim("(first run can take a minute)")}`
105
+ );
77
106
  installed = await installAsync(pm, target);
78
107
  if (installed) sp.succeed(`Dependencies installed with ${pm}`);
79
108
  else sp.fail(`Install failed \u2014 run ${color.cyan(`${pm} install`)} inside ${dir}`);
80
109
  }
110
+ const ext = await offerExtension(args.vscode);
111
+ if (ext) log(` ${color.green("\u2713")} ${ext}`);
81
112
  const runPrefix = pm === "npm" ? "npm run" : pm;
82
113
  log(`
83
114
  ${color.bold("Next steps")}`);
84
115
  log(` ${color.cyan(`cd ${dir}`)}`);
85
116
  if (!installed) log(` ${color.cyan(pm === "yarn" ? "yarn" : `${pm} install`)}`);
86
117
  log(` ${color.cyan("apex dev")} ${color.gray("# \u2192 http://localhost:3000")}`);
87
- log(`
88
- ${color.gray("Not installed globally? Use")} ${color.cyan(`${runPrefix} dev`)}${color.gray(".")}`);
89
- log(` ${color.gray("Islands mode:")} ${color.cyan("apex dev --islands")}${color.gray(" \xB7 API routes are also MCP tools at /mcp.")}
90
- `);
118
+ log(
119
+ `
120
+ ${color.gray("Not installed globally? Use")} ${color.cyan(`${runPrefix} dev`)}${color.gray(".")}`
121
+ );
122
+ log(
123
+ ` ${color.gray("Islands mode:")} ${color.cyan("apex dev --islands")}${color.gray(" \xB7 API routes are also MCP tools at /mcp.")}
124
+ `
125
+ );
91
126
  }
92
127
  });
93
128
 
@@ -97,7 +132,8 @@ var COMMANDS = [
97
132
  ["dev", "Start the dev server (SSR + hydrate, API + MCP)"],
98
133
  ["build", "Build for production (static, islands, or server)"],
99
134
  ["start", "Run a production server build"],
100
- ["make", "Generate a page, component, or API route"],
135
+ ["make", "Generate a page, component, route, store, middleware\u2026"],
136
+ ["upgrade", "Adopt new scaffold defaults (non-destructive)"],
101
137
  ["migrate", "Apply pending database migrations"],
102
138
  ["mcp", "Inspect the MCP server \u2014 list or call tools"]
103
139
  ];
@@ -109,27 +145,32 @@ var main = defineCommand2({
109
145
  },
110
146
  subCommands: {
111
147
  new: newCommand,
112
- dev: () => import("./dev-OCVQRCCE.js").then((m) => m.devCommand),
113
- build: () => import("./build-J47A3B4Y.js").then((m) => m.buildCommand),
114
- start: () => import("./start-AUJJ7HAY.js").then((m) => m.startCommand),
115
- make: () => import("./make-JAW22LQZ.js").then((m) => m.makeCommand),
116
- migrate: () => import("./migrate-NOGFOFV2.js").then((m) => m.migrateCommand),
117
- mcp: () => import("./mcp-DL4J6JFJ.js").then((m) => m.mcpCommand)
148
+ dev: () => import("./dev-G7HPP6KW.js").then((m) => m.devCommand),
149
+ build: () => import("./build-VHS6KZBK.js").then((m) => m.buildCommand),
150
+ start: () => import("./start-3O3E43PT.js").then((m) => m.startCommand),
151
+ make: () => import("./make-VAYO5GWA.js").then((m) => m.makeCommand),
152
+ upgrade: () => import("./upgrade-WC5F5FKY.js").then((m) => m.upgradeCommand),
153
+ migrate: () => import("./migrate-X6LIHMIE.js").then((m) => m.migrateCommand),
154
+ mcp: () => import("./mcp-CH7L4GF3.js").then((m) => m.mcpCommand)
118
155
  },
119
156
  // Shown for a bare `apex` (no subcommand): the brand banner + a command menu.
120
157
  run({ rawArgs }) {
121
158
  if (rawArgs.length > 0) return;
122
159
  process.stdout.write(banner());
123
160
  const log = console.log;
124
- log(` ${color.bold("Usage")} ${color.gray("apex")} ${color.cyan("<command>")} ${color.gray("[options]")}
125
- `);
161
+ log(
162
+ ` ${color.bold("Usage")} ${color.gray("apex")} ${color.cyan("<command>")} ${color.gray("[options]")}
163
+ `
164
+ );
126
165
  log(` ${color.bold("Commands")}`);
127
166
  for (const [name, desc] of COMMANDS) {
128
167
  log(` ${color.cyan(`apex ${name}`.padEnd(13))} ${color.gray(desc)}`);
129
168
  }
130
- log(`
169
+ log(
170
+ `
131
171
  ${color.gray("Run")} ${color.cyan("apex <command> --help")} ${color.gray("for details.")}
132
- `);
172
+ `
173
+ );
133
174
  }
134
175
  });
135
176
  runMain(main);
package/dist/client.d.ts CHANGED
@@ -1 +1 @@
1
- export { registerApexComponent } from '@apex-stack/kit/client';
1
+ export { ActionOptions, ActionState, createAction, registerApexComponent } from '@apex-stack/kit/client';
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/client.ts
2
- import { registerApexComponent } from "@apex-stack/kit/client";
2
+ import { registerApexComponent, createAction } from "@apex-stack/kit/client";
3
3
  export {
4
+ createAction,
4
5
  registerApexComponent
5
6
  };
@@ -12,7 +12,11 @@ var devCommand = defineCommand({
12
12
  args: {
13
13
  root: { type: "positional", required: false, description: "Project root", default: "." },
14
14
  port: { type: "string", description: "Port to listen on", default: "3000" },
15
- islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
15
+ islands: {
16
+ type: "boolean",
17
+ description: "Render in islands mode (static-first)",
18
+ default: false
19
+ }
16
20
  },
17
21
  async run({ args }) {
18
22
  const root = resolve(process.cwd(), String(args.root));
@@ -20,7 +24,7 @@ var devCommand = defineCommand({
20
24
  process.stdout.write(banner());
21
25
  const sp = spinner(`Starting dev server${args.islands ? " (islands mode)" : ""}\u2026`);
22
26
  try {
23
- const { startDevServer } = await import("./server-L3V34B5X.js");
27
+ const { startDevServer } = await import("./server-PTHGOE42.js");
24
28
  const { port: actual } = await startDevServer({ root, port, islands: Boolean(args.islands) });
25
29
  sp.succeed("Dev server ready");
26
30
  ready([
package/dist/index.d.ts CHANGED
@@ -1,13 +1,48 @@
1
1
  import { ZodRawShape, z } from 'zod';
2
2
 
3
+ /** A runtime-config object. Top-level keys are private (server-only); `public` is exposed to the client. */
4
+ interface RuntimeConfig {
5
+ /** Values under `public` are serialized into the page and readable in the browser. */
6
+ public?: Record<string, unknown>;
7
+ [key: string]: unknown;
8
+ }
9
+ /** The shape of `apex.config.ts`'s default export. */
10
+ interface ApexConfig {
11
+ /**
12
+ * Config resolved at runtime. Declare defaults here (the structure), then
13
+ * override any leaf from the environment — `APEX_<KEY>` for private keys and
14
+ * `APEX_PUBLIC_<KEY>` for `public` keys (camelCase ↔ SCREAMING_SNAKE).
15
+ */
16
+ runtimeConfig?: RuntimeConfig;
17
+ [key: string]: unknown;
18
+ }
19
+ /** Author an `apex.config.ts`. Identity function — exists for types + discoverability. */
20
+ declare function defineConfig(config: ApexConfig): ApexConfig;
21
+ /**
22
+ * Read the runtime config. On the server this is the full config (private +
23
+ * public); in the browser it's the `public` subset seeded by the SSR shell.
24
+ * Mirrors Nuxt's `useRuntimeConfig()` — access public values as `config.public.*`.
25
+ */
26
+ declare function useRuntimeConfig(): RuntimeConfig;
27
+ /**
28
+ * Read a raw environment variable with an optional fallback — the Laravel-style
29
+ * `env('KEY', default)` escape hatch for values not declared in `runtimeConfig`.
30
+ * Server-only in practice (returns the fallback in the browser).
31
+ */
32
+ declare function env(key: string, fallback?: string): string | undefined;
33
+
3
34
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
4
35
  /** Inferred, validated input object for a route's handler. */
5
- type InferInput<Shape extends ZodRawShape | undefined> = Shape extends ZodRawShape ? z.infer<z.ZodObject<Shape>> : Record<string, never>;
36
+ type InferInputShape<Shape extends ZodRawShape | undefined> = Shape extends ZodRawShape ? z.infer<z.ZodObject<Shape>> : Record<string, never>;
6
37
  interface ApexRouteHandlerContext<Shape extends ZodRawShape | undefined> {
7
38
  /** The validated input (query for GET, JSON body otherwise). */
8
- input: InferInput<Shape>;
39
+ input: InferInputShape<Shape>;
9
40
  /** The raw request URL. */
10
41
  url: string;
42
+ /** Resolved runtime config (server-side: private + public). */
43
+ config: RuntimeConfig;
44
+ /** Request-scoped state set by middleware (e.g. an authenticated user). */
45
+ locals: Record<string, unknown>;
11
46
  }
12
47
  interface ApexRouteConfig<Shape extends ZodRawShape | undefined, Output> {
13
48
  /** HTTP method. Defaults to GET. */
@@ -39,17 +74,36 @@ interface ApexRoute {
39
74
  handler: (ctx: {
40
75
  input: unknown;
41
76
  url: string;
77
+ config?: RuntimeConfig;
78
+ locals?: Record<string, unknown>;
42
79
  }) => unknown | Promise<unknown>;
43
80
  }
81
+ /**
82
+ * A route that carries its input/output types (phantom fields, erased at runtime)
83
+ * so the frontend can derive them with `InferInput`/`InferOutput` — one contract,
84
+ * no duplicated types across backend + frontend.
85
+ */
86
+ interface TypedApexRoute<In, Out> extends ApexRoute {
87
+ /** Phantom — never present at runtime; carries the validated input type. */
88
+ readonly __input?: In;
89
+ /** Phantom — never present at runtime; carries the handler's (awaited) output type. */
90
+ readonly __output?: Out;
91
+ }
92
+ /** Derive a route's validated input type: `type In = InferInput<typeof route>`. */
93
+ type InferInput<R> = R extends TypedApexRoute<infer In, unknown> ? In : never;
94
+ /** Derive a route's output type: `type Out = InferOutput<typeof route>`. */
95
+ type InferOutput<R> = R extends TypedApexRoute<unknown, infer Out> ? Out : never;
44
96
  /**
45
97
  * Define a typed API route. A single definition serves as:
46
98
  * - a validated REST endpoint, and
47
99
  * - (when `mcp: true`) an MCP tool whose inputSchema is derived from `input`.
48
100
  *
49
101
  * The strict, schema-carrying contract is what makes "any Apex API can be MCP"
50
- * possible with no extra library on the user's side.
102
+ * possible with no extra library on the user's side. The returned route also
103
+ * carries its input/output types — a `import type` of it on the frontend +
104
+ * `InferInput`/`InferOutput` gives the client the API's types with zero drift.
51
105
  */
52
- declare function defineApexRoute<Shape extends ZodRawShape | undefined, Output>(config: ApexRouteConfig<Shape, Output>): ApexRoute;
106
+ declare function defineApexRoute<Shape extends ZodRawShape | undefined, Output>(config: ApexRouteConfig<Shape, Output>): TypedApexRoute<InferInputShape<Shape>, Awaited<Output>>;
53
107
 
54
108
  /** One route within a resource, mounted at `/api/<name><pathSuffix>`. */
55
109
  interface ResourceRoute {
@@ -95,4 +149,33 @@ interface ApexStore {
95
149
  declare function defineStore(name: string, factory: () => StoreState): ApexStore;
96
150
  declare function isApexStore(x: unknown): x is ApexStore;
97
151
 
98
- export { type ApexResource, type ApexRoute, type ApexRouteConfig, type ApexRouteHandlerContext, type ApexStore, type HttpMethod, type ResourceRoute, type StoreState, defineApexRoute, defineStore, isApexResource, isApexStore };
152
+ /** The short-circuit value returned by `ctx.redirect(...)`. */
153
+ interface MiddlewareResult {
154
+ readonly __apexRedirect: true;
155
+ to: string;
156
+ status: number;
157
+ }
158
+ interface MiddlewareContext {
159
+ /** Request path (e.g. `/blog/hello`). Use it to scope a middleware to certain routes. */
160
+ url: string;
161
+ /** HTTP method. */
162
+ method: string;
163
+ /** Resolved runtime config (private + public on the server). */
164
+ config: RuntimeConfig;
165
+ /** Request headers, lowercased keys. */
166
+ headers: Record<string, string>;
167
+ /**
168
+ * Mutable, request-scoped state. Whatever a middleware puts here is handed to
169
+ * the page `loader({ locals })` and every route handler (`{ locals }`) — the
170
+ * seam for attaching an authenticated user, a request id, feature flags, etc.
171
+ */
172
+ locals: Record<string, unknown>;
173
+ /** Return this to short-circuit the request with a redirect (default 302). */
174
+ redirect(to: string, status?: number): MiddlewareResult;
175
+ }
176
+ type MiddlewareReturn = MiddlewareResult | void;
177
+ type Middleware = (ctx: MiddlewareContext) => MiddlewareReturn | Promise<MiddlewareReturn>;
178
+ /** Author a middleware. Identity function — for types + discoverability. */
179
+ declare function defineMiddleware(fn: Middleware): Middleware;
180
+
181
+ export { type ApexConfig, type ApexResource, type ApexRoute, type ApexRouteConfig, type ApexRouteHandlerContext, type ApexStore, type HttpMethod, type InferInput, type InferOutput, type Middleware, type MiddlewareContext, type MiddlewareResult, type ResourceRoute, type RuntimeConfig, type StoreState, type TypedApexRoute, defineApexRoute, defineConfig, defineMiddleware, defineStore, env, isApexResource, isApexStore, useRuntimeConfig };
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import {
2
+ defineMiddleware,
2
3
  isApexResource
3
- } from "./chunk-HRJTOSYH.js";
4
+ } from "./chunk-2C2HRLIY.js";
4
5
  import {
6
+ defineConfig,
5
7
  defineStore,
6
- isApexStore
7
- } from "./chunk-MZVLRU3R.js";
8
+ env,
9
+ isApexStore,
10
+ useRuntimeConfig
11
+ } from "./chunk-JLIAISWM.js";
8
12
 
9
13
  // src/api/defineRoute.ts
10
14
  function defineApexRoute(config) {
@@ -19,7 +23,11 @@ function defineApexRoute(config) {
19
23
  }
20
24
  export {
21
25
  defineApexRoute,
26
+ defineConfig,
27
+ defineMiddleware,
22
28
  defineStore,
29
+ env,
23
30
  isApexResource,
24
- isApexStore
31
+ isApexStore,
32
+ useRuntimeConfig
25
33
  };