@apex-stack/core 0.1.19 → 0.1.20
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-J47A3B4Y.js → build-PETU3URU.js} +17 -7
- package/dist/{chunk-XSN6NDWP.js → chunk-4FUWZLVW.js} +11 -3
- package/dist/{chunk-JWYNLP4L.js → chunk-G77MLFUJ.js} +4 -1
- package/dist/cli.js +53 -23
- package/dist/{dev-OCVQRCCE.js → dev-6YCKNYJ4.js} +6 -2
- package/dist/{make-JAW22LQZ.js → make-WM6DLDCR.js} +47 -5
- package/dist/{mcp-DL4J6JFJ.js → mcp-CH7L4GF3.js} +1 -1
- package/dist/{migrate-NOGFOFV2.js → migrate-X6LIHMIE.js} +3 -1
- package/dist/{server-L3V34B5X.js → server-62UM2N5C.js} +26 -16
- package/dist/{start-AUJJ7HAY.js → start-V2TBGKWH.js} +5 -4
- package/package.json +1 -1
- package/templates/default/README.md +41 -19
- package/templates/default/components/Counter.alpine +15 -0
- package/templates/default/composables/useToggle.ts +14 -0
- package/templates/default/db/README.md +18 -0
- package/templates/default/layouts/default.alpine +16 -0
- package/templates/default/package.json +11 -2
- package/templates/default/pages/index.alpine +23 -92
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/server/api/hello.ts +10 -2
- package/templates/default/services/GreetingService.ts +12 -0
- package/templates/default/shared/types.ts +11 -0
- package/templates/default/stores/ui.ts +9 -0
- package/templates/default/tests/greeting.test.ts +12 -0
- package/templates/default/tsconfig.json +15 -0
- package/templates/default/vitest.config.ts +7 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
renderIslandsPage,
|
|
6
6
|
renderPage,
|
|
7
7
|
scanPages
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-4FUWZLVW.js";
|
|
9
9
|
import "./chunk-MZVLRU3R.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/build.ts
|
|
@@ -38,8 +38,8 @@ async function buildClient(root, routes, outDir) {
|
|
|
38
38
|
return [
|
|
39
39
|
`import Alpine from 'alpinejs'`,
|
|
40
40
|
`import ${JSON.stringify(pageId)}`,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
"window.Alpine = Alpine",
|
|
42
|
+
"Alpine.start()"
|
|
43
43
|
].join("\n");
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -55,7 +55,9 @@ async function buildClient(root, routes, outDir) {
|
|
|
55
55
|
rollupOptions: { input }
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
|
-
const manifest = JSON.parse(
|
|
58
|
+
const manifest = JSON.parse(
|
|
59
|
+
readFileSync(join(outDir, ".vite", "manifest.json"), "utf8")
|
|
60
|
+
);
|
|
59
61
|
const hrefs = /* @__PURE__ */ new Map();
|
|
60
62
|
for (const r of routes) {
|
|
61
63
|
const virt = `${VIRT}${r.pageId}`;
|
|
@@ -136,8 +138,16 @@ var buildCommand = defineCommand({
|
|
|
136
138
|
args: {
|
|
137
139
|
root: { type: "positional", required: false, description: "Project root", default: "." },
|
|
138
140
|
outDir: { type: "string", description: "Output directory", default: "dist" },
|
|
139
|
-
islands: {
|
|
140
|
-
|
|
141
|
+
islands: {
|
|
142
|
+
type: "boolean",
|
|
143
|
+
description: "Static-first islands mode (zero-JS static)",
|
|
144
|
+
default: false
|
|
145
|
+
},
|
|
146
|
+
server: {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
description: "Build a Node server (dynamic routes + API/MCP)",
|
|
149
|
+
default: false
|
|
150
|
+
}
|
|
141
151
|
},
|
|
142
152
|
async run({ args }) {
|
|
143
153
|
const root = resolve(process.cwd(), args.root);
|
|
@@ -179,7 +189,7 @@ var buildCommand = defineCommand({
|
|
|
179
189
|
if (existsSync2(pub)) cpSync(pub, outDir, { recursive: true });
|
|
180
190
|
console.log(
|
|
181
191
|
`
|
|
182
|
-
Built ${staticRoutes.length} page(s) \u2192 ${args.outDir}
|
|
192
|
+
Built ${staticRoutes.length} page(s) \u2192 ${args.outDir}/${args.islands ? " (islands / static-first)" : " (prerendered + hydrated)"}`
|
|
183
193
|
);
|
|
184
194
|
if (dynamic.length) {
|
|
185
195
|
console.log(
|
|
@@ -176,7 +176,11 @@ function storesInitialState(stores) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/dev/renderPage.ts
|
|
179
|
-
import {
|
|
179
|
+
import {
|
|
180
|
+
renderComponent,
|
|
181
|
+
renderFragment,
|
|
182
|
+
stateIsland
|
|
183
|
+
} from "@apex-stack/kit";
|
|
180
184
|
function escAttr(s) {
|
|
181
185
|
return String(s).replace(
|
|
182
186
|
/[&<>"]/g,
|
|
@@ -186,10 +190,14 @@ function escAttr(s) {
|
|
|
186
190
|
function renderHead(head) {
|
|
187
191
|
const parts = [`<title>${head?.title ? escAttr(head.title) : "Apex JS"}</title>`];
|
|
188
192
|
for (const m of head?.meta ?? []) {
|
|
189
|
-
parts.push(
|
|
193
|
+
parts.push(
|
|
194
|
+
`<meta ${Object.entries(m).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`
|
|
195
|
+
);
|
|
190
196
|
}
|
|
191
197
|
for (const l of head?.link ?? []) {
|
|
192
|
-
parts.push(
|
|
198
|
+
parts.push(
|
|
199
|
+
`<link ${Object.entries(l).map(([k, v]) => `${k}="${escAttr(v)}"`).join(" ")} />`
|
|
200
|
+
);
|
|
193
201
|
}
|
|
194
202
|
return parts.join("\n ");
|
|
195
203
|
}
|
|
@@ -111,7 +111,10 @@ function buildServer(entries) {
|
|
|
111
111
|
inputSchema: entry.route.inputShape ?? {}
|
|
112
112
|
},
|
|
113
113
|
async (args) => {
|
|
114
|
-
const result = await entry.route.handler({
|
|
114
|
+
const result = await entry.route.handler({
|
|
115
|
+
input: args ?? {},
|
|
116
|
+
url: `mcp://${entry.mcpName}`
|
|
117
|
+
});
|
|
115
118
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
116
119
|
}
|
|
117
120
|
);
|
package/dist/cli.js
CHANGED
|
@@ -11,11 +11,21 @@ import { defineCommand as defineCommand2, runMain } from "citty";
|
|
|
11
11
|
|
|
12
12
|
// src/commands/new.ts
|
|
13
13
|
import { spawn, spawnSync } from "child_process";
|
|
14
|
-
import { cpSync, existsSync,
|
|
14
|
+
import { cpSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "fs";
|
|
15
15
|
import { basename, join, resolve } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
import { defineCommand } from "citty";
|
|
18
18
|
var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
|
|
19
|
+
function substituteName(dir, name) {
|
|
20
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const p = join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) substituteName(p, name);
|
|
23
|
+
else {
|
|
24
|
+
const txt = readFileSync(p, "utf8");
|
|
25
|
+
if (txt.includes("{{name}}")) writeFileSync(p, txt.replaceAll("{{name}}", name));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
function detectPackageManager() {
|
|
20
30
|
const ua = process.env.npm_config_user_agent || "";
|
|
21
31
|
if (ua.startsWith("pnpm")) return "pnpm";
|
|
@@ -37,9 +47,22 @@ function installAsync(pm, cwd) {
|
|
|
37
47
|
var newCommand = defineCommand({
|
|
38
48
|
meta: { name: "new", description: "Scaffold a new Apex JS app" },
|
|
39
49
|
args: {
|
|
40
|
-
dir: {
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
dir: {
|
|
51
|
+
type: "positional",
|
|
52
|
+
required: false,
|
|
53
|
+
description: "Target directory",
|
|
54
|
+
default: "apex-app"
|
|
55
|
+
},
|
|
56
|
+
install: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
default: true,
|
|
59
|
+
description: "Install dependencies (use --no-install to skip)"
|
|
60
|
+
},
|
|
61
|
+
git: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
default: true,
|
|
64
|
+
description: "Initialize a git repository (use --no-git to skip)"
|
|
65
|
+
}
|
|
43
66
|
},
|
|
44
67
|
async run({ args }) {
|
|
45
68
|
const dir = String(args.dir);
|
|
@@ -55,10 +78,7 @@ var newCommand = defineCommand({
|
|
|
55
78
|
cpSync(TEMPLATE_DIR, target, { recursive: true });
|
|
56
79
|
const gitignore = join(target, "_gitignore");
|
|
57
80
|
if (existsSync(gitignore)) renameSync(gitignore, join(target, ".gitignore"));
|
|
58
|
-
|
|
59
|
-
const file = join(target, rel);
|
|
60
|
-
if (existsSync(file)) writeFileSync(file, readFileSync(file, "utf8").replaceAll("{{name}}", name));
|
|
61
|
-
}
|
|
81
|
+
substituteName(target, name);
|
|
62
82
|
log(` ${color.green("\u2713")} Created ${color.bold(dir)}`);
|
|
63
83
|
const pm = detectPackageManager();
|
|
64
84
|
let gitOk = false;
|
|
@@ -73,7 +93,9 @@ var newCommand = defineCommand({
|
|
|
73
93
|
if (gitOk) log(` ${color.green("\u2713")} Initialized a git repository`);
|
|
74
94
|
let installed = false;
|
|
75
95
|
if (args.install) {
|
|
76
|
-
const sp = spinner(
|
|
96
|
+
const sp = spinner(
|
|
97
|
+
`Installing dependencies with ${pm}\u2026 ${color.dim("(first run can take a minute)")}`
|
|
98
|
+
);
|
|
77
99
|
installed = await installAsync(pm, target);
|
|
78
100
|
if (installed) sp.succeed(`Dependencies installed with ${pm}`);
|
|
79
101
|
else sp.fail(`Install failed \u2014 run ${color.cyan(`${pm} install`)} inside ${dir}`);
|
|
@@ -84,10 +106,14 @@ var newCommand = defineCommand({
|
|
|
84
106
|
log(` ${color.cyan(`cd ${dir}`)}`);
|
|
85
107
|
if (!installed) log(` ${color.cyan(pm === "yarn" ? "yarn" : `${pm} install`)}`);
|
|
86
108
|
log(` ${color.cyan("apex dev")} ${color.gray("# \u2192 http://localhost:3000")}`);
|
|
87
|
-
log(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
log(
|
|
110
|
+
`
|
|
111
|
+
${color.gray("Not installed globally? Use")} ${color.cyan(`${runPrefix} dev`)}${color.gray(".")}`
|
|
112
|
+
);
|
|
113
|
+
log(
|
|
114
|
+
` ${color.gray("Islands mode:")} ${color.cyan("apex dev --islands")}${color.gray(" \xB7 API routes are also MCP tools at /mcp.")}
|
|
115
|
+
`
|
|
116
|
+
);
|
|
91
117
|
}
|
|
92
118
|
});
|
|
93
119
|
|
|
@@ -109,27 +135,31 @@ var main = defineCommand2({
|
|
|
109
135
|
},
|
|
110
136
|
subCommands: {
|
|
111
137
|
new: newCommand,
|
|
112
|
-
dev: () => import("./dev-
|
|
113
|
-
build: () => import("./build-
|
|
114
|
-
start: () => import("./start-
|
|
115
|
-
make: () => import("./make-
|
|
116
|
-
migrate: () => import("./migrate-
|
|
117
|
-
mcp: () => import("./mcp-
|
|
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),
|
|
142
|
+
migrate: () => import("./migrate-X6LIHMIE.js").then((m) => m.migrateCommand),
|
|
143
|
+
mcp: () => import("./mcp-CH7L4GF3.js").then((m) => m.mcpCommand)
|
|
118
144
|
},
|
|
119
145
|
// Shown for a bare `apex` (no subcommand): the brand banner + a command menu.
|
|
120
146
|
run({ rawArgs }) {
|
|
121
147
|
if (rawArgs.length > 0) return;
|
|
122
148
|
process.stdout.write(banner());
|
|
123
149
|
const log = console.log;
|
|
124
|
-
log(
|
|
125
|
-
`)
|
|
150
|
+
log(
|
|
151
|
+
` ${color.bold("Usage")} ${color.gray("apex")} ${color.cyan("<command>")} ${color.gray("[options]")}
|
|
152
|
+
`
|
|
153
|
+
);
|
|
126
154
|
log(` ${color.bold("Commands")}`);
|
|
127
155
|
for (const [name, desc] of COMMANDS) {
|
|
128
156
|
log(` ${color.cyan(`apex ${name}`.padEnd(13))} ${color.gray(desc)}`);
|
|
129
157
|
}
|
|
130
|
-
log(
|
|
158
|
+
log(
|
|
159
|
+
`
|
|
131
160
|
${color.gray("Run")} ${color.cyan("apex <command> --help")} ${color.gray("for details.")}
|
|
132
|
-
`
|
|
161
|
+
`
|
|
162
|
+
);
|
|
133
163
|
}
|
|
134
164
|
});
|
|
135
165
|
runMain(main);
|
|
@@ -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: {
|
|
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-
|
|
27
|
+
const { startDevServer } = await import("./server-62UM2N5C.js");
|
|
24
28
|
const { port: actual } = await startDevServer({ root, port, islands: Boolean(args.islands) });
|
|
25
29
|
sp.succeed("Dev server ready");
|
|
26
30
|
ready([
|
|
@@ -79,32 +79,74 @@ export default defineApexRoute({
|
|
|
79
79
|
})
|
|
80
80
|
`;
|
|
81
81
|
}
|
|
82
|
+
function serviceTemplate(name) {
|
|
83
|
+
const cls = `${pascalCase(name)}Service`;
|
|
84
|
+
return `/**
|
|
85
|
+
* ${cls} \u2014 business logic as a plain, testable class. Keep routes and loaders
|
|
86
|
+
* thin and delegate to services like this one (the clean-code backbone).
|
|
87
|
+
*/
|
|
88
|
+
export class ${cls} {
|
|
89
|
+
// Replace with your methods.
|
|
90
|
+
run(input: string): string {
|
|
91
|
+
return input
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function testTemplate(name) {
|
|
97
|
+
return `import { describe, expect, it } from 'vitest'
|
|
98
|
+
|
|
99
|
+
describe('${name}', () => {
|
|
100
|
+
it('works', () => {
|
|
101
|
+
expect(true).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
82
106
|
function plan(kind, name, root) {
|
|
83
107
|
switch (kind) {
|
|
84
108
|
case "page":
|
|
85
109
|
return { path: join(root, "pages", `${name}.alpine`), contents: pageTemplate(name) };
|
|
86
110
|
case "component":
|
|
87
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
path: join(root, "components", `${pascalCase(name)}.alpine`),
|
|
113
|
+
contents: componentTemplate()
|
|
114
|
+
};
|
|
88
115
|
case "api":
|
|
89
116
|
return { path: join(root, "server", "api", `${name}.ts`), contents: apiTemplate(name) };
|
|
90
117
|
case "store":
|
|
91
118
|
return { path: join(root, "stores", `${name}.ts`), contents: storeTemplate(name) };
|
|
92
119
|
case "layout":
|
|
93
120
|
return { path: join(root, "layouts", `${name}.alpine`), contents: layoutTemplate() };
|
|
121
|
+
case "service":
|
|
122
|
+
return {
|
|
123
|
+
path: join(root, "services", `${pascalCase(name)}Service.ts`),
|
|
124
|
+
contents: serviceTemplate(name)
|
|
125
|
+
};
|
|
126
|
+
case "test":
|
|
127
|
+
return { path: join(root, "tests", `${name}.test.ts`), contents: testTemplate(name) };
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
130
|
var makeCommand = defineCommand({
|
|
97
|
-
meta: {
|
|
131
|
+
meta: {
|
|
132
|
+
name: "make",
|
|
133
|
+
description: "Generate a page, component, API route, store, layout, service, or test"
|
|
134
|
+
},
|
|
98
135
|
args: {
|
|
99
|
-
kind: {
|
|
136
|
+
kind: {
|
|
137
|
+
type: "positional",
|
|
138
|
+
required: true,
|
|
139
|
+
description: "page | component | api | store | layout | service | test"
|
|
140
|
+
},
|
|
100
141
|
name: { type: "positional", required: true, description: "Name (about, Counter, todos, \u2026)" },
|
|
101
142
|
root: { type: "string", description: "Project root", default: "." }
|
|
102
143
|
},
|
|
103
144
|
run({ args }) {
|
|
104
145
|
const kind = args.kind;
|
|
105
|
-
|
|
146
|
+
const kinds = ["page", "component", "api", "store", "layout", "service", "test"];
|
|
147
|
+
if (!kinds.includes(kind)) {
|
|
106
148
|
console.error(`
|
|
107
|
-
Unknown type "${args.kind}". Use:
|
|
149
|
+
Unknown type "${args.kind}". Use: ${kinds.join(" | ")}
|
|
108
150
|
`);
|
|
109
151
|
process.exit(1);
|
|
110
152
|
}
|
|
@@ -30,7 +30,7 @@ var mcpCommand = defineCommand({
|
|
|
30
30
|
console.log(`
|
|
31
31
|
\x1B[36m${args.call}\x1B[0m(${args.args}) \u2192`);
|
|
32
32
|
for (const part of result.content) {
|
|
33
|
-
console.log(
|
|
33
|
+
console.log(` ${part.text ?? JSON.stringify(part)}`);
|
|
34
34
|
}
|
|
35
35
|
console.log();
|
|
36
36
|
} else {
|
|
@@ -19,7 +19,9 @@ var migrateCommand = defineCommand({
|
|
|
19
19
|
const require2 = createRequire(join(root, "package.json"));
|
|
20
20
|
data = await import(pathToFileURL(require2.resolve("@apex-stack/data")).href);
|
|
21
21
|
} catch {
|
|
22
|
-
console.error(
|
|
22
|
+
console.error(
|
|
23
|
+
"\n @apex-stack/data is not installed in this project. Run: npm i @apex-stack/data\n"
|
|
24
|
+
);
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
25
27
|
const config = args.driver === "postgres" ? { driver: "postgres", url: args.url } : args.driver === "pglite" ? { driver: "pglite", dir: args.url } : resolve(root, args.db);
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createApiHandler,
|
|
6
6
|
createMcpHandler,
|
|
7
7
|
loadApiRoutes
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-G77MLFUJ.js";
|
|
9
9
|
import "./chunk-HRJTOSYH.js";
|
|
10
10
|
import {
|
|
11
11
|
loadStores,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderIslandsPage,
|
|
14
14
|
renderPage,
|
|
15
15
|
scanPages
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-4FUWZLVW.js";
|
|
17
17
|
import "./chunk-MZVLRU3R.js";
|
|
18
18
|
|
|
19
19
|
// src/dev/server.ts
|
|
@@ -36,20 +36,24 @@ import { createServer as createViteServer } from "vite";
|
|
|
36
36
|
// src/dev/errorPage.ts
|
|
37
37
|
import { existsSync, readFileSync } from "fs";
|
|
38
38
|
function esc(s) {
|
|
39
|
-
return s.replace(
|
|
39
|
+
return s.replace(
|
|
40
|
+
/[&<>"]/g,
|
|
41
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """
|
|
42
|
+
);
|
|
40
43
|
}
|
|
41
44
|
function firstFileFrame(stack, root) {
|
|
42
45
|
const re = /(?:file:\/\/\/?)?((?:[A-Za-z]:[\\/]|\/)[^\s():]+):(\d+):(\d+)/g;
|
|
43
|
-
let m;
|
|
44
46
|
const frames = [];
|
|
45
|
-
|
|
47
|
+
let m = re.exec(stack);
|
|
48
|
+
while (m) {
|
|
46
49
|
const raw = m[1];
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (raw && (/^[A-Za-z]:[\\/]/.test(raw) || raw.startsWith("/"))) {
|
|
51
|
+
const file = raw.replace(/\//g, process.platform === "win32" ? "\\" : "/");
|
|
52
|
+
if (existsSync(file) && !file.includes("node_modules")) {
|
|
53
|
+
frames.push({ file, line: Number(m[2] ?? 0), col: Number(m[3] ?? 0) });
|
|
54
|
+
}
|
|
52
55
|
}
|
|
56
|
+
m = re.exec(stack);
|
|
53
57
|
}
|
|
54
58
|
return frames.find((f) => f.file.startsWith(root)) ?? frames[0];
|
|
55
59
|
}
|
|
@@ -174,7 +178,9 @@ async function startDevServer(options) {
|
|
|
174
178
|
plugins.unshift(tw());
|
|
175
179
|
} catch {
|
|
176
180
|
}
|
|
177
|
-
const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find(
|
|
181
|
+
const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find(
|
|
182
|
+
(p) => existsSync2(join(options.root, p))
|
|
183
|
+
);
|
|
178
184
|
const appCss = appCssRel ? `/${appCssRel}` : void 0;
|
|
179
185
|
const vite = await createViteServer({
|
|
180
186
|
root: options.root,
|
|
@@ -195,8 +201,14 @@ async function startDevServer(options) {
|
|
|
195
201
|
const app = createApp();
|
|
196
202
|
app.use(fromNodeMiddleware(vite.middlewares));
|
|
197
203
|
const loadEntries = () => loadApiRoutes(options.root, (id) => ssrLoad(id));
|
|
198
|
-
app.use(
|
|
199
|
-
|
|
204
|
+
app.use(
|
|
205
|
+
"/api",
|
|
206
|
+
defineEventHandler((event) => loadEntries().then((e) => createApiHandler(e)(event)))
|
|
207
|
+
);
|
|
208
|
+
app.use(
|
|
209
|
+
"/mcp",
|
|
210
|
+
defineEventHandler((event) => loadEntries().then((e) => createMcpHandler(e)(event)))
|
|
211
|
+
);
|
|
200
212
|
app.use(
|
|
201
213
|
defineEventHandler(async (event) => {
|
|
202
214
|
const url = event.path || "/";
|
|
@@ -252,9 +264,7 @@ async function startDevServer(options) {
|
|
|
252
264
|
port,
|
|
253
265
|
close: async () => {
|
|
254
266
|
await vite.close();
|
|
255
|
-
await new Promise(
|
|
256
|
-
(resolve, reject) => server.close((e) => e ? reject(e) : resolve())
|
|
257
|
-
);
|
|
267
|
+
await new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()));
|
|
258
268
|
}
|
|
259
269
|
};
|
|
260
270
|
}
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
createMcpHandler,
|
|
4
4
|
expandApiModule,
|
|
5
5
|
hasMcpRoutes
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-G77MLFUJ.js";
|
|
7
7
|
import "./chunk-HRJTOSYH.js";
|
|
8
8
|
import {
|
|
9
9
|
matchRoute,
|
|
10
10
|
renderIslandsPage,
|
|
11
11
|
renderPage
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4FUWZLVW.js";
|
|
13
13
|
import "./chunk-MZVLRU3R.js";
|
|
14
14
|
|
|
15
15
|
// src/commands/start.ts
|
|
@@ -18,8 +18,8 @@ import { join as join2, resolve } from "path";
|
|
|
18
18
|
import { defineCommand } from "citty";
|
|
19
19
|
|
|
20
20
|
// src/prod/server.ts
|
|
21
|
-
import { createServer as createHttpServer } from "http";
|
|
22
21
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
22
|
+
import { createServer as createHttpServer } from "http";
|
|
23
23
|
import { join } from "path";
|
|
24
24
|
import { pathToFileURL } from "url";
|
|
25
25
|
import {
|
|
@@ -72,7 +72,8 @@ async function startProdServer(options) {
|
|
|
72
72
|
if (!file.startsWith(dir) || !existsSync(file) || !statSync(file).isFile()) return;
|
|
73
73
|
const ext = path.slice(path.lastIndexOf("."));
|
|
74
74
|
setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
75
|
-
if (path.startsWith("/assets/"))
|
|
75
|
+
if (path.startsWith("/assets/"))
|
|
76
|
+
setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
|
|
76
77
|
return readFileSync(file);
|
|
77
78
|
})
|
|
78
79
|
);
|
package/package.json
CHANGED
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
# {{name}}
|
|
2
2
|
|
|
3
|
-
An [Apex JS](https://
|
|
4
|
-
Alpine.js that renders on the server and hydrates in the browser.
|
|
3
|
+
An [Apex JS](https://apexjs.site) app — HTML-first, server-rendered, AI-native.
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## Commands
|
|
7
6
|
|
|
8
7
|
```bash
|
|
9
|
-
npm
|
|
10
|
-
npm run dev
|
|
8
|
+
npm run dev # dev server → http://localhost:3000
|
|
9
|
+
npm run dev:islands # static-first islands mode (ship ~zero JS)
|
|
10
|
+
npm run build # production build
|
|
11
|
+
npm start # run the production server build
|
|
12
|
+
npm test # run tests (Vitest)
|
|
13
|
+
npm run typecheck # strict type-check
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
> `apex` is a project command — run it via `npm run dev`, or install it globally
|
|
17
|
+
> (`npm i -g @apex-stack/core`) to use `apex dev` directly.
|
|
14
18
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
Ship interactive JavaScript only where you need it:
|
|
19
|
+
## Project structure
|
|
18
20
|
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
+
```
|
|
22
|
+
pages/ File-based routes (.alpine) — server-rendered, then hydrated.
|
|
23
|
+
layouts/ Shared page shells; default.alpine wraps every page (<slot/>).
|
|
24
|
+
components/ Reusable <PascalCase/> components with scoped styles.
|
|
25
|
+
server/api/ Typed routes (defineApexRoute) — each is a REST endpoint AND an MCP tool.
|
|
26
|
+
services/ Business logic as plain OO classes. Keep routes thin; delegate here.
|
|
27
|
+
shared/ Types/interfaces shared by the backend and the frontend.
|
|
28
|
+
stores/ Global, SSR-safe state — $store.x, reactive across pages/islands.
|
|
29
|
+
composables/ Reusable client logic (useX) for <script client> blocks.
|
|
30
|
+
tests/ Vitest tests. `npm test` runs them.
|
|
31
|
+
db/ Optional: a database + resources. See db/README.md.
|
|
32
|
+
public/ Static assets served as-is.
|
|
21
33
|
```
|
|
22
34
|
|
|
23
|
-
##
|
|
35
|
+
## Conventions (clean code)
|
|
36
|
+
|
|
37
|
+
- **Thin routes → services.** A route/loader validates input and delegates to a service
|
|
38
|
+
class in `services/`. Business logic stays testable in isolation and reusable everywhere.
|
|
39
|
+
- **Types live in `shared/`.** One source of truth; strict TypeScript enforces them across
|
|
40
|
+
backend and frontend — no drift.
|
|
41
|
+
- **Tests by default.** `npm test` runs Vitest (see `tests/greeting.test.ts`).
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
the server; its `loader()` return value becomes the Alpine `x-data` scope.
|
|
27
|
-
- `server/api/*.ts` — API routes defined with `defineApexRoute`.
|
|
43
|
+
## Generators
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
```bash
|
|
46
|
+
apex make page about
|
|
47
|
+
apex make component Card
|
|
48
|
+
apex make api todos
|
|
49
|
+
apex make service Billing # → services/BillingService.ts (OO class)
|
|
50
|
+
apex make store cart
|
|
51
|
+
apex make layout marketing
|
|
52
|
+
apex make test billing # → tests/billing.test.ts
|
|
53
|
+
```
|
|
30
54
|
|
|
31
|
-
|
|
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.
|
|
55
|
+
Full docs: https://apexjs.site
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template x-data="{ count: Number(start) }">
|
|
2
|
+
<button class="counter" @click="count++" x-text="label + ': ' + count"></button>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<style scoped>
|
|
6
|
+
.counter {
|
|
7
|
+
padding: 0.45rem 0.9rem;
|
|
8
|
+
border: 1px solid #6366f1;
|
|
9
|
+
border-radius: 0.5rem;
|
|
10
|
+
background: #eef2ff;
|
|
11
|
+
color: #3730a3;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable client logic. Import it in a <script client> block and use it in x-data:
|
|
3
|
+
*
|
|
4
|
+
* <script client> import { useToggle } from '../composables/useToggle' </script>
|
|
5
|
+
* <template x-data="useToggle(true)"> <button @click="toggle()" x-text="on"></button> </template>
|
|
6
|
+
*/
|
|
7
|
+
export function useToggle(initial = false) {
|
|
8
|
+
return {
|
|
9
|
+
on: initial,
|
|
10
|
+
toggle() {
|
|
11
|
+
this.on = !this.on
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# db/
|
|
2
|
+
|
|
3
|
+
Data is opt-in. To add a database with resources that are REST **and** MCP by default:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i @apex-stack/data @libsql/client # install only the driver you use
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then add `db/schema.ts` (Drizzle tables), `db/index.ts` (`createDb` + `applyMigrations`),
|
|
10
|
+
and a resource in `server/api/*.ts`:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineResource } from '@apex-stack/data'
|
|
14
|
+
export default defineResource('posts', { db, table: schema.posts, insert: { title: z.string() } })
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That one line gives you `GET/POST/PATCH/DELETE /api/posts` plus the MCP tools
|
|
18
|
+
`posts_list/get/create/update/delete`. See https://apexjs.site/data.html
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header class="site-header">
|
|
3
|
+
<a class="brand" href="/">{{name}}</a>
|
|
4
|
+
</header>
|
|
5
|
+
<main>
|
|
6
|
+
<slot></slot>
|
|
7
|
+
</main>
|
|
8
|
+
<footer class="site-footer">Built with Apex JS</footer>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<style scoped>
|
|
12
|
+
.site-header { padding: 1rem 1.5rem; border-bottom: 1px solid #e2e8f0; }
|
|
13
|
+
.brand { font-weight: 700; color: #2563eb; text-decoration: none; }
|
|
14
|
+
main { max-width: 44rem; margin: 0 auto; padding: 2.5rem 1.5rem; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.6; color: #1e293b; }
|
|
15
|
+
.site-footer { padding: 1.5rem; text-align: center; color: #64748b; font-size: 0.9rem; border-top: 1px solid #e2e8f0; }
|
|
16
|
+
</style>
|
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "apex dev",
|
|
7
|
-
"dev:islands": "apex dev --islands"
|
|
7
|
+
"dev:islands": "apex dev --islands",
|
|
8
|
+
"build": "apex build",
|
|
9
|
+
"start": "apex start",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
8
13
|
},
|
|
9
14
|
"dependencies": {
|
|
10
|
-
"alpinejs": "^3.14.8",
|
|
11
15
|
"@apex-stack/core": "latest",
|
|
16
|
+
"alpinejs": "^3.14.8",
|
|
12
17
|
"zod": "^4.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.6.0",
|
|
21
|
+
"vitest": "^3.0.0"
|
|
13
22
|
}
|
|
14
23
|
}
|
|
@@ -1,107 +1,38 @@
|
|
|
1
1
|
<script server lang="ts">
|
|
2
2
|
export function loader() {
|
|
3
3
|
return {
|
|
4
|
-
title: 'Welcome to
|
|
4
|
+
title: 'Welcome to {{name}}',
|
|
5
5
|
tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
<template x-data="{ open: false }">
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
<p class="tagline" x-text="tagline"></p>
|
|
11
|
+
<h1 x-text="title"></h1>
|
|
12
|
+
<p class="tagline" x-text="tagline"></p>
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
14
|
+
<section class="card">
|
|
15
|
+
<p>
|
|
16
|
+
This was rendered on the server from <code>pages/index.alpine</code>, wrapped in
|
|
17
|
+
<code>layouts/default.alpine</code>, then hydrated by Alpine. Edit and save to see it live.
|
|
40
18
|
</p>
|
|
41
|
-
|
|
19
|
+
<button type="button" @click="open = !open" x-text="open ? 'Hide' : 'Show me how'"></button>
|
|
20
|
+
<div x-show="open" x-transition class="details">
|
|
21
|
+
<p>The <code>loader()</code> runs on the server and hands its data to Alpine's <code>x-data</code>.</p>
|
|
22
|
+
<p>Open <code>server/api/hello.ts</code>: a thin route → <code>GreetingService</code>, exposed as REST <em>and</em> an MCP tool.</p>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<p class="hint">A reusable component that hydrates in the browser:</p>
|
|
27
|
+
<Counter start="0" label="Clicks" />
|
|
42
28
|
</template>
|
|
43
29
|
|
|
44
30
|
<style scoped>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
}
|
|
31
|
+
h1 { color: #2563eb; font-size: 2.25rem; margin-bottom: 0.25rem; }
|
|
32
|
+
.tagline { font-size: 1.1rem; color: #475569; margin-top: 0; }
|
|
33
|
+
.card { margin-top: 1.5rem; padding: 1.25rem; border: 1px solid #e2e8f0; border-radius: 0.75rem; background: #f8fafc; }
|
|
34
|
+
.card button { margin-top: 0.5rem; padding: 0.55rem 1rem; font-weight: 600; color: #fff; background: #2563eb; border: none; border-radius: 0.5rem; cursor: pointer; }
|
|
35
|
+
.details { margin-top: 1rem; }
|
|
36
|
+
code { padding: 0.1rem 0.35rem; background: #e2e8f0; border-radius: 0.35rem; font-size: 0.9em; }
|
|
37
|
+
.hint { margin-top: 1.5rem; color: #64748b; font-size: 0.95rem; }
|
|
107
38
|
</style>
|
|
File without changes
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { defineApexRoute } from '@apex-stack/core'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
+
import { GreetingService } from '../../services/GreetingService'
|
|
3
4
|
|
|
5
|
+
const greetings = new GreetingService()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A route is a thin adapter: validate input, delegate to a service, return the
|
|
9
|
+
* result. Because `mcp: true`, this is ALSO an MCP tool named "hello" at /mcp —
|
|
10
|
+
* one definition, REST + AI-callable.
|
|
11
|
+
*/
|
|
4
12
|
export default defineApexRoute({
|
|
5
13
|
method: 'GET',
|
|
6
|
-
description: '
|
|
14
|
+
description: 'Greet someone by name',
|
|
7
15
|
input: { name: z.string() },
|
|
8
16
|
mcp: true,
|
|
9
|
-
handler: ({ input }) => (
|
|
17
|
+
handler: ({ input }) => greetings.greet(input.name),
|
|
10
18
|
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Greeting } from '../shared/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A service holds business logic as a plain class — testable in isolation and
|
|
5
|
+
* reusable from routes, page loaders, and jobs. Keep routes/loaders thin: they
|
|
6
|
+
* validate input and delegate to a service. This is the clean-code backbone.
|
|
7
|
+
*/
|
|
8
|
+
export class GreetingService {
|
|
9
|
+
greet(name: string): Greeting {
|
|
10
|
+
return { message: `Hello, ${name}!`, at: new Date().toISOString() }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types — the single source of truth for shapes used across the app,
|
|
3
|
+
* on the BACKEND (routes, services) and the FRONTEND. Import from '../shared/types'.
|
|
4
|
+
*
|
|
5
|
+
* Defining types here (instead of inline) is what keeps a growing codebase clean:
|
|
6
|
+
* one place to change a shape, and the compiler enforces it everywhere it's used.
|
|
7
|
+
*/
|
|
8
|
+
export interface Greeting {
|
|
9
|
+
message: string
|
|
10
|
+
at: string
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { GreetingService } from '../services/GreetingService'
|
|
3
|
+
|
|
4
|
+
// Services are plain classes, so they test in isolation — no server needed.
|
|
5
|
+
// Generate more with: apex make test <name>
|
|
6
|
+
describe('GreetingService', () => {
|
|
7
|
+
it('greets by name', () => {
|
|
8
|
+
const g = new GreetingService().greet('Apex')
|
|
9
|
+
expect(g.message).toBe('Hello, Apex!')
|
|
10
|
+
expect(typeof g.at).toBe('string')
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"noImplicitOverride": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["server", "services", "shared", "stores", "composables", "tests", "db"]
|
|
15
|
+
}
|