@apex-stack/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/{build-VHS6KZBK.js → build-NA6JEP3F.js} +3 -3
  2. package/dist/{chunk-XDKJO6ZC.js → chunk-7CBGVRBB.js} +183 -159
  3. package/dist/chunk-P6KQSPAV.js +95 -0
  4. package/dist/{chunk-JLIAISWM.js → chunk-PMLGY6Z3.js} +14 -14
  5. package/dist/cli.js +6 -6
  6. package/dist/client.js +1 -1
  7. package/dist/{dev-G7HPP6KW.js → dev-6SH5IYNT.js} +1 -1
  8. package/dist/index.d.ts +25 -25
  9. package/dist/index.js +1 -1
  10. package/dist/server-SSHQHMWY.js +479 -0
  11. package/dist/{start-3O3E43PT.js → start-7HSIYO6C.js} +2 -2
  12. package/dist/{upgrade-WC5F5FKY.js → upgrade-KGUW3C3O.js} +2 -2
  13. package/package.json +3 -3
  14. package/templates/default/README.md +22 -14
  15. package/templates/default/components/Badge.alpine +6 -0
  16. package/templates/default/components/Button.alpine +10 -0
  17. package/templates/default/components/Card.alpine +6 -0
  18. package/templates/default/components/Counter.alpine +8 -13
  19. package/templates/default/layouts/default.alpine +39 -14
  20. package/templates/default/package.json +2 -1
  21. package/templates/default/pages/about.alpine +29 -0
  22. package/templates/default/pages/blog/[slug].alpine +38 -0
  23. package/templates/default/pages/blog/index.alpine +37 -0
  24. package/templates/default/pages/index.alpine +59 -25
  25. package/templates/default/public/favicon.svg +11 -0
  26. package/templates/default/server/api/posts.ts +20 -0
  27. package/templates/default/services/PostService.ts +51 -0
  28. package/templates/default/shared/types.ts +9 -4
  29. package/templates/default/tests/posts.test.ts +22 -0
  30. package/vscode/apex-alpine.vsix +0 -0
  31. package/vscode/version.txt +1 -0
  32. package/dist/chunk-CHBSGOB3.js +0 -42
  33. package/dist/server-PTHGOE42.js +0 -307
  34. package/templates/default/server/api/hello.ts +0 -18
  35. package/templates/default/services/GreetingService.ts +0 -12
  36. package/templates/default/tests/greeting.test.ts +0 -12
@@ -0,0 +1,479 @@
1
+ import {
2
+ openInEditor
3
+ } from "./chunk-P6KQSPAV.js";
4
+ import {
5
+ loadComponents
6
+ } from "./chunk-4VG3CZ6H.js";
7
+ import {
8
+ createApiHandler,
9
+ createMcpHandler,
10
+ loadApiRoutes,
11
+ loadMiddleware,
12
+ runMiddleware
13
+ } from "./chunk-HCNNKT4A.js";
14
+ import "./chunk-2C2HRLIY.js";
15
+ import {
16
+ loadStores,
17
+ matchRoute,
18
+ renderIslandsPage,
19
+ renderPage,
20
+ resolveApexConfig,
21
+ scanPages
22
+ } from "./chunk-7CBGVRBB.js";
23
+ import "./chunk-PMLGY6Z3.js";
24
+
25
+ // src/dev/server.ts
26
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
27
+ import { createServer as createHttpServer } from "http";
28
+ import { createRequire } from "module";
29
+ import { join as join2, resolve } from "path";
30
+ import { fileURLToPath, pathToFileURL } from "url";
31
+ import { apex } from "@apex-stack/vite";
32
+ import {
33
+ createApp,
34
+ defineEventHandler,
35
+ fromNodeMiddleware,
36
+ getQuery,
37
+ getRequestHeaders,
38
+ setResponseHeader,
39
+ setResponseStatus,
40
+ toNodeListener
41
+ } from "h3";
42
+ import { createServer as createViteServer } from "vite";
43
+
44
+ // src/dev/errorPage.ts
45
+ import { existsSync, readdirSync, readFileSync } from "fs";
46
+ import { join, relative, sep } from "path";
47
+ function esc(s) {
48
+ return s.replace(
49
+ /[&<>"]/g,
50
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;"
51
+ );
52
+ }
53
+ var escAttr = esc;
54
+ function parseFrames(stack, root) {
55
+ const frames = [];
56
+ const re = /^\s*at\s+(?:(.*?)\s+\()?(?:file:\/\/\/?)?((?:[A-Za-z]:[\\/]|\/)[^\s():]+):(\d+):(\d+)\)?\s*$/;
57
+ for (const line of stack.split("\n")) {
58
+ const m = line.match(re);
59
+ if (!m || !m[2]) continue;
60
+ const file = m[2].replace(/[\\/]/g, sep);
61
+ if (!existsSync(file)) continue;
62
+ frames.push({
63
+ func: m[1]?.trim() || "<anonymous>",
64
+ file,
65
+ line: Number(m[3] ?? 0),
66
+ col: Number(m[4] ?? 0),
67
+ app: file.startsWith(root) && !file.includes(`${sep}node_modules${sep}`)
68
+ });
69
+ }
70
+ return frames;
71
+ }
72
+ function codeContext(file, line) {
73
+ let lines;
74
+ try {
75
+ lines = readFileSync(file, "utf8").split("\n");
76
+ } catch {
77
+ return "";
78
+ }
79
+ const start = Math.max(0, line - 5);
80
+ const end = Math.min(lines.length, line + 4);
81
+ const rows = [];
82
+ for (let i = start; i < end; i++) {
83
+ const n = i + 1;
84
+ const active = n === line;
85
+ rows.push(
86
+ `<div class="cf-row${active ? " active" : ""}"><span class="cf-num">${String(n).padStart(4, " ")}</span><span class="cf-code">${esc(lines[i] || "")}</span></div>`
87
+ );
88
+ }
89
+ return `<pre class="cf">${rows.join("")}</pre>`;
90
+ }
91
+ var OPEN_ICON = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M6.5 3H3.5A1.5 1.5 0 0 0 2 4.5v8A1.5 1.5 0 0 0 3.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-3M9 2h5v5M13.5 2.5 7 9"/></svg>';
92
+ function openAttrs(f) {
93
+ return `data-file="${escAttr(f.file)}" data-line="${f.line}" data-col="${f.col}"`;
94
+ }
95
+ function frameCard(f, root) {
96
+ const loc = f.app ? relative(root, f.file).replace(/\\/g, "/") : f.file.replace(/\\/g, "/");
97
+ const ctx = f.app ? codeContext(f.file, f.line) : "";
98
+ const openBtn = `<button class="fr-open" ${openAttrs(f)} title="Open in editor">${OPEN_ICON}</button>`;
99
+ return `<details class="fr${f.app ? " app" : ""}"${f.app ? " open" : ""}>
100
+ <summary><span class="fr-fn">${esc(f.func)}</span><span class="fr-loc" ${openAttrs(f)}>${esc(loc)}<span class="fr-pos">:${f.line}:${f.col}</span></span>${f.app ? openBtn : ""}</summary>
101
+ ${ctx}
102
+ </details>`;
103
+ }
104
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
105
+ "node_modules",
106
+ "dist",
107
+ "build",
108
+ "out",
109
+ "coverage",
110
+ ".git",
111
+ ".vite",
112
+ ".cache",
113
+ ".turbo",
114
+ ".next"
115
+ ]);
116
+ var FOLDER_ICON = '<svg class="ti" viewBox="0 0 16 16" fill="#f59e0b" aria-hidden="true"><path d="M1.5 3.5A1.5 1.5 0 0 1 3 2h3l1.3 1.5H13A1.5 1.5 0 0 1 14.5 5v7A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12z"/></svg>';
117
+ var EXT_COLOR = {
118
+ alpine: "#818cf8",
119
+ ts: "#3b82f6",
120
+ tsx: "#3b82f6",
121
+ js: "#eab308",
122
+ mjs: "#eab308",
123
+ json: "#eab308",
124
+ css: "#22d3ee",
125
+ html: "#f97316",
126
+ svg: "#22d3ee",
127
+ md: "#94a3b8"
128
+ };
129
+ function fileIcon(name) {
130
+ const ext = name.includes(".") ? name.split(".").pop() ?? "" : "";
131
+ const c = EXT_COLOR[ext] ?? "#8892b0";
132
+ return `<svg class="ti" viewBox="0 0 16 16" fill="${c}" aria-hidden="true"><path d="M4 1.5h5L12.5 5v9a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2a.5.5 0 0 1 .5-.5z" opacity=".92"/></svg>`;
133
+ }
134
+ function fileTree(root, errorFile) {
135
+ const errRel = errorFile ? relative(root, errorFile).replace(/\\/g, "/") : "";
136
+ let count = 0;
137
+ const walk = (dir, rel, depth) => {
138
+ if (count > 600 || depth > 7) return "";
139
+ let entries;
140
+ try {
141
+ entries = readdirSync(dir, { withFileTypes: true });
142
+ } catch {
143
+ return "";
144
+ }
145
+ entries = entries.filter((e) => !IGNORE_DIRS.has(e.name) && !e.name.startsWith(".")).sort(
146
+ (a, b) => a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1
147
+ );
148
+ const items = [];
149
+ for (const e of entries) {
150
+ if (++count > 600) break;
151
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
152
+ if (e.isDirectory()) {
153
+ const onPath = errRel === childRel || errRel.startsWith(`${childRel}/`);
154
+ items.push(
155
+ `<details class="tdir"${onPath ? " open" : ""}><summary>${FOLDER_ICON}<span>${esc(e.name)}</span></summary>${walk(join(dir, e.name), childRel, depth + 1)}</details>`
156
+ );
157
+ } else {
158
+ const isErr = childRel === errRel;
159
+ const label = isErr ? `<button class="tfile-open" ${errorFile ? `data-file="${escAttr(errorFile)}" data-line="1" data-col="1"` : ""}>${esc(e.name)}</button>` : `<span>${esc(e.name)}</span>`;
160
+ items.push(`<div class="tfile${isErr ? " err" : ""}">${fileIcon(e.name)}${label}</div>`);
161
+ }
162
+ }
163
+ return `<div class="tchildren">${items.join("")}</div>`;
164
+ };
165
+ return walk(root, "", 0);
166
+ }
167
+ var SHELL_CSS = `
168
+ :root{color-scheme:dark light}
169
+ *{box-sizing:border-box}
170
+ body{margin:0;min-height:100vh;background:#0a0e1a;color:#f4f7ff;
171
+ font:15px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;padding:clamp(1.25rem,5vh,3rem) 1.25rem}
172
+ @media(prefers-color-scheme:light){body{background:#f4f7ff;color:#0a0e1a}}
173
+ .wrap{width:100%;max-width:1120px;margin:0 auto}
174
+ .brand{display:flex;align-items:center;gap:.55rem;font-weight:600;margin-bottom:1.4rem}
175
+ .mark{width:20px;height:20px}
176
+ .pill{font-size:.8rem;font-weight:600;padding:.15rem .6rem;border-radius:999px;margin-left:.4rem}
177
+ .pill.err{color:#fb7185;background:rgba(244,63,94,.12);border:1px solid rgba(244,63,94,.35)}
178
+ .pill.nf{color:#818cf8;background:rgba(129,140,248,.12);border:1px solid rgba(129,140,248,.35)}
179
+ .kind{font:600 .95rem/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#fb7185;margin:0 0 .5rem}
180
+ .kind.nf{color:#818cf8}
181
+ h1{font-size:clamp(1.3rem,3vw,1.9rem);line-height:1.25;letter-spacing:-.02em;margin:0 0 .6rem;word-break:break-word}
182
+ .sub{color:#9aa6c4;margin:0 0 1.6rem}
183
+ .sub code{background:rgba(130,140,200,.14);border-radius:.35rem;padding:.05rem .35rem;font-size:.9em}
184
+ .cols{display:grid;grid-template-columns:minmax(0,1fr);gap:1.4rem}
185
+ @media(min-width:900px){.cols{grid-template-columns:minmax(0,1fr) 19rem}}
186
+ .col-main{min-width:0}
187
+ .tabs{display:flex;gap:.4rem;margin:0 0 .9rem}
188
+ .tabs button{font:inherit;font-size:.82rem;font-weight:600;padding:.35rem .8rem;border-radius:.5rem;cursor:pointer;
189
+ color:#9aa6c4;background:transparent;border:1px solid rgba(130,140,200,.22)}
190
+ .tabs button.active{color:#0a0e1a;background:#818cf8;border-color:#818cf8}
191
+ .fr{border:1px solid rgba(130,140,200,.18);border-radius:10px;overflow:hidden;margin:0 0 .6rem;background:#11172b}
192
+ @media(prefers-color-scheme:light){.fr{background:#fff}}
193
+ .fr:not(.app){opacity:.62}
194
+ .fr summary{cursor:pointer;display:flex;align-items:center;gap:.6rem;padding:.55rem .8rem;font-size:.83rem;list-style:none}
195
+ .fr summary::-webkit-details-marker{display:none}
196
+ .fr-fn{font:600 .82rem/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f4f7ff;flex-shrink:0}
197
+ @media(prefers-color-scheme:light){.fr-fn{color:#0a0e1a}}
198
+ .fr-loc{font:.78rem/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#9aa6c4;margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
199
+ .fr.app .fr-loc:hover{color:#818cf8;text-decoration:underline}
200
+ .fr-pos{color:#fb7185}
201
+ .fr-open{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;
202
+ border:1px solid rgba(130,140,200,.25);background:transparent;color:#9aa6c4;cursor:pointer}
203
+ .fr-open:hover{color:#818cf8;border-color:#818cf8}
204
+ .fr-open.ok{color:#22c55e;border-color:#22c55e}
205
+ .cf{margin:0;padding:.5rem 0 .6rem;overflow-x:auto;font:.8rem/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;border-top:1px solid rgba(130,140,200,.14)}
206
+ .cf-row{display:flex;padding:0 .8rem;white-space:pre}
207
+ .cf-row.active{background:rgba(244,63,94,.12);box-shadow:inset 3px 0 0 #f43f5e}
208
+ .cf-num{color:#5b6890;user-select:none;padding-right:1rem;text-align:right}
209
+ .cf-row.active .cf-num{color:#fb7185}
210
+ .cf-code{color:#f4f7ff}
211
+ @media(prefers-color-scheme:light){.cf-code{color:#0a0e1a}}
212
+ .raw{margin:0;padding:.9rem;border:1px solid rgba(130,140,200,.18);border-radius:10px;background:#11172b;
213
+ font:.78rem/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;color:#9aa6c4;overflow-x:auto;white-space:pre-wrap;word-break:break-word}
214
+ @media(prefers-color-scheme:light){.raw{background:#fff}}
215
+ .tree{border:1px solid rgba(130,140,200,.18);border-radius:10px;background:#11172b;padding:.5rem;max-height:70vh;overflow:auto;
216
+ font:.8rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
217
+ @media(prefers-color-scheme:light){.tree{background:#fff}}
218
+ .tree-head{font:600 .72rem/1 system-ui;letter-spacing:.06em;text-transform:uppercase;color:#5b6890;padding:.2rem .3rem .5rem}
219
+ .ti{width:14px;height:14px;flex-shrink:0;vertical-align:-2px;margin-right:.35rem}
220
+ .tdir>summary{cursor:pointer;display:flex;align-items:center;padding:.12rem .3rem;border-radius:5px;list-style:none;color:#c7d0ea}
221
+ .tdir>summary::-webkit-details-marker{display:none}
222
+ .tdir>summary:hover{background:rgba(130,140,200,.1)}
223
+ @media(prefers-color-scheme:light){.tdir>summary{color:#334}}
224
+ .tchildren{padding-left:.85rem;border-left:1px solid rgba(130,140,200,.14);margin-left:.55rem}
225
+ .tfile{display:flex;align-items:center;padding:.12rem .3rem;border-radius:5px;color:#9aa6c4}
226
+ .tfile.err{color:#fb7185;font-weight:700;background:rgba(244,63,94,.1)}
227
+ .tfile-open{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-decoration:underline}
228
+ details{border:1px solid rgba(130,140,200,.18);border-radius:12px;background:#11172b}
229
+ @media(prefers-color-scheme:light){details{background:#fff}}
230
+ .routes{list-style:none;padding:0;margin:0 0 1.4rem;display:flex;flex-wrap:wrap;gap:.5rem}
231
+ .routes li{font:.85rem/1 ui-monospace,monospace;color:#818cf8;background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.25);border-radius:.45rem;padding:.35rem .6rem}
232
+ .foot{margin-top:1.8rem;color:#5b6890;font-size:.82rem}
233
+ `;
234
+ var OPEN_JS = `
235
+ document.addEventListener('click', function (e) {
236
+ var el = e.target.closest('[data-file]');
237
+ if (!el) return;
238
+ e.preventDefault(); e.stopPropagation();
239
+ fetch('/__apex_open?file=' + encodeURIComponent(el.dataset.file) + '&line=' + el.dataset.line + '&col=' + el.dataset.col)
240
+ .then(function (r) { if (r.ok && el.classList.contains('fr-open')) { el.classList.add('ok'); setTimeout(function(){el.classList.remove('ok')},1200); } })
241
+ .catch(function(){});
242
+ });
243
+ var tabs = document.querySelectorAll('.tabs button');
244
+ tabs.forEach(function (b) {
245
+ b.addEventListener('click', function () {
246
+ tabs.forEach(function (x) { x.classList.toggle('active', x === b); });
247
+ ['frames','raw','tree'].forEach(function (v) {
248
+ var el = document.getElementById('view-' + v);
249
+ if (el) el.hidden = v !== b.dataset.view;
250
+ });
251
+ });
252
+ });
253
+ `;
254
+ var MARK = `<svg class="mark" viewBox="0 0 64 64" aria-hidden="true"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#22d3ee"/><stop offset="1" stop-color="#6366f1"/></linearGradient></defs><path d="M32 7 L32 35 L20 56 L4 56 Z" fill="url(#g)"/><path d="M32 7 L60 56 L44 56 L32 35 Z" fill="#6366f1"/></svg>`;
255
+ function page(title, bodyInner, script = "") {
256
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${esc(title)}</title><style>${SHELL_CSS}</style></head><body><div class="wrap">${bodyInner}</div>${script ? `<script>${script}</script>` : ""}</body></html>`;
257
+ }
258
+ function renderErrorPage(error, opts) {
259
+ const kind = error.name || "Error";
260
+ const message = error.message || String(error);
261
+ const stack = error.stack || "";
262
+ const frames = parseFrames(stack, opts.root);
263
+ const origin = frames.find((f) => f.app) ?? frames[0];
264
+ const framesHtml = frames.length ? frames.map((f) => frameCard(f, opts.root)).join("") : '<p class="sub">No stack frames available.</p>';
265
+ const body = `
266
+ <div class="brand">${MARK} Apex JS <span class="pill err">Dev error</span></div>
267
+ <p class="kind">${esc(kind)}</p>
268
+ <h1>${esc(message)}</h1>
269
+ <p class="sub">While rendering <code>${esc(opts.url)}</code></p>
270
+ <div class="cols">
271
+ <div class="col-main">
272
+ <div class="tabs">
273
+ <button class="active" data-view="frames">Frames</button>
274
+ <button data-view="raw">Raw</button>
275
+ </div>
276
+ <div id="view-frames">${framesHtml}</div>
277
+ <div id="view-raw" hidden><pre class="raw">${esc(stack)}</pre></div>
278
+ </div>
279
+ <aside class="col-tree" id="view-tree">
280
+ <div class="tree"><div class="tree-head">Project \xB7 error origin in red</div>${fileTree(opts.root, origin?.file)}</div>
281
+ </aside>
282
+ </div>
283
+ <p class="foot">Click a frame's \u2197 to open it in your editor \xB7 fix &amp; save to auto-reload.</p>`;
284
+ return page(`${kind} \u2014 Apex JS`, body, OPEN_JS);
285
+ }
286
+ function renderNotFoundPage(url, routes) {
287
+ const list = routes.length ? routes.map((r) => `<li>${esc(r.pattern)}</li>`).join("") : "<li>no pages yet \u2014 add pages/index.alpine</li>";
288
+ const body = `
289
+ <div class="brand">${MARK} Apex JS <span class="pill nf">404</span></div>
290
+ <p class="kind nf">404 \u2014 Not found</p>
291
+ <h1>No route matches <code>${esc(url)}</code></h1>
292
+ <p class="sub">These routes are available:</p>
293
+ <ul class="routes">${list}</ul>
294
+ <p class="foot">Add a file under <code>pages/</code> to create a new route.</p>`;
295
+ return page("404 \u2014 Apex JS", body);
296
+ }
297
+
298
+ // src/dev/server.ts
299
+ async function startDevServer(options) {
300
+ const port = options.port ?? 3e3;
301
+ const pageId = options.pageId ?? "/pages/index.alpine";
302
+ const req = createRequire(import.meta.url);
303
+ const tryResolve = (spec) => {
304
+ try {
305
+ return req.resolve(spec);
306
+ } catch {
307
+ return void 0;
308
+ }
309
+ };
310
+ const alpine = tryResolve("alpinejs");
311
+ const kit = tryResolve("@apex-stack/kit");
312
+ const coreClient = fileURLToPath(new URL("./client.js", import.meta.url));
313
+ const coreSelf = fileURLToPath(new URL("./index.js", import.meta.url));
314
+ const alias = {
315
+ "@apex-stack/core/client": coreClient,
316
+ "@apex-stack/core": coreSelf
317
+ };
318
+ if (alpine) alias.alpinejs = alpine;
319
+ if (kit) alias["@apex-stack/kit"] = kit;
320
+ const plugins = [apex({ clientRuntime: "@apex-stack/core/client" })];
321
+ let hasTailwind = false;
322
+ try {
323
+ const reqProj = createRequire(join2(options.root, "package.json"));
324
+ const twMod = await import(pathToFileURL(reqProj.resolve("@tailwindcss/vite")).href);
325
+ const tw = twMod.default ?? twMod;
326
+ plugins.unshift(tw());
327
+ hasTailwind = true;
328
+ } catch {
329
+ }
330
+ const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find(
331
+ (p) => existsSync2(join2(options.root, p))
332
+ );
333
+ let appCss = appCssRel ? `/${appCssRel}` : void 0;
334
+ if (appCssRel && !hasTailwind) {
335
+ const css = readFileSync2(join2(options.root, appCssRel), "utf8");
336
+ if (/@import\s+['"]tailwindcss['"]|@tailwind\b/.test(css)) {
337
+ console.warn(
338
+ `
339
+ \u26A0 ${appCssRel} imports Tailwind, but it isn't installed \u2014 skipping it (styles won't apply).
340
+ Fix: npm i -D tailwindcss @tailwindcss/vite
341
+ `
342
+ );
343
+ appCss = void 0;
344
+ }
345
+ }
346
+ const vite = await createViteServer({
347
+ root: options.root,
348
+ appType: "custom",
349
+ // Derive the HMR port from the dev port so multiple `apex dev` instances
350
+ // don't all fight over Vite's default 24678.
351
+ server: { middlewareMode: true, fs: { strict: false }, hmr: { port: port + 1 } },
352
+ resolve: { alias },
353
+ // User apps depend on `@apex-stack/core`, so the client module imports the runtime
354
+ // from `@apex-stack/core/client` (a re-export) rather than the internal kit package.
355
+ plugins,
356
+ optimizeDeps: { include: ["alpinejs"] }
357
+ });
358
+ const ssrLoad = (id) => {
359
+ const resolved = id[0] === "/" && !id.startsWith(options.root) ? join2(options.root, id).replace(/\\/g, "/") : id;
360
+ return vite.ssrLoadModule(resolved);
361
+ };
362
+ const { runtimeConfig, publicConfig } = await resolveApexConfig(
363
+ options.root,
364
+ (id) => ssrLoad(id)
365
+ );
366
+ const app = createApp();
367
+ app.use(fromNodeMiddleware(vite.middlewares));
368
+ app.use(
369
+ defineEventHandler(async (event) => {
370
+ const mws = await loadMiddleware(options.root, (id) => ssrLoad(id));
371
+ if (!mws.length) return;
372
+ const { redirect, locals } = await runMiddleware(mws, {
373
+ url: event.path || "/",
374
+ method: event.method,
375
+ config: runtimeConfig,
376
+ headers: getRequestHeaders(event)
377
+ });
378
+ event.context.apexLocals = locals;
379
+ if (redirect) {
380
+ setResponseStatus(event, redirect.status);
381
+ setResponseHeader(event, "Location", redirect.to);
382
+ return "";
383
+ }
384
+ })
385
+ );
386
+ const loadEntries = () => loadApiRoutes(options.root, (id) => ssrLoad(id));
387
+ app.use(
388
+ "/api",
389
+ defineEventHandler(
390
+ (event) => loadEntries().then((e) => createApiHandler(e, runtimeConfig)(event))
391
+ )
392
+ );
393
+ app.use(
394
+ "/mcp",
395
+ defineEventHandler(
396
+ (event) => loadEntries().then((e) => createMcpHandler(e, runtimeConfig)(event))
397
+ )
398
+ );
399
+ app.use(
400
+ "/__apex_open",
401
+ defineEventHandler((event) => {
402
+ const q = getQuery(event);
403
+ const file = String(q.file ?? "");
404
+ const resolved = resolve(file);
405
+ if (!file || !resolved.startsWith(options.root)) {
406
+ setResponseStatus(event, 400);
407
+ return { ok: false };
408
+ }
409
+ const ok = openInEditor(resolved, Number(q.line) || 1, Number(q.col) || 1);
410
+ setResponseStatus(event, ok ? 200 : 500);
411
+ return { ok };
412
+ })
413
+ );
414
+ app.use(
415
+ defineEventHandler(async (event) => {
416
+ const url = event.path || "/";
417
+ try {
418
+ const routes = scanPages(options.root);
419
+ const matched = routes.length ? matchRoute(routes, url) : { pageId, params: {} };
420
+ if (!matched) {
421
+ setResponseStatus(event, 404);
422
+ setResponseHeader(event, "Content-Type", "text/html");
423
+ return await vite.transformIndexHtml(url, renderNotFoundPage(url, routes));
424
+ }
425
+ const { registry, css: componentCss } = await loadComponents(
426
+ options.root,
427
+ (id) => ssrLoad(id)
428
+ );
429
+ const stores = await loadStores(options.root, (id) => ssrLoad(id));
430
+ const layoutsDir = join2(options.root, "layouts");
431
+ const layouts = existsSync2(layoutsDir) ? readdirSync2(layoutsDir).filter((f) => f.endsWith(".alpine")).map((f) => f.replace(/\.alpine$/, "")) : [];
432
+ const render = options.islands ? renderIslandsPage : renderPage;
433
+ const html = await render({
434
+ loadModule: (id) => ssrLoad(id),
435
+ pageId: matched.pageId,
436
+ params: matched.params,
437
+ url,
438
+ registry,
439
+ componentCss,
440
+ stores,
441
+ appCss,
442
+ layouts,
443
+ runtimeConfig,
444
+ publicConfig,
445
+ locals: event.context.apexLocals ?? {},
446
+ errorPageId: existsSync2(join2(options.root, "pages", "error.alpine")) ? "/pages/error.alpine" : void 0,
447
+ transformHtml: (u, doc) => vite.transformIndexHtml(u, doc)
448
+ });
449
+ setResponseHeader(event, "Content-Type", "text/html");
450
+ return html;
451
+ } catch (err) {
452
+ const error = err;
453
+ vite.ssrFixStacktrace(error);
454
+ setResponseStatus(event, 500);
455
+ setResponseHeader(event, "Content-Type", "text/html");
456
+ const html = renderErrorPage(error, { url, root: options.root });
457
+ try {
458
+ return await vite.transformIndexHtml(url, html);
459
+ } catch {
460
+ return html;
461
+ }
462
+ }
463
+ })
464
+ );
465
+ const server = createHttpServer(toNodeListener(app));
466
+ await new Promise((resolve2) => server.listen(port, resolve2));
467
+ return {
468
+ vite,
469
+ server,
470
+ port,
471
+ close: async () => {
472
+ await vite.close();
473
+ await new Promise((resolve2, reject) => server.close((e) => e ? reject(e) : resolve2()));
474
+ }
475
+ };
476
+ }
477
+ export {
478
+ startDevServer
479
+ };
@@ -11,8 +11,8 @@ import {
11
11
  matchRoute,
12
12
  renderIslandsPage,
13
13
  renderPage
14
- } from "./chunk-XDKJO6ZC.js";
15
- import "./chunk-JLIAISWM.js";
14
+ } from "./chunk-7CBGVRBB.js";
15
+ import "./chunk-PMLGY6Z3.js";
16
16
 
17
17
  // src/commands/start.ts
18
18
  import { existsSync as existsSync2 } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  offerExtension
3
- } from "./chunk-CHBSGOB3.js";
3
+ } from "./chunk-P6KQSPAV.js";
4
4
  import {
5
5
  VERSION,
6
6
  banner,
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/commands/upgrade.ts
11
11
  import { spawnSync } from "child_process";
12
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
13
13
  import { basename, dirname, join, relative, resolve } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import { defineCommand } from "citty";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,8 +47,8 @@
47
47
  "h3": "^1.13.0",
48
48
  "vite": "^6.0.7",
49
49
  "zod": "^4.4.3",
50
- "@apex-stack/kit": "0.2.0",
51
- "@apex-stack/vite": "0.1.6",
50
+ "@apex-stack/kit": "0.3.0",
51
+ "@apex-stack/vite": "0.1.7",
52
52
  "@apex-stack/theme": "0.3.0"
53
53
  },
54
54
  "peerDependencies": {
@@ -2,15 +2,20 @@
2
2
 
3
3
  An [Apex JS](https://apexjs.site) app — HTML-first, server-rendered, AI-native.
4
4
 
5
+ This starter is a small, themed demo: a landing page, a blog (list + dynamic
6
+ `[slug]` detail) served from a sample-data service, an About page with SEO, a
7
+ dark-mode toggle, themeable components, and an API route that's also an MCP tool.
8
+
5
9
  ## Commands
6
10
 
7
11
  ```bash
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
12
+ npm run dev # dev server → http://localhost:3000
13
+ npm run dev:islands # static-first islands mode (ship ~zero JS)
14
+ npm run build # production build (server target: SSR + dynamic routes + API/MCP)
15
+ npm start # run the production server build
16
+ npm run build:static # static build — prerenders static pages (dynamic routes need the server target)
17
+ npm test # run tests (Vitest)
18
+ npm run typecheck # strict type-check
14
19
  ```
15
20
 
16
21
  > `apex` is a project command — run it via `npm run dev`, or install it globally
@@ -21,7 +26,7 @@ npm run typecheck # strict type-check
21
26
  ```
22
27
  pages/ File-based routes (.alpine) — server-rendered, then hydrated.
23
28
  layouts/ Shared page shells; default.alpine wraps every page (<slot/>).
24
- components/ Reusable <PascalCase/> components with scoped styles.
29
+ components/ Reusable <PascalCase/> components, themed with Apex tokens (Button, Card, Badge, Counter).
25
30
  server/api/ Typed routes (defineApexRoute) — each is a REST endpoint AND an MCP tool.
26
31
  services/ Business logic as plain OO classes. Keep routes thin; delegate here.
27
32
  shared/ Types/interfaces shared by the backend and the frontend.
@@ -38,7 +43,7 @@ public/ Static assets served as-is.
38
43
  class in `services/`. Business logic stays testable in isolation and reusable everywhere.
39
44
  - **Types live in `shared/`.** One source of truth; strict TypeScript enforces them across
40
45
  backend and frontend — no drift.
41
- - **Tests by default.** `npm test` runs Vitest (see `tests/greeting.test.ts`).
46
+ - **Tests by default.** `npm test` runs Vitest (see `tests/posts.test.ts`).
42
47
 
43
48
  ## Generators
44
49
 
@@ -53,13 +58,16 @@ apex make middleware auth # → middleware/auth.ts (runs on every request)
53
58
  apex make test billing # → tests/billing.test.ts
54
59
  ```
55
60
 
56
- ## Styling
61
+ ## Styling & theming
57
62
 
58
- - **Scoped by default.** A `<style scoped>` block in an `.alpine` file is scoped to that component.
59
- - **Global / shared styles.** Create `app.css` (also `styles/app.css` or `src/app.css`) in the
60
- project root Apex auto-loads and processes it (Vite HMR in dev, bundled in build).
61
- - **Tailwind.** `npm i tailwindcss @tailwindcss/vite`, then `@import "tailwindcss";` at the top of
62
- `app.css`. Apex auto-detects `@tailwindcss/vite` no extra config.
63
+ - **Tailwind + theme, preinstalled.** `app.css` already imports Tailwind and defines the Apex
64
+ theme tokens (`--color-primary`, `--radius-radius`, fonts, and a `dark` variant). Components use
65
+ token classes like `bg-primary` / `text-on-surface` / `rounded-radius`, so they all restyle at once.
66
+ - **Restyle everything:** `apex theme --primary "#4f46e5" --radius 0.5rem` rewrites the managed
67
+ `/* apex-theme */` block in `app.css`. Or design it visually at https://apexjs.site/theme.html.
68
+ - **Add components:** `apex add <name>` copies a themed component into `components/`. Browse them at
69
+ https://apexjs.site/ui.html.
70
+ - **Scoped styles** still work too: a `<style scoped>` block in an `.alpine` file is scoped to it.
63
71
 
64
72
  ## Config & environment
65
73
 
@@ -0,0 +1,6 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). -->
2
+ <template x-data>
3
+ <span class="inline-flex items-center gap-1 rounded-radius border border-outline bg-surface-alt px-2 py-0.5 text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
4
+ <slot>Badge</slot>
5
+ </span>
6
+ </template>
@@ -0,0 +1,10 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). Styles via theme tokens
2
+ (bg-primary / rounded-radius / …) — needs Tailwind + @apex-stack/theme. -->
3
+ <template x-data>
4
+ <button
5
+ type="button"
6
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius bg-primary border border-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark"
7
+ >
8
+ <slot>Button</slot>
9
+ </button>
10
+ </template>
@@ -0,0 +1,6 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). -->
2
+ <template>
3
+ <div class="flex flex-col gap-4 rounded-radius border border-outline bg-surface-alt p-6 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
4
+ <slot></slot>
5
+ </div>
6
+ </template>
@@ -1,15 +1,10 @@
1
+ <!-- A small interactive component, themed with Apex tokens. `start`/`label` are
2
+ props; the count lives in client state and updates on click (hydrated). -->
1
3
  <template x-data="{ count: Number(start) }">
2
- <button class="counter" @click="count++" x-text="label + ': ' + count"></button>
4
+ <button
5
+ type="button"
6
+ class="inline-flex items-center gap-2 rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm font-medium text-on-surface transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:focus-visible:outline-primary-dark"
7
+ @click="count++"
8
+ x-text="label + ': ' + count"
9
+ ></button>
3
10
  </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>