statikapi 0.1.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/README.md +160 -0
- package/bin/statikapi.js +8 -0
- package/package.json +27 -0
- package/src/build/routeOutPath.js +8 -0
- package/src/commands/build.js +191 -0
- package/src/commands/dev.js +249 -0
- package/src/commands/init.js +10 -0
- package/src/commands/preview.js +320 -0
- package/src/config/defaults.js +4 -0
- package/src/config/loadConfig.js +46 -0
- package/src/config/validate.js +39 -0
- package/src/help.js +21 -0
- package/src/index.js +38 -0
- package/src/loader/errors.js +15 -0
- package/src/loader/loadModuleValue.js +60 -0
- package/src/loader/loadPaths.js +102 -0
- package/src/loader/serializeGuard.js +51 -0
- package/src/router/mapRoutes.js +114 -0
- package/src/util/bytes.js +10 -0
- package/src/util/fsx.js +22 -0
- package/src/util/readFlags.js +28 -0
- package/ui/assets/index-C7lyR6dJ.js +57 -0
- package/ui/assets/index-C7lyR6dJ.js.map +1 -0
- package/ui/assets/index-CnyB4RRg.css +1 -0
- package/ui/index.html +13 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SerializationError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
const PLAIN_TAG = '[object Object]';
|
|
4
|
+
|
|
5
|
+
export function assertSerializable(value, atPath = '$', seen = new WeakSet()) {
|
|
6
|
+
const t = typeof value;
|
|
7
|
+
|
|
8
|
+
if (value === null || t === 'string' || t === 'boolean') return;
|
|
9
|
+
if (t === 'number') {
|
|
10
|
+
if (!Number.isFinite(value)) throw new SerializationError('Number must be finite', atPath);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (t === 'bigint') throw new SerializationError('BigInt is not JSON-serializable', atPath);
|
|
14
|
+
if (t === 'symbol') throw new SerializationError('Symbol is not JSON-serializable', atPath);
|
|
15
|
+
if (t === 'function') throw new SerializationError('Function is not JSON-serializable', atPath);
|
|
16
|
+
|
|
17
|
+
// Objects / Arrays
|
|
18
|
+
if (typeof value === 'object') {
|
|
19
|
+
if (seen.has(value)) throw new SerializationError('Circular structure detected', atPath);
|
|
20
|
+
seen.add(value);
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (let i = 0; i < value.length; i++) {
|
|
24
|
+
assertSerializable(value[i], `${atPath}[${i}]`, seen);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Plain objects only
|
|
30
|
+
const tag = Object.prototype.toString.call(value);
|
|
31
|
+
const proto = Object.getPrototypeOf(value);
|
|
32
|
+
const isPlain = tag === PLAIN_TAG && (proto === Object.prototype || proto === null);
|
|
33
|
+
if (!isPlain) {
|
|
34
|
+
const name = (value && value.constructor && value.constructor.name) || tag;
|
|
35
|
+
throw new SerializationError(`Only plain objects/arrays allowed (got ${name})`, atPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// No symbol keys
|
|
39
|
+
if (Object.getOwnPropertySymbols(value).length) {
|
|
40
|
+
throw new SerializationError('Symbol keys are not JSON-serializable', atPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const k of Object.keys(value)) {
|
|
44
|
+
assertSerializable(value[k], `${atPath}.${k}`, seen);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Anything else falls through as unsupported
|
|
50
|
+
throw new SerializationError(`Unsupported type: ${t}`, atPath);
|
|
51
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const VALID_EXT = new Set(['.js', '.mjs', '.cjs']);
|
|
5
|
+
|
|
6
|
+
export async function mapRoutes({ srcAbs }) {
|
|
7
|
+
const entries = await walk(srcAbs);
|
|
8
|
+
const routes = [];
|
|
9
|
+
|
|
10
|
+
for (const fileAbs of entries) {
|
|
11
|
+
const info = fileToRoute({ srcAbs, fileAbs });
|
|
12
|
+
if (!info) continue;
|
|
13
|
+
const { route, type, normSegments } = info;
|
|
14
|
+
|
|
15
|
+
routes.push({
|
|
16
|
+
file: fileAbs,
|
|
17
|
+
route,
|
|
18
|
+
type, // 'static' | 'dynamic' | 'catchall'
|
|
19
|
+
segments: normSegments, // normalized tokens for sorting (static or :param or *catch)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Deterministic sort
|
|
24
|
+
routes.sort(compareRoutes);
|
|
25
|
+
|
|
26
|
+
return routes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function walk(dir) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const stack = [dir];
|
|
32
|
+
while (stack.length) {
|
|
33
|
+
const cur = stack.pop();
|
|
34
|
+
const items = await fs.readdir(cur, { withFileTypes: true });
|
|
35
|
+
for (const it of items) {
|
|
36
|
+
const a = path.join(cur, it.name);
|
|
37
|
+
if (it.isDirectory()) {
|
|
38
|
+
stack.push(a);
|
|
39
|
+
} else if (it.isFile()) {
|
|
40
|
+
out.push(a);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Map a single file to route metadata, or null if ignored. */
|
|
48
|
+
export function fileToRoute({ srcAbs, fileAbs }) {
|
|
49
|
+
const rel = path.posix.normalize(fileAbs.replaceAll(path.sep, '/').slice(srcAbs.length + 1));
|
|
50
|
+
if (!rel || rel.startsWith('_')) return null; // ignore underscore roots
|
|
51
|
+
const ext = path.extname(rel);
|
|
52
|
+
if (!VALID_EXT.has(ext)) return null;
|
|
53
|
+
|
|
54
|
+
const relNoExt = rel.slice(0, -ext.length);
|
|
55
|
+
const segments = relNoExt
|
|
56
|
+
.split('/')
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
if (segments.some((s) => s.startsWith('_'))) return null;
|
|
60
|
+
|
|
61
|
+
const { route, type, normSegments } = toRoute(segments);
|
|
62
|
+
return { route, type, normSegments };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toRoute(segments) {
|
|
66
|
+
// Handle index collapsing: foo/index -> /foo
|
|
67
|
+
const last = segments[segments.length - 1];
|
|
68
|
+
const isIndex = last === 'index';
|
|
69
|
+
const segs = isIndex ? segments.slice(0, -1) : segments;
|
|
70
|
+
|
|
71
|
+
// Normalize tokens:
|
|
72
|
+
// - static: 'users' stays 'users'
|
|
73
|
+
// - dynamic: '[id]' => ':id'
|
|
74
|
+
// - catch-all: '[...all]' => '*all'
|
|
75
|
+
let type = 'static';
|
|
76
|
+
const normSegments = segs.map((s) => {
|
|
77
|
+
if (isCatchAll(s)) {
|
|
78
|
+
type = 'catchall';
|
|
79
|
+
return '*' + s.slice(4, -1); // [...all] -> *all
|
|
80
|
+
}
|
|
81
|
+
if (isDynamic(s)) {
|
|
82
|
+
if (type !== 'catchall') type = 'dynamic';
|
|
83
|
+
return ':' + s.slice(1, -1); // [id] -> :id
|
|
84
|
+
}
|
|
85
|
+
return s;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const route = '/' + normSegments.filter(Boolean).join('/');
|
|
89
|
+
|
|
90
|
+
// special case: empty means root (/)
|
|
91
|
+
const finalRoute = route === '/' ? '/' : route;
|
|
92
|
+
|
|
93
|
+
return { route: finalRoute, type, normSegments };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isDynamic(seg) {
|
|
97
|
+
return seg.startsWith('[') && seg.endsWith(']') && !seg.startsWith('[...');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isCatchAll(seg) {
|
|
101
|
+
return seg.startsWith('[...') && seg.endsWith(']');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Deterministic sort:
|
|
105
|
+
// 1) static < dynamic < catchall
|
|
106
|
+
// 2) lexicographic by route (so /blog/archive comes before /users)
|
|
107
|
+
// 3) fewer segments as a final tiebreaker
|
|
108
|
+
function compareRoutes(a, b) {
|
|
109
|
+
const rank = { static: 0, dynamic: 1, catchall: 2 };
|
|
110
|
+
if (rank[a.type] !== rank[b.type]) return rank[a.type] - rank[b.type];
|
|
111
|
+
const byRoute = a.route.localeCompare(b.route);
|
|
112
|
+
if (byRoute !== 0) return byRoute;
|
|
113
|
+
return a.segments.length - b.segments.length;
|
|
114
|
+
}
|
package/src/util/fsx.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function ensureDir(p) {
|
|
5
|
+
await fs.mkdir(p, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
export async function writeFileEnsured(fileAbs, data) {
|
|
8
|
+
await ensureDir(path.dirname(fileAbs));
|
|
9
|
+
await fs.writeFile(fileAbs, data);
|
|
10
|
+
}
|
|
11
|
+
export async function emptyDir(dirAbs) {
|
|
12
|
+
// non-destructive: create if missing, else clean
|
|
13
|
+
await ensureDir(dirAbs);
|
|
14
|
+
const entries = await fs.readdir(dirAbs, { withFileTypes: true });
|
|
15
|
+
await Promise.all(
|
|
16
|
+
entries.map(async (e) => {
|
|
17
|
+
const p = path.join(dirAbs, e.name);
|
|
18
|
+
if (e.isDirectory()) await fs.rm(p, { recursive: true, force: true });
|
|
19
|
+
else await fs.rm(p, { force: true });
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function readFlags(argv = []) {
|
|
2
|
+
const out = {};
|
|
3
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4
|
+
const t = argv[i];
|
|
5
|
+
if (!t.startsWith('-')) continue;
|
|
6
|
+
|
|
7
|
+
if (t.startsWith('--')) {
|
|
8
|
+
const [k, v] = t.slice(2).split('=', 2);
|
|
9
|
+
// allow any flag, we’ll read only the ones we need in commands
|
|
10
|
+
if (v !== undefined) out[k] = coerce(v);
|
|
11
|
+
else {
|
|
12
|
+
const next = argv[i + 1];
|
|
13
|
+
if (next && !next.startsWith('-')) {
|
|
14
|
+
out[k] = coerce(next);
|
|
15
|
+
i++;
|
|
16
|
+
} else out[k] = true; // <- bare flag now boolean true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function coerce(v) {
|
|
24
|
+
if (v === 'true') return true;
|
|
25
|
+
if (v === 'false') return false;
|
|
26
|
+
const n = Number(v);
|
|
27
|
+
return Number.isFinite(n) ? n : v;
|
|
28
|
+
}
|