@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.
- package/dist/{build-PETU3URU.js → build-VHS6KZBK.js} +75 -21
- package/dist/chunk-2C2HRLIY.js +18 -0
- package/dist/chunk-CHBSGOB3.js +42 -0
- package/dist/{chunk-G77MLFUJ.js → chunk-HCNNKT4A.js} +48 -9
- package/dist/chunk-JLIAISWM.js +48 -0
- package/dist/{chunk-4FUWZLVW.js → chunk-XDKJO6ZC.js} +148 -22
- package/dist/cli.js +16 -5
- package/dist/client.d.ts +1 -1
- package/dist/client.js +2 -1
- package/dist/{dev-6YCKNYJ4.js → dev-G7HPP6KW.js} +1 -1
- package/dist/index.d.ts +88 -5
- package/dist/index.js +12 -4
- package/dist/{make-WM6DLDCR.js → make-VAYO5GWA.js} +27 -3
- package/dist/{server-62UM2N5C.js → server-PTHGOE42.js} +41 -7
- package/dist/{start-V2TBGKWH.js → start-3O3E43PT.js} +44 -8
- package/dist/upgrade-WC5F5FKY.js +168 -0
- package/package.json +5 -4
- package/templates/default/.env.example +9 -0
- package/templates/default/README.md +25 -1
- package/templates/default/_gitignore +5 -0
- package/templates/default/apex.config.ts +22 -0
- package/vscode/apex-alpine.vsix +0 -0
- package/dist/chunk-HRJTOSYH.js +0 -8
- package/dist/chunk-MZVLRU3R.js +0 -15
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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({
|
|
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 =
|
|
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 =
|
|
85
|
-
if (!
|
|
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
|
|
150
|
-
import { join as
|
|
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 =
|
|
153
|
-
if (!
|
|
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
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
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
|
-
|
|
224
|
-
|
|
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>/, () =>
|
|
227
|
-
layoutCss
|
|
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,
|
|
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-
|
|
139
|
-
build: () => import("./build-
|
|
140
|
-
start: () => import("./start-
|
|
141
|
-
make: () => import("./make-
|
|
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
|
@@ -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-
|
|
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
|
|
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:
|
|
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>):
|
|
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
|
-
|
|
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-
|
|
4
|
+
} from "./chunk-2C2HRLIY.js";
|
|
4
5
|
import {
|
|
6
|
+
defineConfig,
|
|
5
7
|
defineStore,
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
9
|
-
|
|
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-
|
|
17
|
-
import "./chunk-
|
|
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(
|
|
232
|
+
defineEventHandler(
|
|
233
|
+
(event) => loadEntries().then((e) => createApiHandler(e, runtimeConfig)(event))
|
|
234
|
+
)
|
|
207
235
|
);
|
|
208
236
|
app.use(
|
|
209
237
|
"/mcp",
|
|
210
|
-
defineEventHandler(
|
|
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");
|