valyrian.js 7.1.1 → 7.2.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.
@@ -0,0 +1,187 @@
1
+ import { update } from "valyrian.js";
2
+
3
+ /* eslint-disable no-use-before-define */
4
+ interface Cleanup {
5
+ (): void;
6
+ }
7
+
8
+ interface Subscription {
9
+ // eslint-disable-next-line no-unused-vars
10
+ (value: ProxySignal["value"]): void | Cleanup;
11
+ }
12
+
13
+ interface Subscriptions extends Map<Subscription, Cleanup> {}
14
+
15
+ interface Getter {
16
+ // eslint-disable-next-line no-unused-vars
17
+ (value: ProxySignal["value"]): any;
18
+ }
19
+
20
+ interface Getters {
21
+ [key: string | symbol]: Getter;
22
+ }
23
+
24
+ interface ProxySignal {
25
+ // Works as a getter of the value
26
+ (): ProxySignal["value"];
27
+ // Works as a subscription to the value
28
+ // eslint-disable-next-line no-unused-vars
29
+ (value: Subscription): ProxySignal;
30
+ // Works as a setter with a path and a handler
31
+ // eslint-disable-next-line no-unused-vars
32
+ (path: string, handler: (valueAtPathPosition: any) => any): ProxySignal["value"];
33
+ // Works as a setter with a path and a value
34
+ // eslint-disable-next-line no-unused-vars
35
+ (path: string, value: any): ProxySignal["value"];
36
+ // Works as a setter with a value
37
+ // eslint-disable-next-line no-unused-vars
38
+ (value: any): ProxySignal["value"];
39
+ // Gets the current value of the signal.
40
+ value: any;
41
+ // Cleanup function to be called to remove all subscriptions.
42
+ cleanup: () => void;
43
+ // Creates a getter on the signal.
44
+ // eslint-disable-next-line no-unused-vars
45
+ getter: (name: string, handler: Getter) => any;
46
+ // To access the getters on the signal.
47
+ [key: string | number | symbol]: any;
48
+ }
49
+
50
+ function makeUnsubscribe(
51
+ subscriptions: Subscriptions,
52
+ computed: ProxySignal,
53
+ handler: Subscription,
54
+ cleanup?: Cleanup
55
+ ) {
56
+ if (typeof cleanup === "function") {
57
+ computed.cleanup = cleanup;
58
+ }
59
+ computed.unsubscribe = () => {
60
+ subscriptions.delete(handler);
61
+ computed?.cleanup();
62
+ };
63
+ }
64
+
65
+ function createSubscription(signal: ProxySignal, subscriptions: Subscriptions, handler: Subscription) {
66
+ if (subscriptions.has(handler) === false) {
67
+ // eslint-disable-next-line no-use-before-define
68
+ let computed = ProxySignal(() => handler(signal.value));
69
+ let cleanup = computed(); // Execute to register itself
70
+ makeUnsubscribe(subscriptions, computed, handler, cleanup);
71
+ subscriptions.set(handler, computed);
72
+ }
73
+
74
+ return subscriptions.get(handler);
75
+ }
76
+
77
+ let updateTimeout: any;
78
+ function delayedUpdate() {
79
+ clearTimeout(updateTimeout);
80
+ updateTimeout = setTimeout(update);
81
+ }
82
+
83
+ // eslint-disable-next-line sonarjs/cognitive-complexity
84
+ export function ProxySignal(value: any): ProxySignal {
85
+ let subscriptions = new Map();
86
+ let getters: Getters = {};
87
+
88
+ let forceUpdate = false;
89
+
90
+ let signal: ProxySignal = new Proxy(
91
+ // eslint-disable-next-line no-unused-vars
92
+ function (valOrPath?: any | Subscription, handler?: (valueAtPathPosition: any) => any) {
93
+ // Works as a getter
94
+ if (typeof valOrPath === "undefined") {
95
+ return signal.value;
96
+ }
97
+
98
+ // Works as a subscription
99
+ if (typeof valOrPath === "function") {
100
+ return createSubscription(signal, subscriptions, valOrPath);
101
+ }
102
+
103
+ // Works as a setter with a path
104
+ if (typeof valOrPath === "string" && typeof handler !== "undefined") {
105
+ let parsed = valOrPath.split(".");
106
+ let result = signal.value;
107
+ let next;
108
+ while (parsed.length) {
109
+ next = parsed.shift() as string;
110
+ if (parsed.length > 0) {
111
+ if (typeof result[next] !== "object") {
112
+ result[next] = {};
113
+ }
114
+ result = result[next];
115
+ } else {
116
+ result[next] = typeof handler === "function" ? handler(result[next]) : handler;
117
+ }
118
+ }
119
+ forceUpdate = true;
120
+ signal.value = signal.value;
121
+ return signal.value;
122
+ }
123
+
124
+ // Works as a setter with a value
125
+ signal.value = valOrPath;
126
+ return signal.value;
127
+ } as ProxySignal,
128
+ {
129
+ set(state, prop, val) {
130
+ if (prop === "value" || prop === "unsubscribe" || prop === "cleanup") {
131
+ let old = state[prop];
132
+ state[prop] = val;
133
+ if (prop === "value" && (forceUpdate || val !== old)) {
134
+ forceUpdate = false;
135
+ for (let [handler, computed] of subscriptions) {
136
+ computed.cleanup();
137
+ let cleanup = handler(val);
138
+ makeUnsubscribe(subscriptions, computed, handler, cleanup);
139
+ }
140
+ delayedUpdate();
141
+ }
142
+ return true;
143
+ }
144
+ return false;
145
+ },
146
+ get(state, prop) {
147
+ if (prop === "value") {
148
+ return typeof state.value === "function" ? state.value() : state.value;
149
+ }
150
+
151
+ if (prop === "cleanup" || prop === "unsubscribe" || prop === "getter") {
152
+ return state[prop];
153
+ }
154
+
155
+ if (prop in getters) {
156
+ return getters[prop](state.value);
157
+ }
158
+ }
159
+ }
160
+ );
161
+
162
+ Object.defineProperties(signal, {
163
+ value: { value, writable: true, enumerable: true },
164
+ cleanup: {
165
+ value() {
166
+ // eslint-disable-next-line no-unused-vars
167
+ for (let [handler, computed] of subscriptions) {
168
+ computed.unsubscribe();
169
+ }
170
+ },
171
+ writable: true,
172
+ enumerable: true
173
+ },
174
+ getter: {
175
+ value(name: string, handler: Getter) {
176
+ if (name in getters) {
177
+ throw new Error("Named computed already exists.");
178
+ }
179
+
180
+ getters[name] = handler;
181
+ },
182
+ enumerable: true
183
+ }
184
+ });
185
+
186
+ return signal;
187
+ }
@@ -52,10 +52,11 @@ interface RouterInterface {
52
52
  path: string;
53
53
  params: Record<string, string | number | any>;
54
54
  matches: string[];
55
+ pathPrefix: string;
55
56
  // eslint-disable-next-line no-unused-vars
56
57
  add(method: string, ...args: Middlewares): Router;
57
58
  // eslint-disable-next-line no-unused-vars
58
- use(...args: string[] | Middlewares | Router[]): Router;
59
+ use(...args: (string | Middleware | Router)[]): Router;
59
60
 
60
61
  routes(): string[];
61
62
  // eslint-disable-next-line no-unused-vars
@@ -71,6 +72,18 @@ function flat(array: any) {
71
72
  return Array.isArray(array) ? array.flat(Infinity) : [array];
72
73
  }
73
74
 
75
+ function getPathWithoutPrefix(path: string, prefix: string) {
76
+ return getPathWithoutLastSlash(path.replace(new RegExp(`^${prefix}`), ""));
77
+ }
78
+
79
+ function getPathWithoutLastSlash(path: string) {
80
+ let pathWithoutLastSlash = path.replace(/\/$/, "");
81
+ if (pathWithoutLastSlash === "") {
82
+ pathWithoutLastSlash = "/";
83
+ }
84
+ return pathWithoutLastSlash;
85
+ }
86
+
74
87
  const addPath = ({
75
88
  router,
76
89
  method,
@@ -145,7 +158,7 @@ function searchMiddlewares(router: RouterInterface, path: string): Middlewares {
145
158
  matches.push(...match);
146
159
 
147
160
  if (item.method === "add") {
148
- router.path = item.path;
161
+ router.path = getPathWithoutPrefix(item.path, router.pathPrefix);
149
162
  break;
150
163
  }
151
164
  }
@@ -211,12 +224,15 @@ export class Router implements RouterInterface {
211
224
  }
212
225
 
213
226
  add(path: string, ...middlewares: Middlewares): Router {
214
- addPath({ router: this, method: "add", path: `${this.pathPrefix}${path}`, middlewares });
227
+ let pathWithoutLastSlash = getPathWithoutLastSlash(`${this.pathPrefix}${path}`);
228
+ addPath({ router: this, method: "add", path: pathWithoutLastSlash, middlewares });
215
229
  return this;
216
230
  }
217
231
 
218
232
  use(...middlewares: Middlewares | Router[] | string[]): Router {
219
- let path = `${this.pathPrefix}${typeof middlewares[0] === "string" ? middlewares.shift() : "/"}`;
233
+ let path = getPathWithoutLastSlash(
234
+ `${this.pathPrefix}${typeof middlewares[0] === "string" ? middlewares.shift() : "/"}`
235
+ );
220
236
 
221
237
  for (const item of middlewares) {
222
238
  if (item instanceof Router) {
@@ -249,7 +265,7 @@ export class Router implements RouterInterface {
249
265
  throw new Error("router.url.required");
250
266
  }
251
267
 
252
- const constructedPath = `${this.pathPrefix}${path}`;
268
+ let constructedPath = getPathWithoutLastSlash(`${this.pathPrefix}${path}`);
253
269
  const parts = constructedPath.split("?", 2);
254
270
  this.url = constructedPath;
255
271
  this.query = parseQuery(parts[1]);
@@ -309,7 +325,7 @@ export function mountRouter(elementContainer: string | any, router: Router): voi
309
325
 
310
326
  if (!isNodeJs) {
311
327
  function onPopStateGoToRoute(): void {
312
- let pathWithoutPrefix = document.location.pathname.replace(router.pathPrefix, "");
328
+ let pathWithoutPrefix = getPathWithoutPrefix(document.location.pathname, router.pathPrefix);
313
329
  (router as unknown as Router).go(pathWithoutPrefix, undefined, true);
314
330
  }
315
331
  window.addEventListener("popstate", onPopStateGoToRoute, false);
@@ -1,182 +1,102 @@
1
- import { update } from "valyrian.js";
2
-
3
- /* eslint-disable no-use-before-define */
4
- interface Cleanup {
5
- (): void;
6
- }
7
-
8
- interface Subscription {
9
- // eslint-disable-next-line no-unused-vars
10
- (value: Signal["value"]): void | Cleanup;
11
- }
1
+ import { VnodeWithDom, current, update, updateVnode, v } from "valyrian.js";
2
+
3
+ export function Signal(initialValue) {
4
+ // Create a copy of the current context object
5
+ const context = { ...current };
6
+
7
+ // Check if the context object has a vnode property
8
+ if (context.vnode) {
9
+ // Is first call
10
+ if (!context.vnode.signals) {
11
+ // Set the signals property to the signals property of the oldVnode object, or an empty array if that doesn't exist
12
+ context.vnode.signals = context.oldVnode?.signals || [];
13
+ // Set the calls property to -1
14
+ context.vnode.calls = -1;
15
+ // Set the subscribers property to the subscribers property of the oldVnode object, or an empty array if that doesn't exist
16
+ context.vnode.subscribers = context.oldVnode?.subscribers || [];
17
+
18
+ // Set the initialChildren property of the vnode object to a copy of the children array of the vnode object
19
+ context.vnode.initialChildren = [...context.vnode.children];
20
+ }
12
21
 
13
- interface Subscriptions extends Map<Subscription, Cleanup> {}
22
+ // Assign the signal variable to the signal stored at the index of the vnode object's calls property in the vnode's signals array
23
+ let signal = context.vnode.signals[++context.vnode.calls];
14
24
 
15
- interface Getter {
16
- // eslint-disable-next-line no-unused-vars
17
- (value: Signal["value"]): any;
18
- }
25
+ // If a signal has already been assigned to the signal variable, return it
26
+ if (signal) {
27
+ return signal;
28
+ }
29
+ }
19
30
 
20
- interface Getters {
21
- [key: string | symbol]: Getter;
22
- }
31
+ // Declare a variable to store the current value of the Signal
32
+ let value = initialValue;
23
33
 
24
- interface Signal {
25
- // Works as a getter of the value
26
- (): Signal["value"];
27
- // Works as a subscription to the value
28
- // eslint-disable-next-line no-unused-vars
29
- (value: Subscription): Signal;
30
- // Works as a setter with a path and a handler
31
- // eslint-disable-next-line no-unused-vars
32
- (path: string, handler: (valueAtPathPosition: any) => any): Signal["value"];
33
- // Works as a setter with a path and a value
34
- // eslint-disable-next-line no-unused-vars
35
- (path: string, value: any): Signal["value"];
36
- // Works as a setter with a value
37
- // eslint-disable-next-line no-unused-vars
38
- (value: any): Signal["value"];
39
- // Gets the current value of the signal.
40
- value: any;
41
- // Cleanup function to be called to remove all subscriptions.
42
- cleanup: () => void;
43
- // Creates a getter on the signal.
44
- // eslint-disable-next-line no-unused-vars
45
- getter: (name: string, handler: Getter) => any;
46
- // To access the getters on the signal.
47
- [key: string | number | symbol]: any;
48
- }
34
+ // Create an array to store functions that have subscribed to changes to the Signal's value
35
+ const subscribers = [];
49
36
 
50
- function makeUnsubscribe(subscriptions: Subscriptions, computed: Signal, handler: Subscription, cleanup?: Cleanup) {
51
- if (typeof cleanup === "function") {
52
- computed.cleanup = cleanup;
53
- }
54
- computed.unsubscribe = () => {
55
- subscriptions.delete(handler);
56
- computed?.cleanup();
37
+ // Define a function that allows other parts of the code to subscribe to changes to the Signal's value
38
+ const subscribe = (callback) => {
39
+ // Add the callback function to the subscribers array
40
+ if (subscribers.indexOf(callback) === -1) {
41
+ subscribers.push(callback);
42
+ }
57
43
  };
58
- }
59
44
 
60
- function createSubscription(signal: Signal, subscriptions: Subscriptions, handler: Subscription) {
61
- if (subscriptions.has(handler) === false) {
62
- // eslint-disable-next-line no-use-before-define
63
- let computed = Signal(() => handler(signal.value));
64
- let cleanup = computed(); // Execute to register itself
65
- makeUnsubscribe(subscriptions, computed, handler, cleanup);
66
- subscriptions.set(handler, computed);
45
+ // Define a function that returns the current value of the Signal
46
+ function get() {
47
+ return value;
67
48
  }
49
+ // Add value, toJSON, valueOf, and toString properties to the get function
50
+ get.value = value;
51
+ get.toJSON = get.valueOf = get;
52
+ get.toString = () => `${value}`;
53
+
54
+ // Define a function that allows the value of the Signal to be updated and notifies any subscribed functions of the change
55
+ const set = (newValue) => {
56
+ // Update the value of the Signal
57
+ value = newValue;
58
+ // Update the value property of the get function
59
+ get.value = value;
60
+ // Call each subscribed function with the new value of the Signal as an argument
61
+ for (let i = 0, l = subscribers.length; i < l; i++) {
62
+ subscribers[i](value);
63
+ }
68
64
 
69
- return subscriptions.get(handler);
70
- }
65
+ // Check if the context object has a vnode property
66
+ if (context.vnode) {
67
+ // If it does, create a new vnode object based on the original vnode, its children, and its DOM and SVG properties
68
+ let newVnode = v(context.vnode.tag, context.vnode.props, ...context.vnode.initialChildren) as VnodeWithDom;
69
+ newVnode.dom = context.vnode.dom;
70
+ newVnode.isSVG = context.vnode.isSVG;
71
+
72
+ // Clear the subscribers array by setting the length property to 0
73
+ context.vnode.subscribers.forEach(
74
+ (subscribers) =>
75
+ // Setting the length property to 0 is faster than clearing the array with a loop
76
+ (subscribers.length = 0)
77
+ );
78
+
79
+ // Clear the subscribers array by setting it to an empty array
80
+ context.vnode.subscribers = [];
81
+
82
+ // Return the result of updating the original vnode with the new vnode
83
+ return updateVnode(newVnode, context.vnode);
84
+ }
71
85
 
72
- let updateTimeout: any;
73
- function delayedUpdate() {
74
- clearTimeout(updateTimeout);
75
- updateTimeout = setTimeout(update);
76
- }
86
+ // If the context object doesn't have a vnode property, return the result of calling the update function
87
+ return update();
88
+ };
77
89
 
78
- // eslint-disable-next-line sonarjs/cognitive-complexity
79
- export function Signal(value: any): Signal {
80
- let subscriptions = new Map();
81
- let getters: Getters = {};
82
-
83
- let forceUpdate = false;
84
-
85
- let signal: Signal = new Proxy(
86
- // eslint-disable-next-line no-unused-vars
87
- function (valOrPath?: any | Subscription, handler?: (valueAtPathPosition: any) => any) {
88
- // Works as a getter
89
- if (typeof valOrPath === "undefined") {
90
- return signal.value;
91
- }
92
-
93
- // Works as a subscription
94
- if (typeof valOrPath === "function") {
95
- return createSubscription(signal, subscriptions, valOrPath);
96
- }
97
-
98
- // Works as a setter with a path
99
- if (typeof valOrPath === "string" && typeof handler !== "undefined") {
100
- let parsed = valOrPath.split(".");
101
- let result = signal.value;
102
- let next;
103
- while (parsed.length) {
104
- next = parsed.shift() as string;
105
- if (parsed.length > 0) {
106
- if (typeof result[next] !== "object") {
107
- result[next] = {};
108
- }
109
- result = result[next];
110
- } else {
111
- result[next] = typeof handler === "function" ? handler(result[next]) : handler;
112
- }
113
- }
114
- forceUpdate = true;
115
- signal.value = signal.value;
116
- return signal.value;
117
- }
118
-
119
- // Works as a setter with a value
120
- signal.value = valOrPath;
121
- return signal.value;
122
- } as Signal,
123
- {
124
- set(state, prop, val) {
125
- if (prop === "value" || prop === "unsubscribe" || prop === "cleanup") {
126
- let old = state[prop];
127
- state[prop] = val;
128
- if (prop === "value" && (forceUpdate || val !== old)) {
129
- forceUpdate = false;
130
- for (let [handler, computed] of subscriptions) {
131
- computed.cleanup();
132
- let cleanup = handler(val);
133
- makeUnsubscribe(subscriptions, computed, handler, cleanup);
134
- }
135
- delayedUpdate();
136
- }
137
- return true;
138
- }
139
- return false;
140
- },
141
- get(state, prop) {
142
- if (prop === "value") {
143
- return typeof state.value === "function" ? state.value() : state.value;
144
- }
145
-
146
- if (prop === "cleanup" || prop === "unsubscribe" || prop === "getter") {
147
- return state[prop];
148
- }
149
-
150
- if (prop in getters) {
151
- return getters[prop](state.value);
152
- }
153
- }
154
- }
155
- );
156
-
157
- Object.defineProperties(signal, {
158
- value: { value, writable: true, enumerable: true },
159
- cleanup: {
160
- value() {
161
- // eslint-disable-next-line no-unused-vars
162
- for (let [handler, computed] of subscriptions) {
163
- computed.unsubscribe();
164
- }
165
- },
166
- writable: true,
167
- enumerable: true
168
- },
169
- getter: {
170
- value(name: string, handler: Getter) {
171
- if (name in getters) {
172
- throw new Error("Named computed already exists.");
173
- }
174
-
175
- getters[name] = handler;
176
- },
177
- enumerable: true
178
- }
179
- });
90
+ // Assign the signal variable an array containing the get, set, and subscribe functions
91
+ let signal = [get, set, subscribe];
92
+
93
+ // If the context object has a vnode property, add the signal to the vnode's signals array
94
+ // and add the subscribers array to the vnode's subscribers array
95
+ if (context.vnode) {
96
+ context.vnode.signals.push(signal);
97
+ context.vnode.subscribers.push(subscribers);
98
+ }
180
99
 
100
+ // Return the signal
181
101
  return signal;
182
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valyrian.js",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "description": "Lightweight steel to forge PWAs. (Minimal Frontend Framework with server side rendering and other capabilities)",
5
5
  "repository": "git@github.com:Masquerade-Circus/valyrian.js.git",
6
6
  "author": "Masquerade <christian@masquerade-circus.net>",
@@ -83,7 +83,7 @@
83
83
  "typescript": "^4.8.3"
84
84
  },
85
85
  "devDependencies": {
86
- "@release-it/conventional-changelog": "^5.1.0",
86
+ "@release-it/conventional-changelog": "^5.1.1",
87
87
  "@types/clean-css": "^4.2.5",
88
88
  "@types/node": "^18.7.16",
89
89
  "@types/node-fetch": "^2.6.2",
@@ -91,7 +91,6 @@
91
91
  "@types/source-map": "^0.5.7",
92
92
  "@typescript-eslint/eslint-plugin": "^5.36.2",
93
93
  "@typescript-eslint/parser": "^5.36.2",
94
- "browser-sync": "^2.27.10",
95
94
  "buffalo-test": "^2.0.0",
96
95
  "compression": "^1.7.4",
97
96
  "cross-env": "^7.0.3",
@@ -100,7 +99,7 @@
100
99
  "eslint": "^8.23.1",
101
100
  "eslint-plugin-sonarjs": "^0.15.0",
102
101
  "expect": "^29.0.3",
103
- "fastify": "^4.5.3",
102
+ "fastify": "^4.10.2",
104
103
  "gzip-size": "^7.0.0",
105
104
  "mocha": "^10.0.0",
106
105
  "nodemon": "^2.0.19",