@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.
- package/dist/{build-VHS6KZBK.js → build-NA6JEP3F.js} +3 -3
- package/dist/{chunk-XDKJO6ZC.js → chunk-7CBGVRBB.js} +183 -159
- package/dist/chunk-P6KQSPAV.js +95 -0
- package/dist/{chunk-JLIAISWM.js → chunk-PMLGY6Z3.js} +14 -14
- package/dist/cli.js +6 -6
- package/dist/client.js +1 -1
- package/dist/{dev-G7HPP6KW.js → dev-6SH5IYNT.js} +1 -1
- package/dist/index.d.ts +25 -25
- package/dist/index.js +1 -1
- package/dist/server-SSHQHMWY.js +479 -0
- package/dist/{start-3O3E43PT.js → start-7HSIYO6C.js} +2 -2
- package/dist/{upgrade-WC5F5FKY.js → upgrade-KGUW3C3O.js} +2 -2
- package/package.json +3 -3
- package/templates/default/README.md +22 -14
- package/templates/default/components/Badge.alpine +6 -0
- package/templates/default/components/Button.alpine +10 -0
- package/templates/default/components/Card.alpine +6 -0
- package/templates/default/components/Counter.alpine +8 -13
- package/templates/default/layouts/default.alpine +39 -14
- package/templates/default/package.json +2 -1
- package/templates/default/pages/about.alpine +29 -0
- package/templates/default/pages/blog/[slug].alpine +38 -0
- package/templates/default/pages/blog/index.alpine +37 -0
- package/templates/default/pages/index.alpine +59 -25
- package/templates/default/public/favicon.svg +11 -0
- package/templates/default/server/api/posts.ts +20 -0
- package/templates/default/services/PostService.ts +51 -0
- package/templates/default/shared/types.ts +9 -4
- package/templates/default/tests/posts.test.ts +22 -0
- package/vscode/apex-alpine.vsix +0 -0
- package/vscode/version.txt +1 -0
- package/dist/chunk-CHBSGOB3.js +0 -42
- package/dist/server-PTHGOE42.js +0 -307
- package/templates/default/server/api/hello.ts +0 -18
- package/templates/default/services/GreetingService.ts +0 -12
- 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 === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """
|
|
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 & 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-
|
|
15
|
-
import "./chunk-
|
|
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-
|
|
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,
|
|
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.
|
|
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.
|
|
51
|
-
"@apex-stack/vite": "0.1.
|
|
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
|
|
9
|
-
npm run dev:islands
|
|
10
|
-
npm run build
|
|
11
|
-
npm start
|
|
12
|
-
npm
|
|
13
|
-
npm run
|
|
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
|
|
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/
|
|
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
|
-
- **
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **
|
|
62
|
-
`app.css`.
|
|
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
|
|
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>
|