@@ -202,6 +202,47 @@ const WORKER_RUNTIME_JS = `
202202 return route.replace(/^\\//,'').split('/');
203203 }
204204
205+ function matchPattern(patternRoute, concreteRoute) {
206+ const pSegs = splitRoute(patternRoute);
207+ const cSegs = splitRoute(concreteRoute);
208+
209+ const params = {};
210+ let i = 0, j = 0;
211+
212+ while (i < pSegs.length && j < cSegs.length) {
213+ const p = pSegs[i];
214+ const c = cSegs[j];
215+
216+ if (p.startsWith(':')) {
217+ params[p.slice(1)] = decodeURIComponent(c);
218+ i++; j++;
219+ } else if (p.startsWith('*')) {
220+ const name = p.slice(1);
221+ const rest = cSegs.slice(j).map((s) => decodeURIComponent(s));
222+ params[name] = rest;
223+ i = pSegs.length;
224+ j = cSegs.length;
225+ break;
226+ } else {
227+ if (p !== c) return null; // no match
228+ i++; j++;
229+ }
230+ }
231+
232+ if (i !== pSegs.length || j !== cSegs.length) return null;
233+ return params; // matched!
234+ }
235+
236+ function findRouteEntry(registry, concreteRoute) {
237+ for (const r of registry) {
238+ const params = matchPattern(r.route, concreteRoute);
239+ if (params) {
240+ return { routeEntry: r, params };
241+ }
242+ }
243+ return null;
244+ }
245+
205246 function concreteFromPattern(patternSegs, entry) {
206247 // entry can be string (for single dynamic) or array for catchall
207248 const params = {};
@@ -287,6 +328,72 @@ const WORKER_RUNTIME_JS = `
287328 await env.STATIK_MANIFEST.put(k, JSON.stringify(list));
288329 }
289330
331+ async function handleBuildRoute(req, env) {
332+ const auth = req.headers.get('authorization') || '';
333+ const want = 'Bearer ' + (env.STATIK_BUILD_TOKEN || '');
334+ if (!env.STATIK_BUILD_TOKEN || auth !== want) {
335+ return new Response('unauthorized', { status: 401 });
336+ }
337+
338+ const body = await req.json().catch(() => ({}));
339+ const projectId = body.projectId || 'default';
340+ const pretty = body.pretty ?? DEFAULT_PRETTY;
341+ const route = body.route;
342+
343+ if (!route || typeof route !== 'string') {
344+ return new Response(JSON.stringify({ ok: false, error: 'Missing "route" in body' }), {
345+ status: 400,
346+ headers: { 'content-type': 'application/json' },
347+ });
348+ }
349+
350+ const found = findRouteEntry(REGISTRY, route);
351+ if (!found) {
352+ return new Response(JSON.stringify({ ok: false, error: 'No matching route in registry' }), {
353+ status: 404,
354+ headers: { 'content-type': 'application/json' },
355+ });
356+ }
357+
358+ const { routeEntry, params } = found;
359+
360+ const ctx = { params: params || {}, env };
361+ const value = await routeEntry.mod.data(ctx);
362+ assertSerializable(value);
363+
364+ const text = pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
365+ const key = r2Key(projectId, route);
366+ const etag = await digestETag(text);
367+
368+ await writeJsonToR2(env, key, text, { route, etag });
369+
370+ // update manifest: remove old entry for this route, then add fresh one
371+ const existing = await readManifest(env, projectId);
372+ const next = (existing || []).filter((e) => e.route !== route);
373+ const bytes = (new TextEncoder().encode(text)).length;
374+
375+ next.push({
376+ route,
377+ filePath: key,
378+ bytes,
379+ mtime: Date.now(),
380+ hash: etag.replace(/"/g, ''),
381+ });
382+
383+ await writeManifest(env, projectId, next);
384+
385+ const resBody = {
386+ ok: true,
387+ route,
388+ filePath: key,
389+ bytes,
390+ };
391+
392+ return new Response(JSON.stringify(resBody), {
393+ headers: { 'content-type': 'application/json' },
394+ });
395+ }
396+
290397 async function handleBuild(req, env) {
291398 const auth = req.headers.get('authorization') || '';
292399 const want = 'Bearer ' + (env.STATIK_BUILD_TOKEN || '');
@@ -336,6 +443,10 @@ const WORKER_RUNTIME_JS = `
336443 async fetch(req, env) {
337444 const url = new URL(req.url);
338445
446+ if (req.method === 'POST' && url.pathname === '/__statikapi/build/route') {
447+ return handleBuildRoute(req, env);
448+ }
449+
339450 if (req.method === 'POST' && url.pathname === '/__statikapi/build') {
340451 return handleBuild(req, env);
341452 }
0 commit comments