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.
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ export function formatBytes(n) {
2
+ if (n < 1024) return `${n} B`;
3
+ const units = ['KB', 'MB', 'GB'];
4
+ let u = -1;
5
+ do {
6
+ n /= 1024;
7
+ u++;
8
+ } while (n >= 1024 && u < units.length - 1);
9
+ return `${n.toFixed(n >= 10 ? 0 : 1)} ${units[u]}`;
10
+ }
@@ -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
+ }