@cfdez11/vex 0.8.2 → 0.9.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.
- package/dist/bin/vex.js +3 -0
- package/dist/client/services/cache.js +1 -0
- package/dist/client/services/hmr-client.js +1 -0
- package/dist/client/services/html.js +1 -0
- package/dist/client/services/hydrate-client-components.js +1 -0
- package/dist/client/services/hydrate.js +1 -0
- package/dist/client/services/index.js +1 -0
- package/dist/client/services/navigation/create-layouts.js +1 -0
- package/dist/client/services/navigation/create-navigation.js +1 -0
- package/dist/client/services/navigation/index.js +1 -0
- package/dist/client/services/navigation/link-interceptor.js +1 -0
- package/dist/client/services/navigation/metadata.js +1 -0
- package/dist/client/services/navigation/navigate.js +1 -0
- package/dist/client/services/navigation/prefetch.js +1 -0
- package/dist/client/services/navigation/render-page.js +1 -0
- package/dist/client/services/navigation/render-ssr.js +1 -0
- package/dist/client/services/navigation/router.js +1 -0
- package/dist/client/services/navigation/use-query-params.js +1 -0
- package/dist/client/services/navigation/use-route-params.js +1 -0
- package/dist/client/services/navigation.js +1 -0
- package/dist/client/services/reactive.js +1 -0
- package/dist/server/build-static.js +6 -0
- package/dist/server/index.js +4 -0
- package/dist/server/prebuild.js +1 -0
- package/dist/server/utils/cache.js +1 -0
- package/dist/server/utils/component-processor.js +68 -0
- package/dist/server/utils/data-cache.js +1 -0
- package/dist/server/utils/esbuild-plugin.js +1 -0
- package/dist/server/utils/files.js +28 -0
- package/dist/server/utils/hmr.js +1 -0
- package/dist/server/utils/router.js +11 -0
- package/dist/server/utils/streaming.js +1 -0
- package/dist/server/utils/template.js +1 -0
- package/package.json +8 -7
- package/bin/vex.js +0 -69
- package/client/favicon.ico +0 -0
- package/client/services/cache.js +0 -55
- package/client/services/hmr-client.js +0 -22
- package/client/services/html.js +0 -377
- package/client/services/hydrate-client-components.js +0 -97
- package/client/services/hydrate.js +0 -25
- package/client/services/index.js +0 -9
- package/client/services/navigation/create-layouts.js +0 -172
- package/client/services/navigation/create-navigation.js +0 -103
- package/client/services/navigation/index.js +0 -8
- package/client/services/navigation/link-interceptor.js +0 -39
- package/client/services/navigation/metadata.js +0 -23
- package/client/services/navigation/navigate.js +0 -64
- package/client/services/navigation/prefetch.js +0 -43
- package/client/services/navigation/render-page.js +0 -45
- package/client/services/navigation/render-ssr.js +0 -157
- package/client/services/navigation/router.js +0 -48
- package/client/services/navigation/use-query-params.js +0 -225
- package/client/services/navigation/use-route-params.js +0 -76
- package/client/services/navigation.js +0 -6
- package/client/services/reactive.js +0 -247
- package/server/build-static.js +0 -138
- package/server/index.js +0 -135
- package/server/prebuild.js +0 -13
- package/server/utils/cache.js +0 -89
- package/server/utils/component-processor.js +0 -1631
- package/server/utils/data-cache.js +0 -62
- package/server/utils/delay.js +0 -1
- package/server/utils/esbuild-plugin.js +0 -110
- package/server/utils/files.js +0 -845
- package/server/utils/hmr.js +0 -21
- package/server/utils/router.js +0 -375
- package/server/utils/streaming.js +0 -324
- package/server/utils/template.js +0 -274
- /package/{client → dist/client}/app.webmanifest +0 -0
- /package/{server → dist/server}/root.html +0 -0
package/server/utils/hmr.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMR (Hot Module Replacement) event bus for dev mode (FEAT-03).
|
|
3
|
-
*
|
|
4
|
-
* When a `.html` file changes in dev:
|
|
5
|
-
* 1. The file watcher in `component-processor.js` invalidates the in-memory
|
|
6
|
-
* caches (processHtmlFileCache, parsedTemplateCache, etc.) and re-generates
|
|
7
|
-
* the client bundle for that file, then calls `hmrEmitter.emit('reload')`.
|
|
8
|
-
* 2. The SSE endpoint in `index.js` listens on `hmrEmitter` and forwards the
|
|
9
|
-
* event to all connected browsers.
|
|
10
|
-
* 3. The HMR client script in the browser receives the event and triggers a
|
|
11
|
-
* full-page reload so the user sees the updated page immediately.
|
|
12
|
-
*
|
|
13
|
-
* Using a single shared EventEmitter avoids coupling the file watcher
|
|
14
|
-
* (component-processor.js) directly to the HTTP server (index.js).
|
|
15
|
-
*
|
|
16
|
-
* This module is a no-op in production — nothing imports it there.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { EventEmitter } from "events";
|
|
20
|
-
|
|
21
|
-
export const hmrEmitter = new EventEmitter();
|
package/server/utils/router.js
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
renderSuspenseComponent,
|
|
3
|
-
generateReplacementContent,
|
|
4
|
-
} from "./streaming.js";
|
|
5
|
-
import { getCachedComponentHtml, getRevalidateSeconds, revalidateCachedComponentHtml, saveCachedComponentHtml } from "./cache.js";
|
|
6
|
-
import { getPagePath } from "./files.js";
|
|
7
|
-
import { renderPageWithLayout } from "./component-processor.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Routes currently being regenerated in the background.
|
|
11
|
-
*
|
|
12
|
-
* When an ISR page is stale we serve the cached version immediately and kick off
|
|
13
|
-
* a background re-render. This Set prevents multiple concurrent requests from
|
|
14
|
-
* all triggering their own regeneration for the same route simultaneously —
|
|
15
|
-
* only the first one wins; the others get the stale cache until it is replaced.
|
|
16
|
-
*/
|
|
17
|
-
const revalidatingRoutes = new Set();
|
|
18
|
-
|
|
19
|
-
const FALLBACK_ERROR_HTML = `
|
|
20
|
-
<!DOCTYPE html>
|
|
21
|
-
<html>
|
|
22
|
-
<head><title>Error 500</title></head>
|
|
23
|
-
<body>
|
|
24
|
-
<h1>Error 500 - Internal Server Error</h1>
|
|
25
|
-
<p>An unexpected error has occurred.</p>
|
|
26
|
-
<p><a href="/">Back to home</a></p>
|
|
27
|
-
</body>
|
|
28
|
-
</html>
|
|
29
|
-
`;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Sends HTML response
|
|
33
|
-
* @param {import("http").ServerResponse} res
|
|
34
|
-
* @param {number} statusCode
|
|
35
|
-
* @param {string} html
|
|
36
|
-
*/
|
|
37
|
-
const sendResponse = (res, statusCode, html) => {
|
|
38
|
-
res.writeHead(statusCode, { "Content-Type": "text/html" });
|
|
39
|
-
res.end(html);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Start stream response, sending html and update html chunks
|
|
44
|
-
* @param {import("http").ServerResponse} res
|
|
45
|
-
* @param {string[]} htmlChunks
|
|
46
|
-
*/
|
|
47
|
-
const sendStartStreamChunkResponse = (res, statusCode, html, htmlChunks) => {
|
|
48
|
-
res.writeHead(statusCode, {
|
|
49
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
50
|
-
"Transfer-Encoding": "chunked",
|
|
51
|
-
"X-Content-Type-Options": "nosniff",
|
|
52
|
-
});
|
|
53
|
-
sendStreamChunkResponse(res, html, htmlChunks)
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Send html and update html chunks
|
|
58
|
-
* @param {import("http").ServerResponse} res
|
|
59
|
-
* @param {string[]} htmlChunks
|
|
60
|
-
*/
|
|
61
|
-
const sendStreamChunkResponse = (res, html, htmlChunks) => {
|
|
62
|
-
res.write(html);
|
|
63
|
-
htmlChunks.push(html);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Close html and end response
|
|
68
|
-
* @param {import("http").ServerResponse} res
|
|
69
|
-
* @param {string[]} htmlChunks
|
|
70
|
-
*/
|
|
71
|
-
const endStreamResponse = (res, htmlChunks) => {
|
|
72
|
-
res.write("</body></html>");
|
|
73
|
-
res.end();
|
|
74
|
-
htmlChunks.push("</body></html>")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Renders a page using SSR, optionally streams suspense components,
|
|
79
|
-
* applies Incremental Static Regeneration (ISR) when enabled,
|
|
80
|
-
* and sends the resulting HTML response to the client.
|
|
81
|
-
*
|
|
82
|
-
* This function supports:
|
|
83
|
-
* - Full server-side rendering
|
|
84
|
-
* - Streaming suspense components
|
|
85
|
-
* - ISR with cache revalidation
|
|
86
|
-
* - Graceful abort handling on client disconnect
|
|
87
|
-
*
|
|
88
|
-
* @async
|
|
89
|
-
* @function renderAndSendPage
|
|
90
|
-
*
|
|
91
|
-
* @param {Object} params
|
|
92
|
-
* @param {string} params.pageName
|
|
93
|
-
* Name of the page to be rendered (resolved to a server component path).
|
|
94
|
-
*
|
|
95
|
-
* @param {number} [params.statusCode=200]
|
|
96
|
-
* HTTP status code used for the response.
|
|
97
|
-
*
|
|
98
|
-
* @param {Object} [params.context={}]
|
|
99
|
-
* Rendering context shared across server components.
|
|
100
|
-
*
|
|
101
|
-
* @param {import("http").IncomingMessage} params.context.req
|
|
102
|
-
* Incoming HTTP request instance.
|
|
103
|
-
*
|
|
104
|
-
* @param {import("http").ServerResponse} params.context.res
|
|
105
|
-
* HTTP response instance used to stream or send HTML.
|
|
106
|
-
*
|
|
107
|
-
* @param {Object.<string, any>} [params.context]
|
|
108
|
-
* Additional arbitrary values exposed to the rendering pipeline.
|
|
109
|
-
*
|
|
110
|
-
* @param {Object} params.route
|
|
111
|
-
* Matched route configuration.
|
|
112
|
-
*
|
|
113
|
-
* @param {string} params.route.path
|
|
114
|
-
* Public route path (e.g. "/home").
|
|
115
|
-
*
|
|
116
|
-
* @param {string} params.route.serverPath
|
|
117
|
-
* Internal server path used for resolution.
|
|
118
|
-
*
|
|
119
|
-
* @param {Object} params.route.meta
|
|
120
|
-
* Route metadata.
|
|
121
|
-
*
|
|
122
|
-
* @param {boolean} params.route.meta.ssr
|
|
123
|
-
* Whether the route supports server-side rendering.
|
|
124
|
-
*
|
|
125
|
-
* @param {boolean} params.route.meta.requiresAuth
|
|
126
|
-
* Indicates if authentication is required.
|
|
127
|
-
*
|
|
128
|
-
* @param {number} params.route.meta.revalidate
|
|
129
|
-
* ISR revalidation interval in seconds. A value > 0 enables ISR.
|
|
130
|
-
*
|
|
131
|
-
* @returns {Promise<void>}
|
|
132
|
-
* Resolves once the response has been fully sent.
|
|
133
|
-
*
|
|
134
|
-
* @throws {Error}
|
|
135
|
-
* Throws if rendering or streaming fails before the response is committed.
|
|
136
|
-
*/
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Re-renders a stale ISR page and saves the result to cache without sending
|
|
140
|
-
* any HTTP response.
|
|
141
|
-
*
|
|
142
|
-
* Renders with `awaitSuspenseComponents = true` so all Suspense boundaries
|
|
143
|
-
* are resolved synchronously and the full HTML is available in one pass.
|
|
144
|
-
*
|
|
145
|
-
* @param {string} pagePath - Absolute path to the page .html file.
|
|
146
|
-
* @param {object} context - Request context (provides req.params for getData).
|
|
147
|
-
* @param {string} cacheKey - Normalised pathname used as the ISR cache key.
|
|
148
|
-
*/
|
|
149
|
-
async function revalidateInBackground(pagePath, context, cacheKey) {
|
|
150
|
-
try {
|
|
151
|
-
// awaitSuspenseComponents=true resolves all suspense boundaries synchronously
|
|
152
|
-
// so we get the complete HTML in a single renderPageWithLayout call.
|
|
153
|
-
const { html } = await renderPageWithLayout(pagePath, context, true);
|
|
154
|
-
await saveCachedComponentHtml({ componentPath: cacheKey, html });
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error(`[ISR] Background revalidation failed for ${cacheKey}:`, error.message);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
async function renderAndSendPage({
|
|
160
|
-
pageName,
|
|
161
|
-
statusCode = 200,
|
|
162
|
-
context = {},
|
|
163
|
-
route,
|
|
164
|
-
}) {
|
|
165
|
-
const pagePath = getPagePath(pageName);
|
|
166
|
-
const revalidateSeconds = getRevalidateSeconds(route.meta?.revalidate ?? 0);
|
|
167
|
-
const isISR = revalidateSeconds !== 0;
|
|
168
|
-
|
|
169
|
-
// Normalise the cache key to pathname only
|
|
170
|
-
// `context.req.url` includes the query string (e.g. `/page?debug=true`).
|
|
171
|
-
// Using the full URL as a cache key means `/page` and `/page?debug=true`
|
|
172
|
-
// generate two separate cache entries for the same page content.
|
|
173
|
-
const isrCacheKey = new URL(context.req.url, "http://x").pathname;
|
|
174
|
-
|
|
175
|
-
if(isISR) {
|
|
176
|
-
const { html: cachedHtml, isStale } = await getCachedComponentHtml({
|
|
177
|
-
componentPath: isrCacheKey,
|
|
178
|
-
revalidateSeconds: revalidateSeconds,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
if (cachedHtml && !isStale) {
|
|
182
|
-
sendResponse(context.res, statusCode, cachedHtml);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Stale-while-revalidate: serve stale content immediately so the
|
|
187
|
-
// user never waits for a re-render, then regenerate the page in the background.
|
|
188
|
-
// The lock prevents multiple concurrent requests from all re-rendering at once.
|
|
189
|
-
if (cachedHtml && isStale) {
|
|
190
|
-
sendResponse(context.res, statusCode, cachedHtml);
|
|
191
|
-
if (!revalidatingRoutes.has(isrCacheKey)) {
|
|
192
|
-
revalidatingRoutes.add(isrCacheKey);
|
|
193
|
-
revalidateInBackground(pagePath, context, isrCacheKey)
|
|
194
|
-
.finally(() => revalidatingRoutes.delete(isrCacheKey));
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const { html, suspenseComponents, serverComponents } =
|
|
201
|
-
await renderPageWithLayout(pagePath, context);
|
|
202
|
-
|
|
203
|
-
// if no suspense components, send immediately
|
|
204
|
-
if (suspenseComponents.length === 0) {
|
|
205
|
-
sendResponse(context.res, statusCode, html);
|
|
206
|
-
|
|
207
|
-
if(isISR) {
|
|
208
|
-
saveCachedComponentHtml({ componentPath: isrCacheKey, html });
|
|
209
|
-
}
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const htmlChunks = [];
|
|
214
|
-
let abortedStream = false;
|
|
215
|
-
let errorStream = false
|
|
216
|
-
|
|
217
|
-
context.res.on("close", () => abortedStream = true);
|
|
218
|
-
|
|
219
|
-
// send initial HTML (before </body>)
|
|
220
|
-
const [beforeClosing] = html.split("</body>");
|
|
221
|
-
sendStartStreamChunkResponse(context.res, 200, beforeClosing, htmlChunks)
|
|
222
|
-
|
|
223
|
-
// stream suspense components
|
|
224
|
-
const renderPromises = suspenseComponents.map(async (suspenseComponent) => {
|
|
225
|
-
try {
|
|
226
|
-
const renderedContent = await renderSuspenseComponent(
|
|
227
|
-
suspenseComponent,
|
|
228
|
-
serverComponents
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const replacementContent = generateReplacementContent(
|
|
232
|
-
suspenseComponent.id,
|
|
233
|
-
renderedContent
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
sendStreamChunkResponse(context.res, replacementContent, htmlChunks)
|
|
237
|
-
} catch (error) {
|
|
238
|
-
console.error(`Error rendering suspense ${suspenseComponent.id}:`, error);
|
|
239
|
-
|
|
240
|
-
const errorContent = generateReplacementContent(
|
|
241
|
-
suspenseComponent.id,
|
|
242
|
-
`<div class="text-red-500">Error loading content</div>`
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
context.res.write(errorContent);
|
|
246
|
-
errorStream = true;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
await Promise.all(renderPromises);
|
|
251
|
-
|
|
252
|
-
endStreamResponse(context.res, htmlChunks);
|
|
253
|
-
|
|
254
|
-
if(isISR && !abortedStream && !errorStream) {
|
|
255
|
-
saveCachedComponentHtml({
|
|
256
|
-
componentPath: isrCacheKey,
|
|
257
|
-
html: htmlChunks.join("")
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Handles an incoming HTTP request for a page route.
|
|
264
|
-
*
|
|
265
|
-
* Resolves the appropriate route, builds the rendering context,
|
|
266
|
-
* delegates rendering to `renderAndSendPage`, and ensures that
|
|
267
|
-
* errors are handled gracefully by rendering a fallback error page.
|
|
268
|
-
*
|
|
269
|
-
* @async
|
|
270
|
-
* @function handlePageRequest
|
|
271
|
-
*
|
|
272
|
-
* @param {import("http").IncomingMessage} req
|
|
273
|
-
* Incoming HTTP request.
|
|
274
|
-
*
|
|
275
|
-
* @param {import("http").ServerResponse} res
|
|
276
|
-
* HTTP response used to send rendered content.
|
|
277
|
-
*
|
|
278
|
-
* @param {Object|null} route
|
|
279
|
-
* Matched route definition. If null, a fallback 404 route is used.
|
|
280
|
-
*
|
|
281
|
-
* @param {string} route.path
|
|
282
|
-
* Public URL path of the route.
|
|
283
|
-
*
|
|
284
|
-
* @param {Object} route.meta
|
|
285
|
-
* Route metadata used during rendering.
|
|
286
|
-
*
|
|
287
|
-
* @returns {Promise<void>}
|
|
288
|
-
* Resolves once the response has been fully handled.
|
|
289
|
-
*/
|
|
290
|
-
export async function handlePageRequest(req, res, route) {
|
|
291
|
-
const pageName = route.path.slice(1);
|
|
292
|
-
|
|
293
|
-
const context = { req, res };
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
await renderAndSendPage({ pageName, context, route });
|
|
297
|
-
} catch (e) {
|
|
298
|
-
console.error(`[500] Error rendering page "${route.path}":`, e);
|
|
299
|
-
// redirect() in a server script throws a structured error.
|
|
300
|
-
// Intercept it before the generic 500 handler so the browser gets a proper redirect.
|
|
301
|
-
if (e.redirect) {
|
|
302
|
-
res.redirect(e.redirect.statusCode, e.redirect.path);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const errorData = {
|
|
307
|
-
message: e.message || "Internal server error",
|
|
308
|
-
code: 500,
|
|
309
|
-
details: "Could not load the requested page",
|
|
310
|
-
path: route.path,
|
|
311
|
-
stack: e.stack,
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
await renderAndSendPage({
|
|
316
|
-
pageName: "error",
|
|
317
|
-
statusCode: 500,
|
|
318
|
-
context: {
|
|
319
|
-
...context,
|
|
320
|
-
...errorData,
|
|
321
|
-
},
|
|
322
|
-
route,
|
|
323
|
-
});
|
|
324
|
-
} catch (err) {
|
|
325
|
-
console.warn('error}}}}}}}}}}', err)
|
|
326
|
-
console.error(`Failed to render error page: ${err.message}`);
|
|
327
|
-
sendResponse(res, 500, FALLBACK_ERROR_HTML);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Handler to mark a cached component or page as stale for ISR-like revalidation.
|
|
335
|
-
*
|
|
336
|
-
* This endpoint allows clients to request that the server invalidate the cached HTML
|
|
337
|
-
* of a specific component or page. The cache will be regenerated automatically
|
|
338
|
-
* on the next request for that component.
|
|
339
|
-
*
|
|
340
|
-
* @async
|
|
341
|
-
* @param {import('express').Request} req - The Express request object. Expects a query parameter `path` specifying the component/page path to revalidate.
|
|
342
|
-
* @param {import('express').Response} res - The Express response object. Will send a JSON response indicating success or failure.
|
|
343
|
-
*
|
|
344
|
-
* @returns {Promise<import('express').Response>} JSON response with:
|
|
345
|
-
* - 200: { message: string } if the cache was successfully marked as stale
|
|
346
|
-
* - 400: { error: string } if the required `path` query parameter is missing
|
|
347
|
-
* - 500: { error: string } if an unexpected error occurs during revalidation
|
|
348
|
-
*
|
|
349
|
-
* @example
|
|
350
|
-
* Client request:
|
|
351
|
-
* POST /revalidate?path=/about
|
|
352
|
-
*
|
|
353
|
-
* Response:
|
|
354
|
-
* {
|
|
355
|
-
* "message": "Cache for '/about' marked as stale. It will regenerate on next request."
|
|
356
|
-
* }
|
|
357
|
-
*/
|
|
358
|
-
export async function revalidatePath(req, res) {
|
|
359
|
-
try {
|
|
360
|
-
const componentPath = req.query.path;
|
|
361
|
-
|
|
362
|
-
if (!componentPath) {
|
|
363
|
-
return res.status(400).json({ error: "Missing 'path' query parameter" });
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
await revalidateCachedComponentHtml(componentPath);
|
|
367
|
-
|
|
368
|
-
return res.status(200).json({
|
|
369
|
-
message: `Cache for '${componentPath}' marked as stale. It will regenerate on next request.`
|
|
370
|
-
});
|
|
371
|
-
} catch (err) {
|
|
372
|
-
console.error("Error revalidating cache:", err);
|
|
373
|
-
return res.status(500).json({ error: "Failed to revalidate cache" });
|
|
374
|
-
}
|
|
375
|
-
}
|