@apex-stack/core 0.1.20 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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}`;
@@ -202,9 +296,32 @@ function renderHead(head) {
202
296
  return parts.join("\n ");
203
297
  }
204
298
  async function renderPage(opts) {
205
- const mod = await opts.loadModule(opts.pageId);
206
- const loaderData = await mod.loader({ params: opts.params ?? {}, url: opts.url }) ?? {};
207
- 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;
208
325
  const stores = opts.stores ?? [];
209
326
  const { html } = renderComponent({
210
327
  template: mod.template,
@@ -220,11 +337,15 @@ async function renderPage(opts) {
220
337
  const layoutName = mod.layout === false ? null : typeof mod.layout === "string" ? mod.layout : available.includes("default") ? "default" : null;
221
338
  let body = html;
222
339
  let layoutCss = "";
223
- if (layoutName && available.includes(layoutName)) {
224
- 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`);
225
345
  const chrome = renderFragment(layoutMod.template, {}, layoutMod.scopeId, opts.registry);
226
- body = /<slot\b[^>]*>[\s\S]*?<\/slot>/.test(chrome) ? chrome.replace(/<slot\b[^>]*>[\s\S]*?<\/slot>/, () => html) : chrome + html;
227
- 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;
228
349
  }
229
350
  const doc = shell({
230
351
  body,
@@ -234,7 +355,8 @@ async function renderPage(opts) {
234
355
  clientHref: opts.clientHref,
235
356
  storeIds: stores.map((s) => s.id),
236
357
  appCss: opts.appCss,
237
- headTags: renderHead(head)
358
+ headTags: renderHead(head),
359
+ configScript: clientConfigScript(opts.publicConfig ?? {})
238
360
  });
239
361
  return opts.transformHtml ? opts.transformHtml(opts.url, doc) : doc;
240
362
  }
@@ -246,7 +368,8 @@ function shell({
246
368
  clientHref,
247
369
  storeIds = [],
248
370
  appCss,
249
- headTags = "<title>Apex JS</title>"
371
+ headTags = "<title>Apex JS</title>",
372
+ configScript = ""
250
373
  }) {
251
374
  const storeImports = storeIds.map((id, i) => ` import __s${i} from ${JSON.stringify(id)}`).join("\n");
252
375
  const storeRegs = storeIds.map((_, i) => ` Alpine.store(__s${i}.name, __s${i}.factory())`).join("\n");
@@ -270,12 +393,15 @@ ${storeRegs ? `${storeRegs}
270
393
  <body>
271
394
  ${body}
272
395
  ${island}
396
+ ${configScript}
273
397
  ${clientScript}
274
398
  </body>
275
399
  </html>`;
276
400
  }
277
401
 
278
402
  export {
403
+ applyEnvToRuntimeConfig,
404
+ resolveApexConfig,
279
405
  renderIslandsPage,
280
406
  scanPages,
281
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,
@@ -62,6 +65,10 @@ var newCommand = defineCommand({
62
65
  type: "boolean",
63
66
  default: true,
64
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)"
65
72
  }
66
73
  },
67
74
  async run({ args }) {
@@ -100,6 +107,8 @@ var newCommand = defineCommand({
100
107
  if (installed) sp.succeed(`Dependencies installed with ${pm}`);
101
108
  else sp.fail(`Install failed \u2014 run ${color.cyan(`${pm} install`)} inside ${dir}`);
102
109
  }
110
+ const ext = await offerExtension(args.vscode);
111
+ if (ext) log(` ${color.green("\u2713")} ${ext}`);
103
112
  const runPrefix = pm === "npm" ? "npm run" : pm;
104
113
  log(`
105
114
  ${color.bold("Next steps")}`);
@@ -123,7 +132,8 @@ var COMMANDS = [
123
132
  ["dev", "Start the dev server (SSR + hydrate, API + MCP)"],
124
133
  ["build", "Build for production (static, islands, or server)"],
125
134
  ["start", "Run a production server build"],
126
- ["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)"],
127
137
  ["migrate", "Apply pending database migrations"],
128
138
  ["mcp", "Inspect the MCP server \u2014 list or call tools"]
129
139
  ];
@@ -135,10 +145,11 @@ var main = defineCommand2({
135
145
  },
136
146
  subCommands: {
137
147
  new: newCommand,
138
- dev: () => import("./dev-6YCKNYJ4.js").then((m) => m.devCommand),
139
- build: () => import("./build-PETU3URU.js").then((m) => m.buildCommand),
140
- start: () => import("./start-V2TBGKWH.js").then((m) => m.startCommand),
141
- make: () => import("./make-WM6DLDCR.js").then((m) => m.makeCommand),
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),
142
153
  migrate: () => import("./migrate-X6LIHMIE.js").then((m) => m.migrateCommand),
143
154
  mcp: () => import("./mcp-CH7L4GF3.js").then((m) => m.mcpCommand)
144
155
  },
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
  };
@@ -24,7 +24,7 @@ var devCommand = defineCommand({
24
24
  process.stdout.write(banner());
25
25
  const sp = spinner(`Starting dev server${args.islands ? " (islands mode)" : ""}\u2026`);
26
26
  try {
27
- const { startDevServer } = await import("./server-62UM2N5C.js");
27
+ const { startDevServer } = await import("./server-PTHGOE42.js");
28
28
  const { port: actual } = await startDevServer({ root, port, islands: Boolean(args.islands) });
29
29
  sp.succeed("Dev server ready");
30
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
  };
@@ -93,6 +93,19 @@ export class ${cls} {
93
93
  }
94
94
  `;
95
95
  }
96
+ function middlewareTemplate() {
97
+ return `import { defineMiddleware } from '@apex-stack/core'
98
+
99
+ // Runs on every request before the page/API handler. Attach request-scoped
100
+ // state to ctx.locals (read in a page loader via \`loader({ locals })\` and in
101
+ // route handlers via \`{ locals }\`), or return ctx.redirect('/path') to
102
+ // short-circuit. Files run in filename order \u2014 prefix with 01. / 02. to order.
103
+ export default defineMiddleware((ctx) => {
104
+ // ctx.locals.user = await getUser(ctx.headers)
105
+ // if (ctx.url.startsWith('/admin') && !ctx.locals.user) return ctx.redirect('/login')
106
+ })
107
+ `;
108
+ }
96
109
  function testTemplate(name) {
97
110
  return `import { describe, expect, it } from 'vitest'
98
111
 
@@ -125,25 +138,36 @@ function plan(kind, name, root) {
125
138
  };
126
139
  case "test":
127
140
  return { path: join(root, "tests", `${name}.test.ts`), contents: testTemplate(name) };
141
+ case "middleware":
142
+ return { path: join(root, "middleware", `${name}.ts`), contents: middlewareTemplate() };
128
143
  }
129
144
  }
130
145
  var makeCommand = defineCommand({
131
146
  meta: {
132
147
  name: "make",
133
- description: "Generate a page, component, API route, store, layout, service, or test"
148
+ description: "Generate a page, component, API route, store, layout, service, test, or middleware"
134
149
  },
135
150
  args: {
136
151
  kind: {
137
152
  type: "positional",
138
153
  required: true,
139
- description: "page | component | api | store | layout | service | test"
154
+ description: "page | component | api | store | layout | service | test | middleware"
140
155
  },
141
156
  name: { type: "positional", required: true, description: "Name (about, Counter, todos, \u2026)" },
142
157
  root: { type: "string", description: "Project root", default: "." }
143
158
  },
144
159
  run({ args }) {
145
160
  const kind = args.kind;
146
- const kinds = ["page", "component", "api", "store", "layout", "service", "test"];
161
+ const kinds = [
162
+ "page",
163
+ "component",
164
+ "api",
165
+ "store",
166
+ "layout",
167
+ "service",
168
+ "test",
169
+ "middleware"
170
+ ];
147
171
  if (!kinds.includes(kind)) {
148
172
  console.error(`
149
173
  Unknown type "${args.kind}". Use: ${kinds.join(" | ")}
@@ -4,17 +4,20 @@ import {
4
4
  import {
5
5
  createApiHandler,
6
6
  createMcpHandler,
7
- loadApiRoutes
8
- } from "./chunk-G77MLFUJ.js";
9
- import "./chunk-HRJTOSYH.js";
7
+ loadApiRoutes,
8
+ loadMiddleware,
9
+ runMiddleware
10
+ } from "./chunk-HCNNKT4A.js";
11
+ import "./chunk-2C2HRLIY.js";
10
12
  import {
11
13
  loadStores,
12
14
  matchRoute,
13
15
  renderIslandsPage,
14
16
  renderPage,
17
+ resolveApexConfig,
15
18
  scanPages
16
- } from "./chunk-4FUWZLVW.js";
17
- import "./chunk-MZVLRU3R.js";
19
+ } from "./chunk-XDKJO6ZC.js";
20
+ import "./chunk-JLIAISWM.js";
18
21
 
19
22
  // src/dev/server.ts
20
23
  import { existsSync as existsSync2, readdirSync } from "fs";
@@ -27,6 +30,7 @@ import {
27
30
  createApp,
28
31
  defineEventHandler,
29
32
  fromNodeMiddleware,
33
+ getRequestHeaders,
30
34
  setResponseHeader,
31
35
  setResponseStatus,
32
36
  toNodeListener
@@ -198,16 +202,42 @@ async function startDevServer(options) {
198
202
  const resolved = id[0] === "/" && !id.startsWith(options.root) ? join(options.root, id).replace(/\\/g, "/") : id;
199
203
  return vite.ssrLoadModule(resolved);
200
204
  };
205
+ const { runtimeConfig, publicConfig } = await resolveApexConfig(
206
+ options.root,
207
+ (id) => ssrLoad(id)
208
+ );
201
209
  const app = createApp();
202
210
  app.use(fromNodeMiddleware(vite.middlewares));
211
+ app.use(
212
+ defineEventHandler(async (event) => {
213
+ const mws = await loadMiddleware(options.root, (id) => ssrLoad(id));
214
+ if (!mws.length) return;
215
+ const { redirect, locals } = await runMiddleware(mws, {
216
+ url: event.path || "/",
217
+ method: event.method,
218
+ config: runtimeConfig,
219
+ headers: getRequestHeaders(event)
220
+ });
221
+ event.context.apexLocals = locals;
222
+ if (redirect) {
223
+ setResponseStatus(event, redirect.status);
224
+ setResponseHeader(event, "Location", redirect.to);
225
+ return "";
226
+ }
227
+ })
228
+ );
203
229
  const loadEntries = () => loadApiRoutes(options.root, (id) => ssrLoad(id));
204
230
  app.use(
205
231
  "/api",
206
- defineEventHandler((event) => loadEntries().then((e) => createApiHandler(e)(event)))
232
+ defineEventHandler(
233
+ (event) => loadEntries().then((e) => createApiHandler(e, runtimeConfig)(event))
234
+ )
207
235
  );
208
236
  app.use(
209
237
  "/mcp",
210
- defineEventHandler((event) => loadEntries().then((e) => createMcpHandler(e)(event)))
238
+ defineEventHandler(
239
+ (event) => loadEntries().then((e) => createMcpHandler(e, runtimeConfig)(event))
240
+ )
211
241
  );
212
242
  app.use(
213
243
  defineEventHandler(async (event) => {
@@ -238,6 +268,10 @@ async function startDevServer(options) {
238
268
  stores,
239
269
  appCss,
240
270
  layouts,
271
+ runtimeConfig,
272
+ publicConfig,
273
+ locals: event.context.apexLocals ?? {},
274
+ errorPageId: existsSync2(join(options.root, "pages", "error.alpine")) ? "/pages/error.alpine" : void 0,
241
275
  transformHtml: (u, doc) => vite.transformIndexHtml(u, doc)
242
276
  });
243
277
  setResponseHeader(event, "Content-Type", "text/html");