@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,103 +0,0 @@
1
- import { setupLinkInterceptor } from "./link-interceptor.js";
2
- import { setupPrefetchObserver } from "./prefetch.js";
3
- import { navigateInternal } from "./navigate.js";
4
- import { findRouteWithParams } from "./router.js";
5
- import { createLayoutRenderer } from "./create-layouts.js";
6
-
7
- /**
8
- * Creates a new SPA navigation runtime with encapsulated state.
9
- *
10
- * This factory returns a navigation runtime that manages:
11
- * - Current navigation controller (for aborting in-progress navigations)
12
- * - Layout rendering via `layoutRenderer`
13
- * - Link interception for SPA navigation
14
- * - Prefetch observer setup
15
- * - Popstate handling for back/forward browser navigation
16
- *
17
- * Each instance is isolated and maintains its own state, making it safe
18
- * to use multiple runtimes or for testing purposes.
19
- *
20
- * @returns {Object} The navigation runtime API.
21
- * @property {(path: string, addToHistory?: boolean) => Promise<void>} navigate - Programmatically navigate to a given path.
22
- * @property {() => void} initialize - Initializes the SPA router and sets up link interception, prefetching, and initial navigation.
23
- */
24
- export function createNavigationRuntime() {
25
- /** @type {AbortController | null} - Controller for the current navigation request */
26
- let currentNavigationController = null;
27
-
28
- /** Layout renderer instance for wrapping pages with layouts */
29
- const layoutRenderer = createLayoutRenderer();
30
-
31
- /**
32
- * Aborts the currently active navigation, if any.
33
- * @private
34
- */
35
- function abortPrevious() {
36
- if (currentNavigationController) {
37
- currentNavigationController.abort();
38
- }
39
- }
40
-
41
- /**
42
- * Performs SPA navigation to the specified path.
43
- *
44
- * Aborts any in-progress navigation, manages history state, and renders
45
- * the target route. Handles SSR routes and layout rendering internally.
46
- *
47
- * @param {string} path - The URL path to navigate to.
48
- * @param {boolean} [addToHistory=true] - Whether to push this navigation to the browser history.
49
- * @returns {Promise<void>} Resolves when navigation is complete.
50
- */
51
- async function navigate(path, addToHistory = true) {
52
- abortPrevious();
53
-
54
- const controller = new AbortController();
55
- currentNavigationController = controller;
56
-
57
- try {
58
- await navigateInternal({
59
- path,
60
- addToHistory,
61
- controller,
62
- layoutRenderer,
63
- onFinish: () => {
64
- if (currentNavigationController === controller) {
65
- currentNavigationController = null;
66
- }
67
- },
68
- });
69
- } catch (e) {
70
- if (e.name !== "AbortError") {
71
- console.error("Navigation error:", e);
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Initializes the SPA router.
78
- *
79
- * Sets up:
80
- * - Popstate listener for browser back/forward navigation
81
- * - Link interception for SPA navigation
82
- * - Prefetch observer for internal links
83
- * - Initial navigation if the current route is not SSR
84
- *
85
- * Must be called after DOMContentLoaded.
86
- */
87
- function initialize() {
88
- window.addEventListener("popstate", () => {
89
- navigate(location.pathname, false);
90
- });
91
-
92
- setupLinkInterceptor(navigate);
93
- setupPrefetchObserver();
94
- layoutRenderer.reset();
95
-
96
- const { route } = findRouteWithParams(location.pathname);
97
- if (!route?.meta?.ssr) {
98
- navigate(location.pathname, false);
99
- }
100
- }
101
-
102
- return { navigate, initialize };
103
- }
@@ -1,8 +0,0 @@
1
- import { createNavigationRuntime } from "./create-navigation.js";
2
-
3
- const navigation = createNavigationRuntime();
4
-
5
- export const initializeRouter = navigation.initialize;
6
- export const navigate = navigation.navigate;
7
- export { useRouteParams } from "./use-route-params.js";
8
- export { useQueryParams } from "./use-query-params.js";
@@ -1,39 +0,0 @@
1
- /**
2
- * Sets up a global click listener to intercept internal link clicks
3
- * and enable SPA-style navigation without full page reloads.
4
- *
5
- * This function ignores links that:
6
- * - Are external (different origin)
7
- * - Have `target="_blank"` or `rel="external"`
8
- * - Have a `data-reload` attribute
9
- * - Are hash links (start with "#")
10
- *
11
- * When a valid internal link is clicked, it prevents the default browser behavior
12
- * and invokes the provided `navigate` function with the link's path.
13
- *
14
- * @param {(path: string) => void} navigate - A function to handle navigation
15
- * to the given path (e.g., your SPA router's `navigate` method).
16
- */
17
- export function setupLinkInterceptor(navigate) {
18
- document.addEventListener("click", (event) => {
19
- const link = event.target.closest("a");
20
- if (!link) return;
21
-
22
- const href = link.getAttribute("href");
23
- if (!href || href.startsWith("#")) return;
24
-
25
- const url = new URL(href, window.location.origin);
26
-
27
- if (
28
- url.origin !== window.location.origin ||
29
- link.dataset.reload !== undefined ||
30
- link.target === "_blank" ||
31
- link.rel === "external"
32
- ) {
33
- return;
34
- }
35
-
36
- event.preventDefault();
37
- navigate(url.pathname);
38
- });
39
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Updates the document's `<title>` and `<meta name="description">` tags
3
- * based on the provided metadata.
4
- *
5
- * If a `<meta name="description">` tag does not exist, it will be created.
6
- *
7
- * @param {Object} metadata - Page metadata to apply.
8
- * @param {string} [metadata.title] - Title to set for the document.
9
- * @param {string} [metadata.description] - Description to set in the meta tag.
10
- */
11
- export function addMetadata(metadata) {
12
- if (metadata.title) document.title = metadata.title;
13
-
14
- if (metadata.description) {
15
- let meta = document.querySelector('meta[name="description"]');
16
- if (!meta) {
17
- meta = document.createElement("meta");
18
- meta.name = "description";
19
- document.head.appendChild(meta);
20
- }
21
- meta.content = metadata.description;
22
- }
23
- }
@@ -1,64 +0,0 @@
1
- import { findRouteWithParams } from "./router.js";
2
- import { updateRouteParams } from "./use-route-params.js";
3
- import { renderPage } from "./render-page.js";
4
- import { renderSSRPage } from "./render-ssr.js";
5
-
6
- /**
7
- * Handles the internal SPA navigation logic.
8
- *
9
- * This function performs the following tasks:
10
- * - Updates the route parameters store.
11
- * - Updates browser history (if `addToHistory` is true).
12
- * - Resolves the target route using the router.
13
- * - Handles SSR routes by fetching and rendering via streaming.
14
- * - Checks authentication and access restrictions, redirecting if necessary.
15
- * - Renders the target page component and its layouts.
16
- * - Calls `onFinish` when navigation is complete, regardless of success or error.
17
- *
18
- * @param {Object} options - Navigation options.
19
- * @param {string} options.path - The target URL path for navigation.
20
- * @param {boolean} options.addToHistory - Whether to push this navigation to browser history.
21
- * @param {AbortController} options.controller - The controller used to cancel the navigation if needed.
22
- * @param {import('./create-layouts.js').LayoutRenderer} options.layoutRenderer - Instance responsible for rendering layouts.
23
- * @param {() => void} options.onFinish - Callback invoked when navigation completes or is aborted.
24
- *
25
- * @returns {Promise<void>} Resolves when navigation is complete.
26
- */
27
- export async function navigateInternal({
28
- path,
29
- addToHistory,
30
- controller,
31
- layoutRenderer,
32
- onFinish,
33
- }) {
34
- updateRouteParams(path);
35
-
36
- const routePath = path.split("?")[0];
37
- const { route } = findRouteWithParams(routePath);
38
-
39
- if (addToHistory) {
40
- history.pushState({}, "", path);
41
- }
42
-
43
- try {
44
- if (route?.meta?.ssr) {
45
- layoutRenderer.reset();
46
- await renderSSRPage(path, controller.signal);
47
- return;
48
- }
49
-
50
- if (route?.meta?.requiresAuth && !app.Store?.loggedIn) {
51
- location.href = "/account/login";
52
- return;
53
- }
54
-
55
- if (route?.meta?.guestOnly && app.Store?.loggedIn) {
56
- location.href = "/account";
57
- return;
58
- }
59
-
60
- await renderPage({ route, layoutRenderer });
61
- } finally {
62
- onFinish();
63
- }
64
- }
@@ -1,43 +0,0 @@
1
- import { routes } from "../_routes.js";
2
- import { prefetchRouteComponent } from "../cache.js";
3
-
4
- /**
5
- * Sets up an IntersectionObserver to prefetch route components for links
6
- * with the `data-prefetch` attribute when they enter the viewport.
7
- *
8
- * Prefetched components are loaded in advance to improve SPA navigation performance.
9
- *
10
- * Links are only observed once, and the observer is disconnected for each link
11
- * after prefetching to avoid unnecessary observations.
12
- */
13
- export function setupPrefetchObserver() {
14
- /** @type {IntersectionObserver} */
15
- const observer = new IntersectionObserver(
16
- (entries) => {
17
- entries.forEach((entry) => {
18
- if (!entry.isIntersecting) return;
19
-
20
- /** @type {HTMLAnchorElement} */
21
- const link = entry.target;
22
- if (!link.hasAttribute("data-prefetch")) return;
23
-
24
- const url = new URL(link.href, location.origin);
25
- const route = routes.find((r) => r.path === url.pathname);
26
-
27
- if (route?.component) {
28
- prefetchRouteComponent(route.path, route.component);
29
- observer.unobserve(link);
30
- }
31
- });
32
- },
33
- { rootMargin: "200px" }
34
- );
35
-
36
- // Observe all links with data-prefetch attribute that haven't been observed yet
37
- document.querySelectorAll("a[data-prefetch]").forEach((link) => {
38
- if (!link.__prefetchObserved) {
39
- link.__prefetchObserved = true;
40
- observer.observe(link);
41
- }
42
- });
43
- }
@@ -1,45 +0,0 @@
1
- import { addMetadata } from "./metadata.js";
2
-
3
- /**
4
- * Renders a client-side page component into the DOM, wrapping it with layouts if defined.
5
- *
6
- * This function performs the following steps:
7
- * - Dynamically imports the route component.
8
- * - Hydrates the component into a DOM node.
9
- * - Wraps the page node with layouts using the provided `layoutRenderer`.
10
- * - Patches the layout if it already exists, or replaces the root content.
11
- * - Updates page metadata (title, description).
12
- * - Hydrates any client-side components within the page.
13
- *
14
- * @param {Object} options - The rendering options.
15
- * @param {import('../_routes.js').Route} options.route - The route object containing the component and layout info.
16
- * @param {import('./create-layouts.js').LayoutRenderer} options.layoutRenderer - The layout renderer instance responsible for wrapping layouts.
17
- *
18
- * @returns {Promise<void>} Resolves when the page has been rendered and layouts patched.
19
- */
20
- export async function renderPage({ route, layoutRenderer }) {
21
- if (!route?.component) return;
22
-
23
- const mod = await route.component();
24
- if (!mod.hydrateClientComponent) return;
25
-
26
- const root = document.getElementById("app-root") || document.body;
27
- const marker = document.createElement("template");
28
- const pageNode = mod.hydrateClientComponent(marker);
29
-
30
- const { node, layoutId, metadata } = await layoutRenderer.generate({
31
- routeLayouts: route.layouts,
32
- pageNode,
33
- metadata: mod.metadata,
34
- });
35
-
36
- if (layoutId) {
37
- layoutRenderer.patch(layoutId, node);
38
- } else {
39
- root.innerHTML = "";
40
- root.appendChild(node);
41
- }
42
-
43
- if (metadata) addMetadata(metadata);
44
- hydrateComponents();
45
- }
@@ -1,157 +0,0 @@
1
- /**
2
- * Streams and renders a server-side rendered (SSR) page into the DOM.
3
- *
4
- * This function progressively updates the `<main>` element, `<template>` elements,
5
- * `<script>` tags, and metadata while reading the response stream.
6
- *
7
- * @param {string} path - The URL or path of the SSR page to fetch.
8
- * @param {AbortSignal} signal - AbortSignal to cancel the fetch if needed.
9
- * @throws {Error} If the response body is not readable.
10
- */
11
- export async function renderSSRPage(path, signal) {
12
- const res = await fetch(path, { signal });
13
- if (!res.body) throw new Error("Invalid SSR response");
14
-
15
- const reader = res.body.getReader();
16
- const decoder = new TextDecoder();
17
- const parser = new DOMParser();
18
-
19
- let buffer = "";
20
- const main = document.querySelector("main");
21
-
22
- while (true) {
23
- const { done, value } = await reader.read();
24
- if (done) break;
25
-
26
- buffer += decoder.decode(value, { stream: true });
27
-
28
- buffer = processSSRMain(buffer, parser, main);
29
- buffer = processSSRTemplates(buffer, parser);
30
- buffer = processSSRScripts(buffer, parser);
31
- updateSSRMetadata(buffer, parser);
32
-
33
- hydrateComponents(); // global hydration function
34
- }
35
- }
36
-
37
- /**
38
- * Extracts and renders the `<main>` element from an HTML buffer.
39
- *
40
- * @param {string} buffer - The HTML buffer.
41
- * @param {DOMParser} parser - DOMParser instance.
42
- * @param {HTMLElement|null} mainEl - Existing <main> element in the DOM.
43
- * @returns {string} The remaining buffer after processing the <main> tag.
44
- */
45
- function processSSRMain(buffer, parser, mainEl) {
46
- const match = buffer.match(/<main[\s\S]*?<\/main>/i);
47
- if (!match) return buffer;
48
-
49
- const doc = parser.parseFromString(match[0], "text/html");
50
- const newMain = doc.querySelector("main");
51
-
52
- if (newMain && mainEl) {
53
- mainEl.innerHTML = newMain.innerHTML;
54
- }
55
-
56
- return buffer.slice(match.index + match[0].length);
57
- }
58
-
59
- /**
60
- * Extracts `<template id="...">` elements from the stream buffer and appends
61
- * them to the document so the Suspense hydration script can find them by ID.
62
- *
63
- * When a Suspense boundary resolves the server streams:
64
- * <template id="suspense-X-content">…real content…</template>
65
- * <script src="hydrate.js" data-target="suspense-X" data-source="suspense-X-content"></script>
66
- *
67
- * `hydrate.js` looks up the template by ID and swaps the fallback placeholder
68
- * with its content. The template must be in the DOM before the script runs —
69
- * this function is called before `processSSRScripts`, guaranteeing that order.
70
- *
71
- * @param {string} buffer - The HTML buffer.
72
- * @param {DOMParser} parser - DOMParser instance.
73
- * @returns {string} Remaining buffer after extracting all complete <template> tags.
74
- */
75
- function processSSRTemplates(buffer, parser) {
76
- const regex = /<template\b[^>]*>[\s\S]*?<\/template>/gi;
77
- let match;
78
- let lastIndex = -1;
79
-
80
- while ((match = regex.exec(buffer)) !== null) {
81
- const doc = parser.parseFromString(match[0], "text/html");
82
- const template = doc.querySelector("template");
83
- // Only insert templates with an id — those are Suspense replacement payloads.
84
- // Regular <template> tags inside <main> are already handled by processSSRMain.
85
- if (template?.id) {
86
- document.body.appendChild(template);
87
- }
88
- lastIndex = match.index + match[0].length;
89
- }
90
-
91
- return lastIndex !== -1 ? buffer.slice(lastIndex) : buffer;
92
- }
93
-
94
- /**
95
- * Processes <script> elements from an HTML buffer, executes inline scripts,
96
- * and injects external scripts into the DOM if they don't exist yet.
97
- *
98
- * @param {string} buffer - The HTML buffer.
99
- * @param {DOMParser} parser - DOMParser instance.
100
- * @returns {string} Remaining buffer after processing <script> tags.
101
- */
102
- function processSSRScripts(buffer, parser) {
103
- const regex = /<script[\s\S]*?<\/script>/gi;
104
- let match;
105
-
106
- while ((match = regex.exec(buffer))) {
107
- const script = parser
108
- .parseFromString(match[0], "text/html")
109
- .querySelector("script");
110
-
111
- if (!script) continue;
112
-
113
- if (script.src) {
114
- const exists = [...document.scripts].some((s) => s.src === script.src);
115
- if (!exists) {
116
- const s = document.createElement("script");
117
- s.src = script.src;
118
- s.async = true;
119
- document.head.appendChild(s);
120
- }
121
- } else {
122
- try {
123
- new Function(script.textContent)();
124
- } catch (e) {
125
- console.error("Error executing inline SSR script:", e);
126
- }
127
- }
128
- }
129
-
130
- const end = buffer.lastIndexOf("</script>");
131
- return end !== -1 ? buffer.slice(end + 9) : buffer;
132
- }
133
-
134
- /**
135
- * Updates the document `<title>` and `<meta name="description">` based on
136
- * content found in the HTML buffer.
137
- *
138
- * @param {string} buffer - The HTML buffer.
139
- * @param {DOMParser} parser - DOMParser instance.
140
- */
141
- function updateSSRMetadata(buffer, parser) {
142
- const doc = parser.parseFromString(buffer, "text/html");
143
-
144
- const title = doc.querySelector("title");
145
- if (title) document.title = title.textContent;
146
-
147
- const desc = doc.querySelector('meta[name="description"]');
148
- if (desc) {
149
- let meta = document.querySelector('meta[name="description"]');
150
- if (!meta) {
151
- meta = document.createElement("meta");
152
- meta.name = "description";
153
- document.head.appendChild(meta);
154
- }
155
- meta.content = desc.content;
156
- }
157
- }
@@ -1,48 +0,0 @@
1
- import { routes } from "../_routes.js";
2
-
3
- /**
4
- * Converts a route path string with parameters (e.g., "/page/:city/:team")
5
- * into a RegExp for matching and extracts parameter keys.
6
- *
7
- * @param {string} routePath - The route path pattern.
8
- * @returns {{ regex: RegExp, keys: string[] }} An object containing the RegExp and parameter names.
9
- */
10
- function pathToRegex(routePath) {
11
- const keys = [];
12
- const regex = new RegExp(
13
- "^" +
14
- routePath.replace(/:([^/]+)/g, (_, key) => {
15
- keys.push(key);
16
- return "([^/]+)";
17
- }) +
18
- "$"
19
- );
20
- return { regex, keys };
21
- }
22
-
23
- /**
24
- * Finds the first route matching a given path and extracts route parameters.
25
- *
26
- * Supports both string-based paths with parameters and RegExp-based paths.
27
- *
28
- * @param {string} path - The URL path to match (e.g., "/page/madrid/barcelona").
29
- * @returns {{ route: import('../_routes.js').Route | null, params: Record<string, string> }}
30
- * Returns the matched route and an object of extracted parameters.
31
- */
32
- export function findRouteWithParams(path) {
33
- for (const r of routes) {
34
- if (typeof r.path === "string") {
35
- const { regex, keys } = pathToRegex(r.path);
36
- const match = path.match(regex);
37
- if (match) {
38
- const params = {};
39
- keys.forEach((k, i) => (params[k] = match[i + 1]));
40
- return { route: r, params };
41
- }
42
- } else if (r.path instanceof RegExp && r.path.test(path)) {
43
- return { route: r, params: {} };
44
- }
45
- }
46
-
47
- return { route: null, params: {} };
48
- }