@apex-stack/core 0.1.8 → 0.1.10
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/cli.js
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
} from "./chunk-IEXQ7E5C.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
|
-
import { resolve as
|
|
17
|
-
import { defineCommand as
|
|
16
|
+
import { resolve as resolve6 } from "path";
|
|
17
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
18
18
|
|
|
19
19
|
// src/commands/build.ts
|
|
20
20
|
import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "fs";
|
|
@@ -417,15 +417,186 @@ var migrateCommand = defineCommand4({
|
|
|
417
417
|
}
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
-
// src/commands/
|
|
421
|
-
import {
|
|
422
|
-
import {
|
|
420
|
+
// src/commands/new.ts
|
|
421
|
+
import { spawn, spawnSync } from "child_process";
|
|
422
|
+
import { cpSync as cpSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
423
|
+
import { basename, join as join6, resolve as resolve4 } from "path";
|
|
424
|
+
import { fileURLToPath } from "url";
|
|
423
425
|
import { defineCommand as defineCommand5 } from "citty";
|
|
424
426
|
|
|
427
|
+
// src/ui.ts
|
|
428
|
+
var TTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
429
|
+
var RESET = "\x1B[0m";
|
|
430
|
+
function truecolor(r, g, b, s) {
|
|
431
|
+
return TTY ? `\x1B[38;2;${r};${g};${b}m${s}${RESET}` : s;
|
|
432
|
+
}
|
|
433
|
+
function style(code, s) {
|
|
434
|
+
return TTY ? `\x1B[${code}m${s}${RESET}` : s;
|
|
435
|
+
}
|
|
436
|
+
var color = {
|
|
437
|
+
cyan: (s) => truecolor(34, 211, 238, s),
|
|
438
|
+
indigo: (s) => truecolor(129, 140, 248, s),
|
|
439
|
+
green: (s) => truecolor(52, 211, 153, s),
|
|
440
|
+
red: (s) => truecolor(248, 113, 113, s),
|
|
441
|
+
gray: (s) => truecolor(154, 166, 196, s),
|
|
442
|
+
bold: (s) => style("1", s),
|
|
443
|
+
dim: (s) => style("2", s)
|
|
444
|
+
};
|
|
445
|
+
var LOGO = [
|
|
446
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
447
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
|
|
448
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D ",
|
|
449
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 ",
|
|
450
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
|
|
451
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
452
|
+
];
|
|
453
|
+
var FROM = [99, 102, 241];
|
|
454
|
+
var TO = [34, 211, 238];
|
|
455
|
+
function banner(subtitle = "The full-stack, AI-native meta-framework for Alpine.js") {
|
|
456
|
+
const width = LOGO[0].length;
|
|
457
|
+
const rows = LOGO.map((line) => {
|
|
458
|
+
if (!TTY) return ` ${line}`;
|
|
459
|
+
let out = " ";
|
|
460
|
+
for (let i = 0; i < line.length; i++) {
|
|
461
|
+
const t = width > 1 ? i / (width - 1) : 0;
|
|
462
|
+
const r = Math.round(FROM[0] + (TO[0] - FROM[0]) * t);
|
|
463
|
+
const g = Math.round(FROM[1] + (TO[1] - FROM[1]) * t);
|
|
464
|
+
const b = Math.round(FROM[2] + (TO[2] - FROM[2]) * t);
|
|
465
|
+
out += `\x1B[38;2;${r};${g};${b}m${line[i]}`;
|
|
466
|
+
}
|
|
467
|
+
return out + RESET;
|
|
468
|
+
});
|
|
469
|
+
return `
|
|
470
|
+
${rows.join("\n")}
|
|
471
|
+
${color.gray(subtitle)}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
475
|
+
function spinner(text) {
|
|
476
|
+
if (!TTY) {
|
|
477
|
+
process.stdout.write(` ${text}
|
|
478
|
+
`);
|
|
479
|
+
return {
|
|
480
|
+
succeed: (t) => console.log(` ${color.green("\u2713")} ${t || text}`),
|
|
481
|
+
fail: (t) => console.log(` ${color.red("\u2717")} ${t || text}`),
|
|
482
|
+
stop: () => {
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
let i = 0;
|
|
487
|
+
process.stdout.write("\x1B[?25l");
|
|
488
|
+
const id = setInterval(() => {
|
|
489
|
+
process.stdout.write(`\r\x1B[2K ${color.cyan(FRAMES[i++ % FRAMES.length])} ${text}`);
|
|
490
|
+
}, 80);
|
|
491
|
+
const end = (symbol, t) => {
|
|
492
|
+
clearInterval(id);
|
|
493
|
+
process.stdout.write(`\r\x1B[2K ${symbol} ${t || text}
|
|
494
|
+
\x1B[?25h`);
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
succeed: (t) => end(color.green("\u2713"), t),
|
|
498
|
+
fail: (t) => end(color.red("\u2717"), t),
|
|
499
|
+
stop: () => {
|
|
500
|
+
clearInterval(id);
|
|
501
|
+
process.stdout.write("\r\x1B[2K\x1B[?25h");
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function ready(rows) {
|
|
506
|
+
const w = Math.max(...rows.map(([l]) => l.length));
|
|
507
|
+
console.log();
|
|
508
|
+
for (const [label, value] of rows) {
|
|
509
|
+
console.log(` ${color.cyan("\u279C")} ${color.bold(label.padEnd(w))} ${color.cyan(value)}`);
|
|
510
|
+
}
|
|
511
|
+
console.log();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/new.ts
|
|
515
|
+
var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
|
|
516
|
+
function detectPackageManager() {
|
|
517
|
+
const ua = process.env.npm_config_user_agent || "";
|
|
518
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
519
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
520
|
+
if (ua.startsWith("bun")) return "bun";
|
|
521
|
+
return "npm";
|
|
522
|
+
}
|
|
523
|
+
function runSync(cmd, args, cwd) {
|
|
524
|
+
return spawnSync(cmd, args, { cwd, stdio: "ignore", shell: process.platform === "win32" }).status === 0;
|
|
525
|
+
}
|
|
526
|
+
function installAsync(pm, cwd) {
|
|
527
|
+
const args = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
|
|
528
|
+
return new Promise((res) => {
|
|
529
|
+
const child = spawn(pm, args, { cwd, stdio: "ignore", shell: process.platform === "win32" });
|
|
530
|
+
child.on("close", (code) => res(code === 0));
|
|
531
|
+
child.on("error", () => res(false));
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
var newCommand = defineCommand5({
|
|
535
|
+
meta: { name: "new", description: "Scaffold a new Apex JS app" },
|
|
536
|
+
args: {
|
|
537
|
+
dir: { type: "positional", required: false, description: "Target directory", default: "apex-app" },
|
|
538
|
+
install: { type: "boolean", default: true, description: "Install dependencies (use --no-install to skip)" },
|
|
539
|
+
git: { type: "boolean", default: true, description: "Initialize a git repository (use --no-git to skip)" }
|
|
540
|
+
},
|
|
541
|
+
async run({ args }) {
|
|
542
|
+
const dir = String(args.dir);
|
|
543
|
+
const target = resolve4(process.cwd(), dir);
|
|
544
|
+
const name = basename(target);
|
|
545
|
+
process.stdout.write(banner());
|
|
546
|
+
const log = console.log;
|
|
547
|
+
if (existsSync4(target) && readdirSync3(target).length > 0) {
|
|
548
|
+
console.error(` ${color.red("\u2717")} Target directory is not empty: ${target}
|
|
549
|
+
`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
cpSync2(TEMPLATE_DIR, target, { recursive: true });
|
|
553
|
+
const gitignore = join6(target, "_gitignore");
|
|
554
|
+
if (existsSync4(gitignore)) renameSync(gitignore, join6(target, ".gitignore"));
|
|
555
|
+
for (const rel of ["package.json", "README.md"]) {
|
|
556
|
+
const file = join6(target, rel);
|
|
557
|
+
if (existsSync4(file)) writeFileSync3(file, readFileSync2(file, "utf8").replaceAll("{{name}}", name));
|
|
558
|
+
}
|
|
559
|
+
log(` ${color.green("\u2713")} Created ${color.bold(dir)}`);
|
|
560
|
+
const pm = detectPackageManager();
|
|
561
|
+
let gitOk = false;
|
|
562
|
+
if (args.git) {
|
|
563
|
+
const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
|
|
564
|
+
if (hasGit && runSync("git", ["init", "-q"], target)) {
|
|
565
|
+
runSync("git", ["add", "-A"], target);
|
|
566
|
+
runSync("git", ["commit", "-m", "Initial commit from Apex JS", "--no-gpg-sign"], target);
|
|
567
|
+
gitOk = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (gitOk) log(` ${color.green("\u2713")} Initialized a git repository`);
|
|
571
|
+
let installed = false;
|
|
572
|
+
if (args.install) {
|
|
573
|
+
const sp = spinner(`Installing dependencies with ${pm}\u2026 ${color.dim("(first run can take a minute)")}`);
|
|
574
|
+
installed = await installAsync(pm, target);
|
|
575
|
+
if (installed) sp.succeed(`Dependencies installed with ${pm}`);
|
|
576
|
+
else sp.fail(`Install failed \u2014 run ${color.cyan(`${pm} install`)} inside ${dir}`);
|
|
577
|
+
}
|
|
578
|
+
const runPrefix = pm === "npm" ? "npm run" : pm;
|
|
579
|
+
log(`
|
|
580
|
+
${color.bold("Next steps")}`);
|
|
581
|
+
log(` ${color.cyan(`cd ${dir}`)}`);
|
|
582
|
+
if (!installed) log(` ${color.cyan(pm === "yarn" ? "yarn" : `${pm} install`)}`);
|
|
583
|
+
log(` ${color.cyan("apex dev")} ${color.gray("# \u2192 http://localhost:3000")}`);
|
|
584
|
+
log(`
|
|
585
|
+
${color.gray("Not installed globally? Use")} ${color.cyan(`${runPrefix} dev`)}${color.gray(".")}`);
|
|
586
|
+
log(` ${color.gray("Islands mode:")} ${color.cyan("apex dev --islands")}${color.gray(" \xB7 API routes are also MCP tools at /mcp.")}
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// src/commands/start.ts
|
|
592
|
+
import { existsSync as existsSync6 } from "fs";
|
|
593
|
+
import { join as join8, resolve as resolve5 } from "path";
|
|
594
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
595
|
+
|
|
425
596
|
// src/prod/server.ts
|
|
426
597
|
import { createServer as createHttpServer } from "http";
|
|
427
|
-
import { existsSync as
|
|
428
|
-
import { join as
|
|
598
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
599
|
+
import { join as join7 } from "path";
|
|
429
600
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
430
601
|
import {
|
|
431
602
|
createApp,
|
|
@@ -450,8 +621,8 @@ var MIME = {
|
|
|
450
621
|
async function startProdServer(options) {
|
|
451
622
|
const dir = options.dir;
|
|
452
623
|
const port = options.port ?? 3e3;
|
|
453
|
-
const manifest = JSON.parse(
|
|
454
|
-
const importServer = (relFile) => import(pathToFileURL2(
|
|
624
|
+
const manifest = JSON.parse(readFileSync3(join7(dir, "apex-manifest.json"), "utf8"));
|
|
625
|
+
const importServer = (relFile) => import(pathToFileURL2(join7(dir, "server", relFile)).href);
|
|
455
626
|
const registry = {};
|
|
456
627
|
let componentCss = "";
|
|
457
628
|
for (const [name, file] of Object.entries(manifest.components)) {
|
|
@@ -473,12 +644,12 @@ async function startProdServer(options) {
|
|
|
473
644
|
if (event.method !== "GET") return;
|
|
474
645
|
const path = decodeURIComponent(getRequestURL(event).pathname);
|
|
475
646
|
if (path === "/" || path.startsWith("/api") || path === "/mcp") return;
|
|
476
|
-
const file =
|
|
477
|
-
if (!file.startsWith(dir) || !
|
|
647
|
+
const file = join7(dir, path);
|
|
648
|
+
if (!file.startsWith(dir) || !existsSync5(file) || !statSync(file).isFile()) return;
|
|
478
649
|
const ext = path.slice(path.lastIndexOf("."));
|
|
479
650
|
setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
480
651
|
if (path.startsWith("/assets/")) setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
|
|
481
|
-
return
|
|
652
|
+
return readFileSync3(file);
|
|
482
653
|
})
|
|
483
654
|
);
|
|
484
655
|
if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
|
|
@@ -508,20 +679,20 @@ async function startProdServer(options) {
|
|
|
508
679
|
})
|
|
509
680
|
);
|
|
510
681
|
const server = createHttpServer(toNodeListener(app));
|
|
511
|
-
await new Promise((
|
|
682
|
+
await new Promise((resolve7) => server.listen(port, resolve7));
|
|
512
683
|
return { server, port };
|
|
513
684
|
}
|
|
514
685
|
|
|
515
686
|
// src/commands/start.ts
|
|
516
|
-
var startCommand =
|
|
687
|
+
var startCommand = defineCommand6({
|
|
517
688
|
meta: { name: "start", description: "Run a production build (from `apex build --server`)" },
|
|
518
689
|
args: {
|
|
519
690
|
dir: { type: "positional", required: false, description: "Build directory", default: "dist" },
|
|
520
691
|
port: { type: "string", description: "Port to listen on", default: "3000" }
|
|
521
692
|
},
|
|
522
693
|
async run({ args }) {
|
|
523
|
-
const dir =
|
|
524
|
-
if (!
|
|
694
|
+
const dir = resolve5(process.cwd(), args.dir);
|
|
695
|
+
if (!existsSync6(join8(dir, "apex-manifest.json"))) {
|
|
525
696
|
console.error(`
|
|
526
697
|
No build found in ${args.dir}/. Run \`apex build --server\` first.
|
|
527
698
|
`);
|
|
@@ -536,7 +707,7 @@ var startCommand = defineCommand5({
|
|
|
536
707
|
});
|
|
537
708
|
|
|
538
709
|
// src/cli.ts
|
|
539
|
-
var dev =
|
|
710
|
+
var dev = defineCommand7({
|
|
540
711
|
meta: { name: "dev", description: "Start the Apex JS development server" },
|
|
541
712
|
args: {
|
|
542
713
|
root: { type: "positional", required: false, description: "Project root", default: "." },
|
|
@@ -544,27 +715,60 @@ var dev = defineCommand6({
|
|
|
544
715
|
islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
|
|
545
716
|
},
|
|
546
717
|
async run({ args }) {
|
|
547
|
-
const root =
|
|
718
|
+
const root = resolve6(process.cwd(), args.root);
|
|
548
719
|
const port = Number(args.port);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
720
|
+
process.stdout.write(banner());
|
|
721
|
+
const sp = spinner(`Starting dev server${args.islands ? " (islands mode)" : ""}\u2026`);
|
|
722
|
+
try {
|
|
723
|
+
const { port: actual } = await startDevServer({ root, port, islands: args.islands });
|
|
724
|
+
sp.succeed("Dev server ready");
|
|
725
|
+
ready([
|
|
726
|
+
["Local", `http://localhost:${actual}/`],
|
|
727
|
+
["MCP", `http://localhost:${actual}/mcp`]
|
|
728
|
+
]);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
sp.fail("Failed to start the dev server");
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
554
733
|
}
|
|
555
734
|
});
|
|
556
|
-
var
|
|
735
|
+
var COMMANDS = [
|
|
736
|
+
["new", "Scaffold a new app"],
|
|
737
|
+
["dev", "Start the dev server (SSR + hydrate, API + MCP)"],
|
|
738
|
+
["build", "Build for production (static, islands, or server)"],
|
|
739
|
+
["start", "Run a production server build"],
|
|
740
|
+
["make", "Generate a page, component, or API route"],
|
|
741
|
+
["migrate", "Apply pending database migrations"],
|
|
742
|
+
["mcp", "Inspect the MCP server \u2014 list or call tools"]
|
|
743
|
+
];
|
|
744
|
+
var main = defineCommand7({
|
|
557
745
|
meta: {
|
|
558
746
|
name: "apex",
|
|
559
747
|
description: "The full-stack meta-framework for Alpine.js"
|
|
560
748
|
},
|
|
561
749
|
subCommands: {
|
|
750
|
+
new: newCommand,
|
|
562
751
|
dev,
|
|
563
752
|
build: buildCommand,
|
|
564
753
|
start: startCommand,
|
|
565
754
|
make: makeCommand,
|
|
566
755
|
migrate: migrateCommand,
|
|
567
756
|
mcp: mcpCommand
|
|
757
|
+
},
|
|
758
|
+
// Shown for a bare `apex` (no subcommand): the brand banner + a command menu.
|
|
759
|
+
run({ rawArgs }) {
|
|
760
|
+
if (rawArgs.length > 0) return;
|
|
761
|
+
process.stdout.write(banner());
|
|
762
|
+
const log = console.log;
|
|
763
|
+
log(` ${color.bold("Usage")} ${color.gray("apex")} ${color.cyan("<command>")} ${color.gray("[options]")}
|
|
764
|
+
`);
|
|
765
|
+
log(` ${color.bold("Commands")}`);
|
|
766
|
+
for (const [name, desc] of COMMANDS) {
|
|
767
|
+
log(` ${color.cyan(`apex ${name}`.padEnd(13))} ${color.gray(desc)}`);
|
|
768
|
+
}
|
|
769
|
+
log(`
|
|
770
|
+
${color.gray("Run")} ${color.cyan("apex <command> --help")} ${color.gray("for details.")}
|
|
771
|
+
`);
|
|
568
772
|
}
|
|
569
773
|
});
|
|
570
774
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apex-stack/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
-
"dist"
|
|
39
|
+
"dist",
|
|
40
|
+
"templates"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"node": ">=20.19"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
|
-
"build": "tsup",
|
|
58
|
+
"build": "node scripts/copy-templates.mjs && tsup",
|
|
58
59
|
"dev": "tsup --watch",
|
|
59
60
|
"typecheck": "tsc --noEmit"
|
|
60
61
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
An [Apex JS](https://github.com/andrecorugda/apexjs) app — a meta-framework for
|
|
4
|
+
Alpine.js that renders on the server and hydrates in the browser.
|
|
5
|
+
|
|
6
|
+
## Getting started
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open [http://localhost:3000](http://localhost:3000).
|
|
14
|
+
|
|
15
|
+
## Islands mode
|
|
16
|
+
|
|
17
|
+
Ship interactive JavaScript only where you need it:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
apex dev --islands
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
- `pages/*.alpine` — single-file components. The `<script server>` block runs on
|
|
26
|
+
the server; its `loader()` return value becomes the Alpine `x-data` scope.
|
|
27
|
+
- `server/api/*.ts` — API routes defined with `defineApexRoute`.
|
|
28
|
+
|
|
29
|
+
## AI-native API
|
|
30
|
+
|
|
31
|
+
Every route in `server/api/*.ts` is a REST endpoint **and** an MCP tool at the
|
|
32
|
+
same time. Set `mcp: true` on a route (see `server/api/hello.ts`) and it is
|
|
33
|
+
automatically exposed to AI agents at the `/mcp` endpoint — no extra wiring.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script server lang="ts">
|
|
2
|
+
export function loader() {
|
|
3
|
+
return {
|
|
4
|
+
title: 'Welcome to Apex JS',
|
|
5
|
+
tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template x-data="{ open: false }">
|
|
11
|
+
<main>
|
|
12
|
+
<h1 x-text="title"></h1>
|
|
13
|
+
<p class="tagline" x-text="tagline"></p>
|
|
14
|
+
|
|
15
|
+
<section class="card">
|
|
16
|
+
<p>
|
|
17
|
+
This page was rendered on the server from
|
|
18
|
+
<code>pages/index.alpine</code>, then hydrated by Alpine in the browser.
|
|
19
|
+
Edit it and save to see your changes live.
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<button type="button" @click="open = !open" x-text="open ? 'Hide the details' : 'Show me how'"></button>
|
|
23
|
+
|
|
24
|
+
<div x-show="open" x-transition class="details">
|
|
25
|
+
<p>
|
|
26
|
+
The <code>loader()</code> in the server block runs on the server and
|
|
27
|
+
hands its data straight to Alpine's <code>x-data</code> scope — no
|
|
28
|
+
fetch, no boilerplate.
|
|
29
|
+
</p>
|
|
30
|
+
<p>
|
|
31
|
+
Next, open <code>server/api/hello.ts</code>: it's a REST endpoint
|
|
32
|
+
<em>and</em> an MCP tool at <code>/mcp</code> at the same time.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<p class="hint">
|
|
38
|
+
Run <code>apex dev --islands</code> to ship interactive islands only where
|
|
39
|
+
you need them.
|
|
40
|
+
</p>
|
|
41
|
+
</main>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style scoped>
|
|
45
|
+
main {
|
|
46
|
+
max-width: 40rem;
|
|
47
|
+
margin: 4rem auto;
|
|
48
|
+
padding: 0 1.5rem;
|
|
49
|
+
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
50
|
+
line-height: 1.6;
|
|
51
|
+
color: #1e293b;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
h1 {
|
|
55
|
+
color: #2563eb;
|
|
56
|
+
font-size: 2.5rem;
|
|
57
|
+
margin-bottom: 0.5rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tagline {
|
|
61
|
+
font-size: 1.15rem;
|
|
62
|
+
color: #475569;
|
|
63
|
+
margin-top: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.card {
|
|
67
|
+
margin-top: 2rem;
|
|
68
|
+
padding: 1.5rem;
|
|
69
|
+
border: 1px solid #e2e8f0;
|
|
70
|
+
border-radius: 0.75rem;
|
|
71
|
+
background: #f8fafc;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
button {
|
|
75
|
+
margin-top: 0.5rem;
|
|
76
|
+
padding: 0.6rem 1.1rem;
|
|
77
|
+
font-size: 1rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
color: #fff;
|
|
80
|
+
background: #2563eb;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 0.5rem;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: background 0.15s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
button:hover {
|
|
88
|
+
background: #1d4ed8;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.details {
|
|
92
|
+
margin-top: 1rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
code {
|
|
96
|
+
padding: 0.1rem 0.35rem;
|
|
97
|
+
font-size: 0.9em;
|
|
98
|
+
background: #e2e8f0;
|
|
99
|
+
border-radius: 0.35rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.hint {
|
|
103
|
+
margin-top: 2rem;
|
|
104
|
+
font-size: 0.95rem;
|
|
105
|
+
color: #64748b;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineApexRoute } from '@apex-stack/core'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export default defineApexRoute({
|
|
5
|
+
method: 'GET',
|
|
6
|
+
description: 'Say hello to someone by name',
|
|
7
|
+
input: { name: z.string() },
|
|
8
|
+
mcp: true,
|
|
9
|
+
handler: ({ input }) => ({ message: `Hello, ${input.name}!` }),
|
|
10
|
+
})
|