statikapi 0.1.0 → 0.1.2
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 +1 -1
- package/package.json +1 -1
- package/src/build/routeOutPath.js +1 -0
- package/src/commands/build.js +9 -8
- package/src/commands/dev.js +27 -4
- package/src/commands/init.js +4 -9
- package/src/commands/preview.js +7 -1
- package/src/config/loadConfig.js +2 -0
- package/src/index.js +2 -0
- package/src/loader/loadModuleValue.js +2 -0
- package/src/loader/loadPaths.js +8 -0
- package/src/loader/serializeGuard.js +6 -0
- package/src/router/mapRoutes.js +9 -0
- package/src/util/bytes.js +3 -0
- package/src/util/fsx.js +4 -0
- package/src/util/readFlags.js +4 -0
- package/ui/assets/index-C7lyR6dJ.js.map +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -4,5 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
export function routeToOutPath({ outAbs, route }) {
|
|
5
5
|
// '/' -> index.json, '/users' -> users/index.json, '/blog/archive' -> blog/archive/index.json
|
|
6
6
|
const rel = route === '/' ? 'index.json' : route.slice(1) + '/index.json';
|
|
7
|
+
|
|
7
8
|
return path.join(outAbs, rel);
|
|
8
9
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { readFlags } from '../util/readFlags.js'; // from Task 3
|
|
2
|
-
import { loadConfig } from '../config/loadConfig.js'; // from Task 3
|
|
3
|
-
import { ConfigError } from '../config/validate.js'; // from Task 3
|
|
4
|
-
import { mapRoutes } from '../router/mapRoutes.js'; // from Task 4
|
|
5
|
-
import { loadModuleValue } from '../loader/loadModuleValue.js'; // from Task 5
|
|
6
|
-
import { loadPaths } from '../loader/loadPaths.js';
|
|
7
1
|
import path from 'node:path';
|
|
8
2
|
import fs from 'node:fs/promises';
|
|
9
3
|
import crypto from 'node:crypto';
|
|
10
4
|
|
|
5
|
+
import { loadConfig } from '../config/loadConfig.js';
|
|
6
|
+
import { ConfigError } from '../config/validate.js';
|
|
7
|
+
import { loadPaths } from '../loader/loadPaths.js';
|
|
8
|
+
import { loadModuleValue } from '../loader/loadModuleValue.js';
|
|
9
|
+
import { mapRoutes } from '../router/mapRoutes.js';
|
|
10
|
+
import { readFlags } from '../util/readFlags.js';
|
|
11
11
|
import { emptyDir, writeFileEnsured } from '../util/fsx.js';
|
|
12
|
-
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
13
12
|
import { formatBytes } from '../util/bytes.js';
|
|
13
|
+
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
14
14
|
|
|
15
15
|
function toConcrete(routePattern, segTokens, segs) {
|
|
16
16
|
// segTokens: ['users', ':id'] or ['docs','*slug']
|
|
17
17
|
// segs: ['1'] or ['a','b']
|
|
18
18
|
let idx = 0;
|
|
19
|
+
|
|
19
20
|
const parts = routePattern.split('/').map((p) => {
|
|
20
21
|
if (p.startsWith(':')) return segs[idx++] ?? '';
|
|
21
22
|
if (p.startsWith('*')) return segs.slice(idx).join('/');
|
|
22
23
|
return p;
|
|
23
24
|
});
|
|
24
25
|
const concrete = parts.join('/').replace(/\/+/g, '/');
|
|
26
|
+
|
|
25
27
|
return concrete;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -56,7 +58,6 @@ export default async function buildCmd(argv) {
|
|
|
56
58
|
// discover routes
|
|
57
59
|
const routes = await mapRoutes({ srcAbs: config.paths.srcAbs });
|
|
58
60
|
|
|
59
|
-
// MVP: only handle static routes (dynamic/catch-all in next task)
|
|
60
61
|
const staticRoutes = routes.filter((r) => r.type === 'static');
|
|
61
62
|
const dynRoutes = routes.filter((r) => r.type === 'dynamic');
|
|
62
63
|
const catRoutes = routes.filter((r) => r.type === 'catchall');
|
package/src/commands/dev.js
CHANGED
|
@@ -2,13 +2,14 @@ import chokidar from 'chokidar';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { loadConfig } from '../config/loadConfig.js';
|
|
7
|
-
import { mapRoutes, fileToRoute } from '../router/mapRoutes.js';
|
|
8
|
-
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
9
|
-
import { writeFileEnsured } from '../util/fsx.js';
|
|
10
7
|
import { loadModuleValue } from '../loader/loadModuleValue.js';
|
|
11
8
|
import { loadPaths } from '../loader/loadPaths.js';
|
|
9
|
+
import { mapRoutes, fileToRoute } from '../router/mapRoutes.js';
|
|
10
|
+
import { readFlags } from '../util/readFlags.js';
|
|
11
|
+
import { writeFileEnsured } from '../util/fsx.js';
|
|
12
|
+
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
12
13
|
|
|
13
14
|
function clearScreen() {
|
|
14
15
|
process.stdout.write('\x1Bc'); // ANSI "clear screen"
|
|
@@ -16,25 +17,31 @@ function clearScreen() {
|
|
|
16
17
|
|
|
17
18
|
function toConcrete(routePattern, segTokens, segs) {
|
|
18
19
|
let idx = 0;
|
|
20
|
+
|
|
19
21
|
const parts = routePattern.split('/').map((p) => {
|
|
20
22
|
if (p.startsWith(':')) return segs[idx++] ?? '';
|
|
21
23
|
if (p.startsWith('*')) return segs.slice(idx).join('/');
|
|
22
24
|
return p;
|
|
23
25
|
});
|
|
26
|
+
|
|
24
27
|
return parts.join('/').replace(/\/+/g, '/');
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
function toParams(segTokens, concreteRoute) {
|
|
28
31
|
const concreteSegs = concreteRoute.split('/').filter(Boolean);
|
|
29
32
|
const params = {};
|
|
33
|
+
|
|
30
34
|
for (let i = 0; i < segTokens.length; i++) {
|
|
31
35
|
const tok = segTokens[i];
|
|
36
|
+
|
|
32
37
|
if (tok.startsWith(':')) params[tok.slice(1)] = concreteSegs[i] ?? '';
|
|
33
38
|
else if (tok.startsWith('*')) {
|
|
34
39
|
params[tok.slice(1)] = concreteSegs.slice(i);
|
|
40
|
+
|
|
35
41
|
break;
|
|
36
42
|
}
|
|
37
43
|
}
|
|
44
|
+
|
|
38
45
|
return params;
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -42,6 +49,7 @@ export default async function devCmd(argv) {
|
|
|
42
49
|
// In non-TTY (like node --test), behave like the old stub so tests don't hang.
|
|
43
50
|
if (!process.stdout.isTTY) {
|
|
44
51
|
console.log('statikapi dev → starting dev server (stub)');
|
|
52
|
+
|
|
45
53
|
return 0;
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -57,6 +65,7 @@ export default async function devCmd(argv) {
|
|
|
57
65
|
try {
|
|
58
66
|
// Node 18+ has global fetch
|
|
59
67
|
const u = `${notifyOrigin}/_ui/changed?route=${encodeURIComponent(route)}`;
|
|
68
|
+
|
|
60
69
|
await fetch(u, { method: 'POST' }).catch(() => {});
|
|
61
70
|
} catch {
|
|
62
71
|
// Ignore if preview isn't running
|
|
@@ -68,6 +77,7 @@ export default async function devCmd(argv) {
|
|
|
68
77
|
|
|
69
78
|
// Manifest state
|
|
70
79
|
const manifestByRoute = new Map(); // route -> entry
|
|
80
|
+
|
|
71
81
|
const digest = (s) => crypto.createHash('sha1').update(s).digest('hex');
|
|
72
82
|
const relSrc = (abs) => {
|
|
73
83
|
try {
|
|
@@ -103,6 +113,7 @@ export default async function devCmd(argv) {
|
|
|
103
113
|
hash: digest(json),
|
|
104
114
|
revalidate: null,
|
|
105
115
|
};
|
|
116
|
+
|
|
106
117
|
manifestByRoute.set(route, entry);
|
|
107
118
|
}
|
|
108
119
|
function deleteFromManifest(route) {
|
|
@@ -143,6 +154,7 @@ export default async function devCmd(argv) {
|
|
|
143
154
|
await upsertManifest({ route: concrete, srcFile: r.file, outFile, json });
|
|
144
155
|
written++;
|
|
145
156
|
}
|
|
157
|
+
|
|
146
158
|
// Delete stale outputs from this file (not present anymore)
|
|
147
159
|
const prev = lastEmitted.get(r.file) || new Set();
|
|
148
160
|
for (const oldRoute of prev) {
|
|
@@ -174,11 +186,13 @@ export default async function devCmd(argv) {
|
|
|
174
186
|
if (!rel) return false;
|
|
175
187
|
if (rel.startsWith('_')) return false;
|
|
176
188
|
const ext = path.extname(rel);
|
|
189
|
+
|
|
177
190
|
return ext === '.js' || ext === '.mjs' || ext === '.cjs';
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
async function buildOne(fileAbs, kind) {
|
|
181
194
|
if (!shouldHandle(fileAbs)) return;
|
|
195
|
+
|
|
182
196
|
const info = fileToRoute({ srcAbs: config.paths.srcAbs, fileAbs });
|
|
183
197
|
clearScreen();
|
|
184
198
|
console.log(`statikapi dev → ${kind}: ${path.relative(process.cwd(), fileAbs)}`);
|
|
@@ -186,6 +200,7 @@ export default async function devCmd(argv) {
|
|
|
186
200
|
if (!info) {
|
|
187
201
|
// File is ignored or no longer maps; delete prior outputs if any
|
|
188
202
|
const prev = lastEmitted.get(fileAbs);
|
|
203
|
+
|
|
189
204
|
if (prev) {
|
|
190
205
|
for (const route of prev) {
|
|
191
206
|
const p = routeToOutPath({ outAbs: config.paths.outAbs, route });
|
|
@@ -199,12 +214,15 @@ export default async function devCmd(argv) {
|
|
|
199
214
|
}
|
|
200
215
|
lastEmitted.delete(fileAbs);
|
|
201
216
|
}
|
|
217
|
+
|
|
202
218
|
console.log(`[statikapi] (ignored or unmapped)`);
|
|
203
219
|
await writeManifest();
|
|
220
|
+
|
|
204
221
|
return;
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
const r = { file: fileAbs, route: info.route, type: info.type, segments: info.normSegments };
|
|
225
|
+
|
|
208
226
|
try {
|
|
209
227
|
if (r.type === 'static') {
|
|
210
228
|
const files = await emitStatic(r, { fresh: true });
|
|
@@ -223,11 +241,14 @@ export default async function devCmd(argv) {
|
|
|
223
241
|
// Initial full build
|
|
224
242
|
clearScreen();
|
|
225
243
|
console.log('statikapi dev → initial build…');
|
|
244
|
+
|
|
226
245
|
const routes = await mapRoutes({ srcAbs: config.paths.srcAbs });
|
|
246
|
+
|
|
227
247
|
for (const r of routes) {
|
|
228
248
|
if (r.type === 'static') await emitStatic(r);
|
|
229
249
|
else await emitDynamic(r);
|
|
230
250
|
}
|
|
251
|
+
|
|
231
252
|
await writeManifest();
|
|
232
253
|
console.log(`[statikapi] ready. Watching ${path.relative(process.cwd(), config.paths.srcAbs)}/`);
|
|
233
254
|
|
|
@@ -235,6 +256,7 @@ export default async function devCmd(argv) {
|
|
|
235
256
|
ignoreInitial: true,
|
|
236
257
|
ignored: (p) => path.basename(p).startsWith('_'),
|
|
237
258
|
});
|
|
259
|
+
|
|
238
260
|
watcher.on('add', (p) => buildOne(p, 'add'));
|
|
239
261
|
watcher.on('change', (p) => buildOne(p, 'change'));
|
|
240
262
|
watcher.on('unlink', (p) => buildOne(p, 'unlink'));
|
|
@@ -245,5 +267,6 @@ export default async function devCmd(argv) {
|
|
|
245
267
|
process.on('SIGINT', stop);
|
|
246
268
|
process.on('SIGTERM', stop);
|
|
247
269
|
});
|
|
270
|
+
|
|
248
271
|
return 0;
|
|
249
272
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
export default async function initCmd() {
|
|
2
|
-
|
|
1
|
+
export default async function initCmd(argv) {
|
|
2
|
+
// Delegate to create-statikapi programmatically
|
|
3
|
+
const { main } = await import('create-statikapi/src/index.js');
|
|
4
|
+
return (await main(argv), 0);
|
|
3
5
|
}
|
|
4
|
-
|
|
5
|
-
// after publishing, replace the above with the below:
|
|
6
|
-
// export default async function initCmd(argv) {
|
|
7
|
-
// // Delegate to create-statikapi programmatically
|
|
8
|
-
// const { main } = await import('create-statikapi/src/index.js');
|
|
9
|
-
// return (await main(argv), 0);
|
|
10
|
-
// }
|
package/src/commands/preview.js
CHANGED
|
@@ -4,14 +4,16 @@ import fss from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { URL, fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import { loadConfig } from '../config/loadConfig.js';
|
|
9
|
+
import { readFlags } from '../util/readFlags.js';
|
|
9
10
|
import { routeToOutPath } from '../build/routeOutPath.js';
|
|
10
11
|
|
|
11
12
|
export default async function previewCmd(argv) {
|
|
12
13
|
// Keep old tests green: in non-TTY (node --test), behave like stub and exit.
|
|
13
14
|
if (!process.stdout.isTTY) {
|
|
14
15
|
console.log('statikapi preview → previewing built JSON (stub)');
|
|
16
|
+
|
|
15
17
|
return 0;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -53,7 +55,9 @@ export default async function previewCmd(argv) {
|
|
|
53
55
|
'Cache-Control': 'no-store',
|
|
54
56
|
...headers,
|
|
55
57
|
};
|
|
58
|
+
|
|
56
59
|
res.writeHead(code, h);
|
|
60
|
+
|
|
57
61
|
if (body && (typeof body === 'string' || Buffer.isBuffer(body))) res.end(body);
|
|
58
62
|
else res.end();
|
|
59
63
|
};
|
|
@@ -81,10 +85,12 @@ export default async function previewCmd(argv) {
|
|
|
81
85
|
|
|
82
86
|
// --- SSE: subscribers/broadcast ---
|
|
83
87
|
const clients = new Set(); // Set<http.ServerResponse>
|
|
88
|
+
|
|
84
89
|
function sseSend(res, data) {
|
|
85
90
|
// default "message" event with one data line
|
|
86
91
|
res.write(`data: ${data}\n\n`);
|
|
87
92
|
}
|
|
93
|
+
|
|
88
94
|
function broadcast(data) {
|
|
89
95
|
for (const res of clients) {
|
|
90
96
|
try {
|
package/src/config/loadConfig.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
4
5
|
import { DEFAULT_CONFIG } from './defaults.js';
|
|
5
6
|
import { validateAndNormalize, ConfigError } from './validate.js';
|
|
6
7
|
|
|
@@ -42,5 +43,6 @@ function pickFlags(flags) {
|
|
|
42
43
|
const o = {};
|
|
43
44
|
if (flags.srcDir != null) o.srcDir = String(flags.srcDir);
|
|
44
45
|
if (flags.outDir != null) o.outDir = String(flags.outDir);
|
|
46
|
+
|
|
45
47
|
return o;
|
|
46
48
|
}
|
package/src/index.js
CHANGED
|
@@ -13,11 +13,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
13
13
|
|
|
14
14
|
if (!cmd || cmd === '-h' || cmd === '--help') {
|
|
15
15
|
console.log(HELP);
|
|
16
|
+
|
|
16
17
|
return 0;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
if (cmd === '-v' || cmd === '--version') {
|
|
20
21
|
console.log(`statikapi v${version}`);
|
|
22
|
+
|
|
21
23
|
return 0;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
3
4
|
import { LoaderError } from './errors.js';
|
|
4
5
|
import { assertSerializable } from './serializeGuard.js';
|
|
5
6
|
|
|
@@ -15,6 +16,7 @@ export async function loadModuleValue(fileAbs, args = {}) {
|
|
|
15
16
|
|
|
16
17
|
const fileInfo = short(fileAbs);
|
|
17
18
|
let mod;
|
|
19
|
+
|
|
18
20
|
try {
|
|
19
21
|
const u = new URL(pathToFileURL(fileAbs).href);
|
|
20
22
|
if (fresh) u.search = `v=${Date.now()}-${Math.random()}`;
|
package/src/loader/loadPaths.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
3
4
|
import { LoaderError } from './errors.js';
|
|
4
5
|
|
|
5
6
|
export async function loadPaths(fileAbs, { route, type, segments }, { fresh = false } = {}) {
|
|
6
7
|
const fileInfo = short(fileAbs);
|
|
7
8
|
let mod;
|
|
9
|
+
|
|
8
10
|
try {
|
|
9
11
|
const u = new URL(pathToFileURL(fileAbs).href);
|
|
10
12
|
if (fresh) u.search = `v=${Date.now()}-${Math.random()}`;
|
|
@@ -12,9 +14,11 @@ export async function loadPaths(fileAbs, { route, type, segments }, { fresh = fa
|
|
|
12
14
|
} catch (e) {
|
|
13
15
|
throw new LoaderError(fileInfo, `Failed to import for paths(): ${e.message}`);
|
|
14
16
|
}
|
|
17
|
+
|
|
15
18
|
if (typeof mod?.paths !== 'function') return null;
|
|
16
19
|
|
|
17
20
|
let res;
|
|
21
|
+
|
|
18
22
|
try {
|
|
19
23
|
res = await mod.paths();
|
|
20
24
|
} catch (e) {
|
|
@@ -40,12 +44,14 @@ export async function loadPaths(fileAbs, { route, type, segments }, { fresh = fa
|
|
|
40
44
|
`paths() entry for :${paramName(segments)} must not contain '/'`
|
|
41
45
|
);
|
|
42
46
|
}
|
|
47
|
+
|
|
43
48
|
return res.map((v) => [v]); // normalize to array-of-segments
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
if (type === 'catchall') {
|
|
47
52
|
// /docs/*slug → (string | string[])[]
|
|
48
53
|
const out = [];
|
|
54
|
+
|
|
49
55
|
for (const v of res) {
|
|
50
56
|
if (typeof v === 'string') {
|
|
51
57
|
if (!v)
|
|
@@ -80,6 +86,7 @@ export async function loadPaths(fileAbs, { route, type, segments }, { fresh = fa
|
|
|
80
86
|
throw new LoaderError(fileInfo, `paths() for ${route} must be (string | string[])[]`);
|
|
81
87
|
}
|
|
82
88
|
}
|
|
89
|
+
|
|
83
90
|
return out;
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -90,6 +97,7 @@ export async function loadPaths(fileAbs, { route, type, segments }, { fresh = fa
|
|
|
90
97
|
function paramName(segTokens) {
|
|
91
98
|
// segTokens like ['users', ':id'] or ['docs','*slug']
|
|
92
99
|
const tok = segTokens.find((t) => t.startsWith(':') || t.startsWith('*'));
|
|
100
|
+
|
|
93
101
|
return tok ? tok.slice(1) : 'param';
|
|
94
102
|
}
|
|
95
103
|
|
|
@@ -6,8 +6,10 @@ export function assertSerializable(value, atPath = '$', seen = new WeakSet()) {
|
|
|
6
6
|
const t = typeof value;
|
|
7
7
|
|
|
8
8
|
if (value === null || t === 'string' || t === 'boolean') return;
|
|
9
|
+
|
|
9
10
|
if (t === 'number') {
|
|
10
11
|
if (!Number.isFinite(value)) throw new SerializationError('Number must be finite', atPath);
|
|
12
|
+
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
if (t === 'bigint') throw new SerializationError('BigInt is not JSON-serializable', atPath);
|
|
@@ -17,12 +19,14 @@ export function assertSerializable(value, atPath = '$', seen = new WeakSet()) {
|
|
|
17
19
|
// Objects / Arrays
|
|
18
20
|
if (typeof value === 'object') {
|
|
19
21
|
if (seen.has(value)) throw new SerializationError('Circular structure detected', atPath);
|
|
22
|
+
|
|
20
23
|
seen.add(value);
|
|
21
24
|
|
|
22
25
|
if (Array.isArray(value)) {
|
|
23
26
|
for (let i = 0; i < value.length; i++) {
|
|
24
27
|
assertSerializable(value[i], `${atPath}[${i}]`, seen);
|
|
25
28
|
}
|
|
29
|
+
|
|
26
30
|
return;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -30,6 +34,7 @@ export function assertSerializable(value, atPath = '$', seen = new WeakSet()) {
|
|
|
30
34
|
const tag = Object.prototype.toString.call(value);
|
|
31
35
|
const proto = Object.getPrototypeOf(value);
|
|
32
36
|
const isPlain = tag === PLAIN_TAG && (proto === Object.prototype || proto === null);
|
|
37
|
+
|
|
33
38
|
if (!isPlain) {
|
|
34
39
|
const name = (value && value.constructor && value.constructor.name) || tag;
|
|
35
40
|
throw new SerializationError(`Only plain objects/arrays allowed (got ${name})`, atPath);
|
|
@@ -43,6 +48,7 @@ export function assertSerializable(value, atPath = '$', seen = new WeakSet()) {
|
|
|
43
48
|
for (const k of Object.keys(value)) {
|
|
44
49
|
assertSerializable(value[k], `${atPath}.${k}`, seen);
|
|
45
50
|
}
|
|
51
|
+
|
|
46
52
|
return;
|
|
47
53
|
}
|
|
48
54
|
|
package/src/router/mapRoutes.js
CHANGED
|
@@ -29,11 +29,14 @@ export async function mapRoutes({ srcAbs }) {
|
|
|
29
29
|
async function walk(dir) {
|
|
30
30
|
const out = [];
|
|
31
31
|
const stack = [dir];
|
|
32
|
+
|
|
32
33
|
while (stack.length) {
|
|
33
34
|
const cur = stack.pop();
|
|
34
35
|
const items = await fs.readdir(cur, { withFileTypes: true });
|
|
36
|
+
|
|
35
37
|
for (const it of items) {
|
|
36
38
|
const a = path.join(cur, it.name);
|
|
39
|
+
|
|
37
40
|
if (it.isDirectory()) {
|
|
38
41
|
stack.push(a);
|
|
39
42
|
} else if (it.isFile()) {
|
|
@@ -41,6 +44,7 @@ async function walk(dir) {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
|
|
44
48
|
return out;
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -59,6 +63,7 @@ export function fileToRoute({ srcAbs, fileAbs }) {
|
|
|
59
63
|
if (segments.some((s) => s.startsWith('_'))) return null;
|
|
60
64
|
|
|
61
65
|
const { route, type, normSegments } = toRoute(segments);
|
|
66
|
+
|
|
62
67
|
return { route, type, normSegments };
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -107,8 +112,12 @@ function isCatchAll(seg) {
|
|
|
107
112
|
// 3) fewer segments as a final tiebreaker
|
|
108
113
|
function compareRoutes(a, b) {
|
|
109
114
|
const rank = { static: 0, dynamic: 1, catchall: 2 };
|
|
115
|
+
|
|
110
116
|
if (rank[a.type] !== rank[b.type]) return rank[a.type] - rank[b.type];
|
|
117
|
+
|
|
111
118
|
const byRoute = a.route.localeCompare(b.route);
|
|
119
|
+
|
|
112
120
|
if (byRoute !== 0) return byRoute;
|
|
121
|
+
|
|
113
122
|
return a.segments.length - b.segments.length;
|
|
114
123
|
}
|
package/src/util/bytes.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export function formatBytes(n) {
|
|
2
2
|
if (n < 1024) return `${n} B`;
|
|
3
|
+
|
|
3
4
|
const units = ['KB', 'MB', 'GB'];
|
|
4
5
|
let u = -1;
|
|
6
|
+
|
|
5
7
|
do {
|
|
6
8
|
n /= 1024;
|
|
7
9
|
u++;
|
|
8
10
|
} while (n >= 1024 && u < units.length - 1);
|
|
11
|
+
|
|
9
12
|
return `${n.toFixed(n >= 10 ? 0 : 1)} ${units[u]}`;
|
|
10
13
|
}
|
package/src/util/fsx.js
CHANGED
|
@@ -4,14 +4,18 @@ import path from 'node:path';
|
|
|
4
4
|
export async function ensureDir(p) {
|
|
5
5
|
await fs.mkdir(p, { recursive: true });
|
|
6
6
|
}
|
|
7
|
+
|
|
7
8
|
export async function writeFileEnsured(fileAbs, data) {
|
|
8
9
|
await ensureDir(path.dirname(fileAbs));
|
|
9
10
|
await fs.writeFile(fileAbs, data);
|
|
10
11
|
}
|
|
12
|
+
|
|
11
13
|
export async function emptyDir(dirAbs) {
|
|
12
14
|
// non-destructive: create if missing, else clean
|
|
13
15
|
await ensureDir(dirAbs);
|
|
16
|
+
|
|
14
17
|
const entries = await fs.readdir(dirAbs, { withFileTypes: true });
|
|
18
|
+
|
|
15
19
|
await Promise.all(
|
|
16
20
|
entries.map(async (e) => {
|
|
17
21
|
const p = path.join(dirAbs, e.name);
|
package/src/util/readFlags.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export function readFlags(argv = []) {
|
|
2
2
|
const out = {};
|
|
3
|
+
|
|
3
4
|
for (let i = 0; i < argv.length; i++) {
|
|
4
5
|
const t = argv[i];
|
|
5
6
|
if (!t.startsWith('-')) continue;
|
|
@@ -17,12 +18,15 @@ export function readFlags(argv = []) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
return out;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function coerce(v) {
|
|
24
26
|
if (v === 'true') return true;
|
|
25
27
|
if (v === 'false') return false;
|
|
28
|
+
|
|
26
29
|
const n = Number(v);
|
|
30
|
+
|
|
27
31
|
return Number.isFinite(n) ? n : v;
|
|
28
32
|
}
|