statikapi 0.1.4 → 0.3.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/package.json +9 -5
- package/src/commands/build.js +0 -1
- package/src/commands/dev.js +202 -18
- package/src/help.js +0 -2
- package/src/index.js +0 -6
- package/src/loader/importModule.js +38 -0
- package/src/loader/loadModuleValue.js +2 -4
- package/src/loader/loadPaths.js +2 -4
- package/src/router/mapRoutes.js +1 -1
- package/ui/EMBEDDED_UI_README.txt +2 -0
- package/ui/assets/index-BU1U7AZy.css +1 -0
- package/ui/assets/index-BkqZSp06.js +142 -0
- package/ui/assets/index-BkqZSp06.js.map +1 -0
- package/ui/index.html +15 -2
- package/src/commands/init.js +0 -5
- package/src/commands/preview.js +0 -326
- package/ui/assets/index-C7lyR6dJ.js +0 -57
- package/ui/assets/index-C7lyR6dJ.js.map +0 -1
- package/ui/assets/index-CnyB4RRg.css +0 -1
package/ui/index.html
CHANGED
|
@@ -4,8 +4,21 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>StatikAPI UI</title>
|
|
7
|
-
<script
|
|
8
|
-
|
|
7
|
+
<script>
|
|
8
|
+
(function () {
|
|
9
|
+
try {
|
|
10
|
+
const KEY = 'statik-theme';
|
|
11
|
+
const saved = localStorage.getItem(KEY) || 'system';
|
|
12
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
13
|
+
const wantDark = saved === 'dark' || (saved === 'system' && prefersDark);
|
|
14
|
+
const html = document.documentElement;
|
|
15
|
+
html.classList.toggle('dark', wantDark);
|
|
16
|
+
html.setAttribute('data-theme', saved);
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
})();
|
|
19
|
+
</script>
|
|
20
|
+
<script type="module" crossorigin src="/_ui/assets/index-BkqZSp06.js"></script>
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/_ui/assets/index-BU1U7AZy.css">
|
|
9
22
|
</head>
|
|
10
23
|
<body>
|
|
11
24
|
<div id="root"></div>
|
package/src/commands/init.js
DELETED
package/src/commands/preview.js
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import fss from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import { URL, fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
import { loadConfig } from '../config/loadConfig.js';
|
|
9
|
-
import { readFlags } from '../util/readFlags.js';
|
|
10
|
-
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
11
|
-
|
|
12
|
-
export default async function previewCmd(argv) {
|
|
13
|
-
// Keep old tests green: in non-TTY (node --test), behave like stub and exit.
|
|
14
|
-
if (!process.stdout.isTTY) {
|
|
15
|
-
console.log('statikapi preview → previewing built JSON (stub)');
|
|
16
|
-
|
|
17
|
-
return 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const flags = readFlags(argv || []);
|
|
21
|
-
const host = String(flags.host ?? '127.0.0.1');
|
|
22
|
-
const port = Number.isFinite(flags.port) ? Number(flags.port) : 8788;
|
|
23
|
-
const autoOpen = flags.open === true;
|
|
24
|
-
|
|
25
|
-
const { config } = await loadConfig({ flags });
|
|
26
|
-
|
|
27
|
-
// --- React UI defaults ---
|
|
28
|
-
// Prefer --uiDir; else use embedded UI inside this package; else proxy to Vite dev.
|
|
29
|
-
const here = path.dirname(fileURLToPath(import.meta.url)); // .../packages/cli/src/commands
|
|
30
|
-
const embeddedUi = path.resolve(here, '../../ui'); // .../packages/cli/ui
|
|
31
|
-
const uiDir = flags.uiDir ? path.resolve(String(flags.uiDir)) : embeddedUi;
|
|
32
|
-
const hasUi = uiDir && fss.existsSync(uiDir);
|
|
33
|
-
|
|
34
|
-
const uiDevHost = String(flags.uiDevHost ?? '127.0.0.1');
|
|
35
|
-
const uiDevPort = Number.isFinite(flags.uiDevPort) ? Number(flags.uiDevPort) : 5173;
|
|
36
|
-
|
|
37
|
-
const MIME = {
|
|
38
|
-
'.html': 'text/html; charset=utf-8',
|
|
39
|
-
'.js': 'application/javascript; charset=utf-8',
|
|
40
|
-
'.css': 'text/css; charset=utf-8',
|
|
41
|
-
'.json': 'application/json; charset=utf-8',
|
|
42
|
-
'.svg': 'image/svg+xml',
|
|
43
|
-
'.png': 'image/png',
|
|
44
|
-
'.jpg': 'image/jpeg',
|
|
45
|
-
'.jpeg': 'image/jpeg',
|
|
46
|
-
'.ico': 'image/x-icon',
|
|
47
|
-
'.map': 'application/json',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const outDir = config.paths.outAbs;
|
|
51
|
-
const manifestPath = path.join(outDir, '.statikapi', 'manifest.json');
|
|
52
|
-
|
|
53
|
-
const send = (res, code, body, headers = {}) => {
|
|
54
|
-
const h = {
|
|
55
|
-
'Cache-Control': 'no-store',
|
|
56
|
-
...headers,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
res.writeHead(code, h);
|
|
60
|
-
|
|
61
|
-
if (body && (typeof body === 'string' || Buffer.isBuffer(body))) res.end(body);
|
|
62
|
-
else res.end();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const notFound = (res, msg = 'Not found') =>
|
|
66
|
-
send(res, 404, JSON.stringify({ error: msg }) + '\n', {
|
|
67
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const badReq = (res, msg) =>
|
|
71
|
-
send(res, 400, JSON.stringify({ error: msg }) + '\n', {
|
|
72
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const etag = (buf) => `"sha1-${crypto.createHash('sha1').update(buf).digest('hex')}"`;
|
|
76
|
-
|
|
77
|
-
async function readManifest() {
|
|
78
|
-
try {
|
|
79
|
-
const raw = await fs.readFile(manifestPath);
|
|
80
|
-
return raw;
|
|
81
|
-
} catch {
|
|
82
|
-
return Buffer.from('[]', 'utf8');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- SSE: subscribers/broadcast ---
|
|
87
|
-
const clients = new Set(); // Set<http.ServerResponse>
|
|
88
|
-
|
|
89
|
-
function sseSend(res, data) {
|
|
90
|
-
// default "message" event with one data line
|
|
91
|
-
res.write(`data: ${data}\n\n`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function broadcast(data) {
|
|
95
|
-
for (const res of clients) {
|
|
96
|
-
try {
|
|
97
|
-
sseSend(res, data);
|
|
98
|
-
} catch {
|
|
99
|
-
/* ignore */
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Simple proxy to Vite dev server (only used if no built UI is found)
|
|
105
|
-
async function proxyUi(req, res, uiPathname) {
|
|
106
|
-
const httpMod = uiDevHost.startsWith('https')
|
|
107
|
-
? await import('node:https')
|
|
108
|
-
: await import('node:http');
|
|
109
|
-
const client = uiDevHost.startsWith('https') ? httpMod.default : httpMod.default;
|
|
110
|
-
const targetPath =
|
|
111
|
-
uiPathname + (req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '');
|
|
112
|
-
const opts = {
|
|
113
|
-
hostname: uiDevHost,
|
|
114
|
-
port: uiDevPort,
|
|
115
|
-
method: req.method || 'GET',
|
|
116
|
-
path: targetPath,
|
|
117
|
-
headers: req.headers,
|
|
118
|
-
};
|
|
119
|
-
const p = client.request(opts, (up) => {
|
|
120
|
-
const headers = { ...up.headers };
|
|
121
|
-
// Always no-store for UI assets
|
|
122
|
-
headers['cache-control'] = 'no-store';
|
|
123
|
-
res.writeHead(up.statusCode || 502, headers);
|
|
124
|
-
up.pipe(res);
|
|
125
|
-
});
|
|
126
|
-
p.on('error', () => {
|
|
127
|
-
const msg = `StatikAPI UI dev server not found at http://${uiDevHost}:${uiDevPort}. Start it with: pnpm -w --filter packages/ui dev`;
|
|
128
|
-
res.writeHead(502, {
|
|
129
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
130
|
-
'Cache-Control': 'no-store',
|
|
131
|
-
});
|
|
132
|
-
res.end(msg);
|
|
133
|
-
});
|
|
134
|
-
if (req.readable) req.pipe(p);
|
|
135
|
-
else p.end();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function tryServeFrom(rootDir, reqPath, { spaFallback = null } = {}) {
|
|
139
|
-
const target = path.normalize(path.join(rootDir, reqPath.replace(/^\/+/, '')));
|
|
140
|
-
if (!target.startsWith(rootDir)) return null; // path traversal guard
|
|
141
|
-
try {
|
|
142
|
-
const st = await fs.stat(target);
|
|
143
|
-
if (st.isDirectory()) {
|
|
144
|
-
const idx = path.join(target, 'index.html');
|
|
145
|
-
const buf = await fs.readFile(idx);
|
|
146
|
-
return { buf, ctype: MIME['.html'] };
|
|
147
|
-
}
|
|
148
|
-
const buf = await fs.readFile(target);
|
|
149
|
-
const ext = path.extname(target).toLowerCase();
|
|
150
|
-
return { buf, ctype: MIME[ext] || 'application/octet-stream' };
|
|
151
|
-
} catch {
|
|
152
|
-
if (spaFallback) {
|
|
153
|
-
try {
|
|
154
|
-
const fallback = path.join(rootDir, spaFallback);
|
|
155
|
-
const buf = await fs.readFile(fallback);
|
|
156
|
-
return { buf, ctype: MIME['.html'] };
|
|
157
|
-
} catch {
|
|
158
|
-
/* ignore */
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const server = http.createServer(async (req, res) => {
|
|
166
|
-
const base = `http://${host}:${port}`;
|
|
167
|
-
let url;
|
|
168
|
-
try {
|
|
169
|
-
url = new URL(req.url || '/', base);
|
|
170
|
-
} catch {
|
|
171
|
-
return notFound(res, 'Invalid URL');
|
|
172
|
-
}
|
|
173
|
-
const pathname = url.pathname;
|
|
174
|
-
|
|
175
|
-
// --- SSE subscription ---
|
|
176
|
-
if (pathname === '/_ui/events') {
|
|
177
|
-
res.writeHead(200, {
|
|
178
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
179
|
-
'Cache-Control': 'no-store',
|
|
180
|
-
Connection: 'keep-alive',
|
|
181
|
-
});
|
|
182
|
-
res.write(': connected\n\n'); // comment line
|
|
183
|
-
clients.add(res);
|
|
184
|
-
|
|
185
|
-
// keepalive pings
|
|
186
|
-
const ping = setInterval(() => {
|
|
187
|
-
try {
|
|
188
|
-
res.write(': ping\n\n');
|
|
189
|
-
} catch {
|
|
190
|
-
// ignore write errors
|
|
191
|
-
}
|
|
192
|
-
}, 30000);
|
|
193
|
-
|
|
194
|
-
req.on('close', () => {
|
|
195
|
-
clearInterval(ping);
|
|
196
|
-
clients.delete(res);
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Internal notify hook (used by dev watcher)
|
|
202
|
-
if (pathname === '/_ui/changed') {
|
|
203
|
-
const route = url.searchParams.get('route') || '';
|
|
204
|
-
broadcast(`changed:${route}`);
|
|
205
|
-
return send(res, 204, '');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// UI root always React: serve built dist if present; otherwise proxy to Vite dev
|
|
209
|
-
if (pathname === '/_ui' || pathname === '/ui' || pathname === '/ui/') {
|
|
210
|
-
if (hasUi) {
|
|
211
|
-
const served = await tryServeFrom(uiDir, 'index.html', { spaFallback: null });
|
|
212
|
-
if (served) {
|
|
213
|
-
return send(res, 200, served.buf, {
|
|
214
|
-
'Content-Type': served.ctype,
|
|
215
|
-
'Cache-Control': 'no-store',
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return proxyUi(req, res, '/_ui/');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Helper: manifest passthrough
|
|
223
|
-
if (pathname === '/_ui/index' || pathname === '/ui/index') {
|
|
224
|
-
const raw = await readManifest();
|
|
225
|
-
const tag = etag(raw);
|
|
226
|
-
if (req.headers['if-none-match'] === tag) {
|
|
227
|
-
res.writeHead(304, { ETag: tag, 'Cache-Control': 'no-store' });
|
|
228
|
-
return res.end();
|
|
229
|
-
}
|
|
230
|
-
return send(res, 200, raw, {
|
|
231
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
232
|
-
ETag: tag,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Helper: stream a built JSON by route
|
|
237
|
-
if (pathname === '/_ui/file' || pathname === '/ui/file') {
|
|
238
|
-
const route = url.searchParams.get('route');
|
|
239
|
-
if (!route || !route.startsWith('/')) {
|
|
240
|
-
return badReq(res, 'query parameter "route" is required and must start with "/"');
|
|
241
|
-
}
|
|
242
|
-
const fileAbs = routeToOutPath({ outAbs: outDir, route });
|
|
243
|
-
if (!fss.existsSync(fileAbs)) return notFound(res, `No file for route: ${route}`);
|
|
244
|
-
res.writeHead(200, {
|
|
245
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
246
|
-
'X-StatikAPI-Route': route,
|
|
247
|
-
'X-StatikAPI-File': path.relative(process.cwd(), fileAbs).replaceAll(path.sep, '/'),
|
|
248
|
-
'Cache-Control': 'no-store',
|
|
249
|
-
});
|
|
250
|
-
fss.createReadStream(fileAbs).pipe(res);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Serve UI assets: built if present, else proxy to Vite dev
|
|
255
|
-
if (pathname.startsWith('/_ui') || pathname.startsWith('/ui')) {
|
|
256
|
-
if (hasUi) {
|
|
257
|
-
const rel = pathname.replace(/^\/_?ui\/?/, '');
|
|
258
|
-
const reqPath = rel === '' ? 'index.html' : rel;
|
|
259
|
-
const served = await tryServeFrom(uiDir, reqPath, { spaFallback: 'index.html' });
|
|
260
|
-
if (served) {
|
|
261
|
-
return send(res, 200, served.buf, {
|
|
262
|
-
'Content-Type': served.ctype,
|
|
263
|
-
'Cache-Control': 'no-store',
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return notFound(res);
|
|
267
|
-
}
|
|
268
|
-
return proxyUi(req, res, pathname);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Static serve from api-out (best-effort)
|
|
272
|
-
const safe = path.normalize(path.join(outDir, pathname));
|
|
273
|
-
if (!safe.startsWith(outDir)) return notFound(res);
|
|
274
|
-
try {
|
|
275
|
-
const stat = await fs.stat(safe);
|
|
276
|
-
if (stat.isDirectory()) {
|
|
277
|
-
const idx = path.join(safe, 'index.json');
|
|
278
|
-
const s2 = await fs.readFile(idx);
|
|
279
|
-
return send(res, 200, s2, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
280
|
-
} else {
|
|
281
|
-
const buf = await fs.readFile(safe);
|
|
282
|
-
const ctype = safe.endsWith('.json')
|
|
283
|
-
? 'application/json; charset=utf-8'
|
|
284
|
-
: 'text/plain; charset=utf-8';
|
|
285
|
-
return send(res, 200, buf, { 'Content-Type': ctype });
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
return notFound(res);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await new Promise((resolve, reject) => {
|
|
293
|
-
server.once('error', reject);
|
|
294
|
-
server.listen(port, host, resolve);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const url = `http://${host}:${port}/_ui`;
|
|
298
|
-
console.log(`statikapi preview → serving ${path.relative(process.cwd(), outDir) || outDir}`);
|
|
299
|
-
console.log(`open ${url}`);
|
|
300
|
-
|
|
301
|
-
if (autoOpen) {
|
|
302
|
-
openBrowser(url).catch(() => {});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Graceful shutdown
|
|
306
|
-
await new Promise((resolve) => {
|
|
307
|
-
const stop = () => server.close(() => resolve());
|
|
308
|
-
process.on('SIGINT', stop);
|
|
309
|
-
process.on('SIGTERM', stop);
|
|
310
|
-
});
|
|
311
|
-
return 0;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function openBrowser(url) {
|
|
315
|
-
const { exec } = await import('node:child_process');
|
|
316
|
-
const plat = process.platform;
|
|
317
|
-
return new Promise((resolve) => {
|
|
318
|
-
const cmd =
|
|
319
|
-
plat === 'darwin'
|
|
320
|
-
? `open "${url}"`
|
|
321
|
-
: plat === 'win32'
|
|
322
|
-
? `start "" "${url}"`
|
|
323
|
-
: `xdg-open "${url}"`;
|
|
324
|
-
exec(cmd, () => resolve());
|
|
325
|
-
});
|
|
326
|
-
}
|