@cfdez11/vex 0.8.3 → 0.9.1

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.
Files changed (71) hide show
  1. package/dist/bin/vex.js +3 -0
  2. package/dist/client/services/cache.js +1 -0
  3. package/dist/client/services/hmr-client.js +1 -0
  4. package/dist/client/services/html.js +1 -0
  5. package/dist/client/services/hydrate-client-components.js +1 -0
  6. package/dist/client/services/hydrate.js +1 -0
  7. package/dist/client/services/index.js +1 -0
  8. package/dist/client/services/navigation/create-layouts.js +1 -0
  9. package/dist/client/services/navigation/create-navigation.js +1 -0
  10. package/dist/client/services/navigation/index.js +1 -0
  11. package/dist/client/services/navigation/link-interceptor.js +1 -0
  12. package/dist/client/services/navigation/metadata.js +1 -0
  13. package/dist/client/services/navigation/navigate.js +1 -0
  14. package/dist/client/services/navigation/prefetch.js +1 -0
  15. package/dist/client/services/navigation/render-page.js +1 -0
  16. package/dist/client/services/navigation/render-ssr.js +1 -0
  17. package/dist/client/services/navigation/router.js +1 -0
  18. package/dist/client/services/navigation/use-query-params.js +1 -0
  19. package/dist/client/services/navigation/use-route-params.js +1 -0
  20. package/dist/client/services/navigation.js +1 -0
  21. package/dist/client/services/reactive.js +1 -0
  22. package/dist/server/build-static.js +6 -0
  23. package/dist/server/index.js +4 -0
  24. package/dist/server/prebuild.js +1 -0
  25. package/dist/server/utils/cache.js +1 -0
  26. package/dist/server/utils/component-processor.js +68 -0
  27. package/dist/server/utils/data-cache.js +1 -0
  28. package/dist/server/utils/esbuild-plugin.js +1 -0
  29. package/dist/server/utils/files.js +28 -0
  30. package/dist/server/utils/hmr.js +1 -0
  31. package/dist/server/utils/router.js +11 -0
  32. package/dist/server/utils/streaming.js +1 -0
  33. package/dist/server/utils/template.js +1 -0
  34. package/package.json +8 -7
  35. package/bin/vex.js +0 -69
  36. package/client/favicon.ico +0 -0
  37. package/client/services/cache.js +0 -55
  38. package/client/services/hmr-client.js +0 -22
  39. package/client/services/html.js +0 -378
  40. package/client/services/hydrate-client-components.js +0 -97
  41. package/client/services/hydrate.js +0 -25
  42. package/client/services/index.js +0 -9
  43. package/client/services/navigation/create-layouts.js +0 -172
  44. package/client/services/navigation/create-navigation.js +0 -103
  45. package/client/services/navigation/index.js +0 -8
  46. package/client/services/navigation/link-interceptor.js +0 -39
  47. package/client/services/navigation/metadata.js +0 -23
  48. package/client/services/navigation/navigate.js +0 -64
  49. package/client/services/navigation/prefetch.js +0 -43
  50. package/client/services/navigation/render-page.js +0 -45
  51. package/client/services/navigation/render-ssr.js +0 -157
  52. package/client/services/navigation/router.js +0 -48
  53. package/client/services/navigation/use-query-params.js +0 -225
  54. package/client/services/navigation/use-route-params.js +0 -76
  55. package/client/services/navigation.js +0 -6
  56. package/client/services/reactive.js +0 -247
  57. package/server/build-static.js +0 -138
  58. package/server/index.js +0 -135
  59. package/server/prebuild.js +0 -13
  60. package/server/utils/cache.js +0 -89
  61. package/server/utils/component-processor.js +0 -1631
  62. package/server/utils/data-cache.js +0 -62
  63. package/server/utils/delay.js +0 -1
  64. package/server/utils/esbuild-plugin.js +0 -110
  65. package/server/utils/files.js +0 -845
  66. package/server/utils/hmr.js +0 -21
  67. package/server/utils/router.js +0 -375
  68. package/server/utils/streaming.js +0 -324
  69. package/server/utils/template.js +0 -274
  70. /package/{client → dist/client}/app.webmanifest +0 -0
  71. /package/{server → dist/server}/root.html +0 -0
@@ -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();
@@ -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
- }