Skip to content

Commit caeca52

Browse files
committed
feat(adapter-cf): individual route rebuild trigger
1 parent 9c8c1fc commit caeca52

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export async function paths() {
2+
// first 10 posts
3+
return Array.from({ length: 10 }, (_, i) => String(i + 1));
4+
}
5+
6+
export async function data({ params }) {
7+
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
8+
const post = await res.json();
9+
10+
const commentsRes = await fetch(
11+
`https://jsonplaceholder.typicode.com/posts/${params.id}/comments`
12+
);
13+
const comments = await commentsRes.json();
14+
15+
return {
16+
id: post.id,
17+
title: post.title,
18+
body: post.body,
19+
comments: comments.map((c) => ({ id: c.id, name: c.name, email: c.email })),
20+
};
21+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export async function data() {
2+
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
3+
const posts = await res.json();
4+
5+
return {
6+
total: posts.length,
7+
items: posts.map((p) => ({ id: p.id, title: p.title, userId: p.userId })),
8+
};
9+
}

packages/adapter-cloudflare/src/node/bundle.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)