statikapi 0.1.1 → 0.1.3

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 CHANGED
@@ -157,4 +157,4 @@ pnpm -C example/dynamic preview
157
157
 
158
158
  License
159
159
 
160
- ISC – see LICENSE
160
+ MIT – see LICENSE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statikapi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zonayedpca/statikapi",
@@ -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
  }
@@ -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');
@@ -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
- import { readFlags } from '../util/readFlags.js';
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
  }
@@ -1,10 +1,5 @@
1
- export default async function initCmd() {
2
- console.log('statikapi init use `npx create-statikapi <name>` to scaffold a new project.');
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
- // }
@@ -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
- import { readFlags } from '../util/readFlags.js';
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 {
@@ -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()}`;
@@ -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
 
@@ -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);
@@ -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
  }