@cfdez11/vex 0.1.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.
Files changed (35) hide show
  1. package/README.md +1383 -0
  2. package/client/app.webmanifest +14 -0
  3. package/client/favicon.ico +0 -0
  4. package/client/services/cache.js +55 -0
  5. package/client/services/hmr-client.js +22 -0
  6. package/client/services/html.js +377 -0
  7. package/client/services/hydrate-client-components.js +97 -0
  8. package/client/services/hydrate.js +25 -0
  9. package/client/services/index.js +9 -0
  10. package/client/services/navigation/create-layouts.js +172 -0
  11. package/client/services/navigation/create-navigation.js +103 -0
  12. package/client/services/navigation/index.js +8 -0
  13. package/client/services/navigation/link-interceptor.js +39 -0
  14. package/client/services/navigation/metadata.js +23 -0
  15. package/client/services/navigation/navigate.js +64 -0
  16. package/client/services/navigation/prefetch.js +43 -0
  17. package/client/services/navigation/render-page.js +45 -0
  18. package/client/services/navigation/render-ssr.js +157 -0
  19. package/client/services/navigation/router.js +48 -0
  20. package/client/services/navigation/use-query-params.js +225 -0
  21. package/client/services/navigation/use-route-params.js +76 -0
  22. package/client/services/reactive.js +231 -0
  23. package/package.json +24 -0
  24. package/server/index.js +115 -0
  25. package/server/prebuild.js +12 -0
  26. package/server/root.html +15 -0
  27. package/server/utils/cache.js +89 -0
  28. package/server/utils/component-processor.js +1526 -0
  29. package/server/utils/data-cache.js +62 -0
  30. package/server/utils/delay.js +1 -0
  31. package/server/utils/files.js +723 -0
  32. package/server/utils/hmr.js +21 -0
  33. package/server/utils/router.js +373 -0
  34. package/server/utils/streaming.js +315 -0
  35. package/server/utils/template.js +263 -0
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Parses the URL search string into a plain object.
3
+ *
4
+ * This function represents the **raw URL state**:
5
+ * - Keys and values are always strings
6
+ * - No defaults are applied
7
+ * - No parsing or validation is performed
8
+ *
9
+ * Example:
10
+ * "?page=2&tags=js,spa" → { page: "2", tags: "js,spa" }
11
+ *
12
+ * @param {string} search - window.location.search
13
+ * @returns {Object.<string, string>}
14
+ */
15
+ function parseRawQuery(search) {
16
+ const out = {};
17
+ const qs = new URLSearchParams(search);
18
+
19
+ for (const [k, v] of qs.entries()) {
20
+ out[k] = v;
21
+ }
22
+
23
+ return out;
24
+ }
25
+
26
+ /**
27
+ * Builds a query string from a raw params object.
28
+ *
29
+ * - Values are stringified
30
+ * - `null` and `undefined` values are omitted
31
+ *
32
+ * Example:
33
+ * { page: "2", tags: "js,spa" } → "page=2&tags=js,spa"
34
+ *
35
+ * @param {Object.<string, any>} raw
36
+ * @returns {string} Query string without leading "?"
37
+ */
38
+ function buildQueryString(raw) {
39
+ const qs = new URLSearchParams();
40
+
41
+ for (const k in raw) {
42
+ if (raw[k] != null) {
43
+ qs.set(k, String(raw[k]));
44
+ }
45
+ }
46
+
47
+ return qs.toString();
48
+ }
49
+
50
+ /**
51
+ * Manages URL query parameters as application state.
52
+ *
53
+ * This hook provides:
54
+ * - Parsing via schema (similar to nuqs)
55
+ * - Default values
56
+ * - URL synchronization (push / replace)
57
+ * - Back/forward navigation support
58
+ *
59
+ * The URL remains the single source of truth.
60
+ *
61
+ * @param {Object} options
62
+ * @param {Object.<string, Function>} [options.schema]
63
+ * Map of query param parsers.
64
+ * Each function receives the raw string value (or undefined)
65
+ * and must return a parsed value with a default fallback.
66
+ *
67
+ * @param {boolean} [options.replace=false]
68
+ * If true, uses history.replaceState instead of pushState.
69
+ *
70
+ * @param {boolean} [options.listen=true]
71
+ * If true, listens to popstate events to keep state in sync.
72
+ *
73
+ * @returns {Object}
74
+ */
75
+ export function useQueryParams(options = {}) {
76
+ const { schema = {}, replace = false, listen = true } = options;
77
+
78
+ /**
79
+ * Compute default values by executing schema parsers
80
+ * with an undefined input.
81
+ */
82
+ const defaults = {};
83
+ for (const key in schema) {
84
+ defaults[key] = schema[key](undefined);
85
+ }
86
+
87
+ /**
88
+ * Raw query params as strings.
89
+ * This mirrors exactly what exists in the URL.
90
+ */
91
+ let raw = parseRawQuery(window.location.search);
92
+
93
+ /**
94
+ * Parses raw query params using the provided schema.
95
+ *
96
+ * - Schema keys are always present (defaults applied)
97
+ * - Unknown params are passed through as strings
98
+ *
99
+ * @param {Object.<string, string>} raw
100
+ * @returns {Object} Parsed params ready for application use
101
+ */
102
+ function parseWithSchema(raw) {
103
+ const parsed = {};
104
+
105
+ // Apply schema parsing and defaults
106
+ for (const key in schema) {
107
+ const parser = schema[key];
108
+ parsed[key] = parser(raw[key]);
109
+ }
110
+
111
+ // Preserve non-declared query params
112
+ for (const key in raw) {
113
+ if (!(key in parsed)) {
114
+ parsed[key] = raw[key];
115
+ }
116
+ }
117
+
118
+ return parsed;
119
+ }
120
+
121
+ /**
122
+ * Serializes application-level values into
123
+ * raw URL-safe string values.
124
+ *
125
+ * - Arrays are joined by comma
126
+ * - null / undefined values are omitted
127
+ *
128
+ * @param {Object} next
129
+ * @returns {Object.<string, string>}
130
+ */
131
+ function serializeWithSchema(next) {
132
+ const out = {};
133
+
134
+ for (const key in next) {
135
+ const value = next[key];
136
+
137
+ if (Array.isArray(value)) {
138
+ out[key] = value.join(",");
139
+ } else if (value != null) {
140
+ out[key] = String(value);
141
+ }
142
+ }
143
+
144
+ return out;
145
+ }
146
+
147
+ /**
148
+ * Synchronizes the internal raw state with the browser URL.
149
+ *
150
+ * @param {Object.<string, string>} nextRaw
151
+ */
152
+ function sync(nextRaw) {
153
+ raw = nextRaw;
154
+
155
+ const qs = buildQueryString(raw);
156
+ const url =
157
+ window.location.pathname + (qs ? `?${qs}` : "") + window.location.hash;
158
+
159
+ history[replace ? "replaceState" : "pushState"](null, "", url);
160
+ }
161
+
162
+ /**
163
+ * Updates one or more query params.
164
+ *
165
+ * Values are serialized and merged with existing params.
166
+ *
167
+ * @param {Object} next
168
+ */
169
+ function set(next) {
170
+ const serialized = serializeWithSchema(next);
171
+ sync({ ...raw, ...serialized });
172
+ }
173
+
174
+ /**
175
+ * Removes one or more query params.
176
+ *
177
+ * @param {...string} keys
178
+ */
179
+ function remove(...keys) {
180
+ const next = { ...raw };
181
+ keys.forEach((k) => delete next[k]);
182
+ sync(next);
183
+ }
184
+
185
+ /**
186
+ * Removes all query params from the URL.
187
+ */
188
+ function reset() {
189
+ sync({});
190
+ }
191
+
192
+ /**
193
+ * Keeps internal state in sync with browser
194
+ * back/forward navigation.
195
+ */
196
+ if (listen) {
197
+ window.addEventListener("popstate", () => {
198
+ raw = parseRawQuery(window.location.search);
199
+ });
200
+ }
201
+
202
+ return {
203
+ /**
204
+ * Parsed query params.
205
+ *
206
+ * This is a getter, so values are always derived
207
+ * from the current raw URL state.
208
+ */
209
+ get params() {
210
+ return parseWithSchema(raw);
211
+ },
212
+
213
+ /**
214
+ * Raw query params as strings.
215
+ * Exposed mainly for debugging or tooling.
216
+ */
217
+ get raw() {
218
+ return { ...raw };
219
+ },
220
+
221
+ set,
222
+ remove,
223
+ reset,
224
+ };
225
+ }
@@ -0,0 +1,76 @@
1
+ import { reactive } from "../reactive.js";
2
+ import { routes } from "../_routes.js";
3
+
4
+ /**
5
+ * Reactive store holding the current route params.
6
+ * This object is updated whenever the URL changes.
7
+ */
8
+ const routeParams = reactive({});
9
+
10
+ /**
11
+ * Extracts dynamic parameters from a pathname based on route definitions.
12
+ *
13
+ * Supported syntax:
14
+ * /posts/:id
15
+ * /users/:userId/:postId
16
+ *
17
+ * @param {string} pathname - URL pathname (no query, no hash)
18
+ * @returns {Object} Extracted params
19
+ */
20
+ function extractParams(pathname) {
21
+ const pathParts = pathname.split("/").filter(Boolean);
22
+
23
+ for (const route of routes) {
24
+ const routeParts = route.path.split("/").filter(Boolean);
25
+ if (routeParts.length !== pathParts.length) continue;
26
+
27
+ const params = {};
28
+ let match = true;
29
+
30
+ for (let i = 0; i < routeParts.length; i++) {
31
+ const routePart = routeParts[i];
32
+ const pathPart = pathParts[i];
33
+
34
+ if (routePart.startsWith(":")) {
35
+ params[routePart.slice(1)] = pathPart;
36
+ } else if (routePart !== pathPart) {
37
+ match = false;
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (match) return params;
43
+ }
44
+
45
+ return {};
46
+ }
47
+
48
+ /**
49
+ * Updates the reactive route params based on the current URL.
50
+ * @param {string} [path=window.location.pathname] - URL pathname to extract params from
51
+ * @example
52
+ * updateRouteParams(); // Updates params from current URL
53
+ * updateRouteParams("/posts/42"); // Updates params from given path
54
+ */
55
+ export function updateRouteParams(path = window.location.pathname) {
56
+ const newParams = extractParams(path);
57
+
58
+ Object.keys(routeParams).forEach((k) => delete routeParams[k]);
59
+ Object.assign(routeParams, newParams);
60
+ }
61
+
62
+ /**
63
+ * Composition function returning reactive route params.
64
+ *
65
+ * @returns {Object} Reactive route params
66
+ *
67
+ * @example
68
+ * const params = useRouteParams();
69
+ *
70
+ * effect(() => {
71
+ * console.log(params.id);
72
+ * });
73
+ */
74
+ export function useRouteParams() {
75
+ return routeParams;
76
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Tracks the currently executing effect function for dependency collection.
3
+ * This global variable allows the reactive system to know which effect
4
+ * is currently running and should be notified of reactive property changes.
5
+ */
6
+ let activeEffect = null;
7
+
8
+ /**
9
+ * Adapts primitive values (string, number, boolean, null) to work with the reactive system.
10
+ * Wraps primitive values in an object with a 'value' property and marks them as primitive.
11
+ * Objects are returned as-is since they can already have reactive properties.
12
+ *
13
+ * @param {any} input - The value to adapt for reactivity
14
+ * @returns {object} Object with either the original object or wrapped primitive
15
+ *
16
+ * @example
17
+ * adaptPrimitiveValue(42) // → { value: 42, __isPrimitive: true }
18
+ * adaptPrimitiveValue("hello") // → { value: "hello", __isPrimitive: true }
19
+ * adaptPrimitiveValue({x: 1}) // → {x: 1} (unchanged)
20
+ */
21
+ function adaptPrimitiveValue(input) {
22
+ if (input === null || typeof input !== "object") {
23
+ return { value: input, __isPrimitive: true };
24
+ }
25
+ return input;
26
+ }
27
+
28
+ /**
29
+ * Creates a reactive proxy that automatically tracks dependencies and triggers effects.
30
+ * The core of the reactivity system - makes any object or primitive reactive.
31
+ *
32
+ * Features:
33
+ * - Automatic dependency tracking when properties are accessed during effects
34
+ * - Automatic effect triggering when properties change
35
+ * - Support for primitive values through value wrapping
36
+ * - Memory cleanup to prevent leaks
37
+ *
38
+ * @param {any} obj - Object or primitive to make reactive
39
+ * @returns {Proxy} Reactive proxy that tracks dependencies and triggers effects
40
+ *
41
+ * @example
42
+ * With objects
43
+ * const state = reactive({ count: 0, name: "John" });
44
+ * state.count++; // Triggers any effects that used state.count
45
+ *
46
+ * @example
47
+ * With primitives
48
+ * const counter = reactive(0);
49
+ * counter.value++; // Triggers effects that used counter.value
50
+ *
51
+ * @example
52
+ * In components
53
+ * class Counter {
54
+ * count = reactive(0);
55
+ *
56
+ * increment() {
57
+ * this.count.value++; // Automatically re-renders component
58
+ * }
59
+ *
60
+ * effect(() => this.render());
61
+ *
62
+ * render() {
63
+ * return html`<button @click="${this.increment}">Count: ${this.count.value}</button>`;
64
+ * }
65
+ * }
66
+ *
67
+ * @example
68
+ * In components with Component base class doesn't require manual effect()
69
+ * class Counter extends Component {
70
+ * count = reactive(0);
71
+ *
72
+ * increment() {
73
+ * this.count.value++; // Automatically re-renders component
74
+ * }
75
+ *
76
+ * render() {
77
+ * return html`<button @click="${this.increment}">Count: ${this.count.value}</button>`;
78
+ * }
79
+ * }
80
+ *
81
+ */
82
+ export function reactive(obj) {
83
+ obj = adaptPrimitiveValue(obj);
84
+
85
+ // Map to store dependencies for each property
86
+ const depsMap = new Map();
87
+
88
+ const proxy = new Proxy(obj, {
89
+ get(target, prop) {
90
+ // Handle primitive value conversion (for template literals, etc.)
91
+ if (target.__isPrimitive && prop === Symbol.toPrimitive) {
92
+ return () => target.value;
93
+ }
94
+
95
+ // Use 'value' key for primitives, actual property name for objects
96
+ const key = target.__isPrimitive ? "value" : prop;
97
+
98
+ // Dependency tracking: if an effect is running, register it as dependent on this property
99
+ if (activeEffect) {
100
+ if (!depsMap.has(key)) depsMap.set(key, new Set());
101
+ const depSet = depsMap.get(key);
102
+ depSet.add(activeEffect);
103
+
104
+ // Track dependencies on the effect for cleanup
105
+ if (!activeEffect.deps) activeEffect.deps = [];
106
+ activeEffect.deps.push(depSet);
107
+ }
108
+
109
+ return target[key];
110
+ },
111
+ set(target, prop, value) {
112
+ const key = target.__isPrimitive ? "value" : prop;
113
+ target[key] = value;
114
+
115
+ // Trigger all effects that depend on this property
116
+ if (depsMap.has(key)) {
117
+ depsMap.get(key).forEach((effect) => effect());
118
+ }
119
+
120
+ return true;
121
+ },
122
+ });
123
+
124
+ return proxy;
125
+ }
126
+
127
+ /**
128
+ * Creates a reactive effect that automatically re-runs when its dependencies change.
129
+ * This is the foundation of the reactivity system - it tracks which reactive properties
130
+ * are accessed during execution and re-runs the function when any of them change.
131
+ *
132
+ * @param {function} fn - Function to run reactively. Will re-execute when dependencies change
133
+ * @returns {function} Cleanup function to stop the effect and remove all dependencies
134
+ *
135
+ * @example
136
+ * Basic usage
137
+ * const count = reactive(0);
138
+ * const cleanup = effect(() => {
139
+ * console.log(`Count is: ${count.value}`); // Logs immediately and on changes
140
+ * });
141
+ *
142
+ * count.value++; // Logs: "Count is: 1"
143
+ * cleanup(); // Stops the effect
144
+ */
145
+ export function effect(fn) {
146
+ const wrapped = () => {
147
+ activeEffect = wrapped;
148
+ fn();
149
+ activeEffect = null;
150
+ };
151
+
152
+ // Run the effect immediately to collect initial dependencies
153
+ wrapped();
154
+
155
+ // Return cleanup function
156
+ return () => {
157
+ if (wrapped.deps) {
158
+ wrapped.deps.forEach((depSet) => depSet.delete(wrapped));
159
+ }
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Creates a computed reactive value that automatically updates when its dependencies change.
165
+ * @param {Function} getter
166
+ * @returns {{ value: Object }} Computed reactive value
167
+ *
168
+ * @example
169
+ * Basic usage
170
+ * const count = reactive(1);
171
+ * const doubleCount = computed(() => count.value * 2);
172
+ * console.log(doubleCount.value); // 2
173
+ * count.value = 3;
174
+ * console.log(doubleCount.value); // 6
175
+ */
176
+ export function computed(getter) {
177
+ let value;
178
+
179
+ effect(() => {
180
+ value = getter();
181
+ });
182
+
183
+ return {
184
+ get value() {
185
+ return value;
186
+ },
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Watches a reactive source and runs a callback when its value changes.
192
+ *
193
+ * @template T
194
+ * @param {() => T} source - A getter function returning the reactive value to watch.
195
+ * @param {(newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void} callback
196
+ * @param {{ immediate?: boolean }} [options]
197
+ */
198
+ export function watch(source, callback, options = {}) {
199
+ let oldValue;
200
+ let cleanupFn;
201
+
202
+ const onCleanup = (fn) => {
203
+ cleanupFn = fn;
204
+ };
205
+
206
+ const runner = () => {
207
+ const newValue = source();
208
+
209
+ // Skip first run if not immediate
210
+ if (oldValue === undefined && !options.immediate) {
211
+ oldValue = newValue;
212
+ return;
213
+ }
214
+
215
+ // Avoid unnecessary executions
216
+ if (Object.is(newValue, oldValue)) return;
217
+
218
+ // Cleanup previous effect
219
+ if (cleanupFn) {
220
+ cleanupFn();
221
+ cleanupFn = null;
222
+ }
223
+
224
+ callback(newValue, oldValue, onCleanup);
225
+ oldValue = newValue;
226
+ };
227
+
228
+ // Track dependencies reactively
229
+ effect(runner);
230
+ }
231
+
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@cfdez11/vex",
3
+ "version": "0.1.0",
4
+ "description": "A vanilla JavaScript meta-framework with file-based routing, SSR/CSR/SSG/ISR and Vue-like reactivity",
5
+ "type": "module",
6
+ "main": "./server/index.js",
7
+ "exports": {
8
+ ".": "./server/index.js"
9
+ },
10
+ "files": [
11
+ "server",
12
+ "client"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "keywords": ["framework", "ssr", "ssg", "reactive", "file-based-routing", "express", "vanilla-js"],
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "dom-serializer": "^2.0.0",
21
+ "express": "^5.2.1",
22
+ "htmlparser2": "^10.0.0"
23
+ }
24
+ }