@radishland/runtime 0.0.8 → 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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/client/boot.js ADDED
@@ -0,0 +1,150 @@
1
+ // src/config.ts
2
+ var bindingConfig = {
3
+ "checked": {
4
+ element: ["input"],
5
+ type: ["boolean"],
6
+ event: "change"
7
+ },
8
+ "value": {
9
+ element: ["input", "select", "textarea"],
10
+ type: ["string", "number"],
11
+ event: "input"
12
+ }
13
+ };
14
+
15
+ // src/utils.ts
16
+ var spaces_sep_by_comma = /\s*,\s*/;
17
+
18
+ // src/boot.ts
19
+ var bindingsQueryString = Object.keys(bindingConfig).map(
20
+ (property) => `[\\@bind\\:${property}]`
21
+ ).join(",");
22
+ setTimeout(() => {
23
+ customElements?.whenDefined("handler-registry").then(() => {
24
+ document.querySelectorAll(
25
+ `[\\@on],[\\@use],[\\@attr],[\\@attr\\|client],[\\@prop],${bindingsQueryString},[\\@text],[\\@html]`
26
+ ).forEach(
27
+ (entry) => {
28
+ const events = entry.getAttribute("@on")?.trim()?.split(spaces_sep_by_comma);
29
+ if (events) {
30
+ for (const event of events) {
31
+ const [type, handler] = event.split(":");
32
+ const onRequest = new CustomEvent("@on-request", {
33
+ bubbles: true,
34
+ cancelable: true,
35
+ composed: true,
36
+ detail: {
37
+ type,
38
+ handler: handler || type
39
+ }
40
+ });
41
+ entry.dispatchEvent(onRequest);
42
+ }
43
+ }
44
+ const hooks = entry.getAttribute("@use")?.trim()?.split(spaces_sep_by_comma);
45
+ if (hooks) {
46
+ for (const hook of hooks) {
47
+ const useRequest = new CustomEvent("@use-request", {
48
+ bubbles: true,
49
+ cancelable: true,
50
+ composed: true,
51
+ detail: {
52
+ hook
53
+ }
54
+ });
55
+ entry.dispatchEvent(useRequest);
56
+ }
57
+ }
58
+ const props = entry.getAttribute("@prop")?.trim().split(spaces_sep_by_comma);
59
+ if (props) {
60
+ for (const prop of props) {
61
+ const [key, value] = prop.split(":");
62
+ const propRequest = new CustomEvent("@prop-request", {
63
+ bubbles: true,
64
+ cancelable: true,
65
+ composed: true,
66
+ detail: {
67
+ property: key,
68
+ identifier: value || key
69
+ }
70
+ });
71
+ entry.dispatchEvent(propRequest);
72
+ }
73
+ }
74
+ const text = entry.hasAttribute("@text");
75
+ if (text) {
76
+ const identifier = entry.getAttribute("@text") || "text";
77
+ const textRequest = new CustomEvent("@text-request", {
78
+ bubbles: true,
79
+ cancelable: true,
80
+ composed: true,
81
+ detail: {
82
+ identifier
83
+ }
84
+ });
85
+ entry.dispatchEvent(textRequest);
86
+ }
87
+ const html = entry.hasAttribute("@html");
88
+ if (html) {
89
+ const identifier = entry.getAttribute("@html") || "html";
90
+ const htmlRequest = new CustomEvent("@html-request", {
91
+ bubbles: true,
92
+ cancelable: true,
93
+ composed: true,
94
+ detail: {
95
+ identifier
96
+ }
97
+ });
98
+ entry.dispatchEvent(htmlRequest);
99
+ }
100
+ const classList = entry.hasAttribute("@class");
101
+ if (classList) {
102
+ const identifier = entry.getAttribute("@class") || "class";
103
+ const classRequest = new CustomEvent("@class-request", {
104
+ bubbles: true,
105
+ cancelable: true,
106
+ composed: true,
107
+ detail: {
108
+ identifier
109
+ }
110
+ });
111
+ entry.dispatchEvent(classRequest);
112
+ }
113
+ const attributes = [
114
+ ...entry.getAttribute("@attr")?.trim().split(spaces_sep_by_comma) ?? [],
115
+ ...entry.getAttribute("@attr|client")?.trim().split(spaces_sep_by_comma) ?? []
116
+ ];
117
+ if (attributes.length > 0) {
118
+ for (const attribute of attributes) {
119
+ const [key, value] = attribute.split(":");
120
+ const attrRequest = new CustomEvent("@attr-request", {
121
+ bubbles: true,
122
+ cancelable: true,
123
+ composed: true,
124
+ detail: {
125
+ attribute: key,
126
+ identifier: value || key
127
+ }
128
+ });
129
+ entry.dispatchEvent(attrRequest);
130
+ }
131
+ }
132
+ for (const property of Object.keys(bindingConfig)) {
133
+ if (entry.hasAttribute(`@bind:${property}`)) {
134
+ const identifier = entry.getAttribute(`@bind:${property}`)?.trim() || property;
135
+ const bindRequest = new CustomEvent("@bind-request", {
136
+ bubbles: true,
137
+ cancelable: true,
138
+ composed: true,
139
+ detail: {
140
+ property,
141
+ identifier
142
+ }
143
+ });
144
+ entry.dispatchEvent(bindRequest);
145
+ }
146
+ }
147
+ }
148
+ );
149
+ });
150
+ }, 100);
package/client/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Signal } from 'signal-polyfill';
1
+ import { Signal, ReadonlySignal } from '@preact/signals-core';
2
2
 
3
3
  interface AutonomousCustomElement {
4
4
  /**
@@ -30,7 +30,6 @@ interface AutonomousCustomElement {
30
30
  formStateRestoreCallback?(): void;
31
31
  }
32
32
 
33
- type ReactivityOptions = { deep: boolean };
34
33
  type Destructor = () => void;
35
34
  type EffectCallback = () => Destructor | void;
36
35
  type EffectOptions = {
@@ -57,33 +56,35 @@ declare class HandlerRegistry extends HTMLElement implements AutonomousCustomEle
57
56
  *
58
57
  * An optional AbortSignal can be provided to abort the effect prematurely
59
58
  */
60
- $effect(callback: EffectCallback, options?: EffectOptions): void;
59
+ effect(callback: EffectCallback): void;
60
+ /**
61
+ * Looks up an identifier on the instance
62
+ */
63
+ lookup(identifier: string): any;
61
64
  connectedCallback(): void;
62
65
  disconnectedCallback(): void;
63
66
  }
64
67
 
65
- declare class ReactiveValue<T> extends Signal.State<T> {
66
- private get;
67
- private set;
68
- get value(): T;
69
- set value(newValue: T);
70
- }
71
- declare class ReactiveComputation<T> extends Signal.Computed<T> {
72
- private get;
73
- get value(): T;
74
- }
75
- declare const $object: <T extends Record<PropertyKey, any>>(init: T, options?: ReactivityOptions) => T;
76
- declare const $array: <T extends ArrayLike<any>>(init: T, options?: ReactivityOptions) => T;
77
- declare const isState: (s: unknown) => s is InstanceType<typeof ReactiveValue>;
78
- declare const isComputed: (s: unknown) => s is InstanceType<typeof ReactiveComputation>;
79
- declare const getValue: (signal: unknown) => unknown;
80
- declare const $state: <T>(initialValue: T, options?: Signal.Options<T | undefined>) => ReactiveValue<T>;
81
- declare const $computed: <T>(computation: () => T, options?: Signal.Options<T>) => ReactiveComputation<T>;
68
+ declare const isState: (value: unknown) => value is InstanceType<typeof Signal>;
69
+ declare const signal: <T>(value: T) => Signal<T>;
70
+ declare const computed: <T>(computation: () => T) => ReadonlySignal<T>;
82
71
  /**
83
72
  * Create an unowned effect that must be cleanup up manually
84
73
  *
85
74
  * Accept an AbortSignal to abort the effect
86
75
  */
87
- declare const $effect: (cb: EffectCallback, options?: EffectOptions) => Destructor;
76
+ declare const effect: (cb: EffectCallback, options?: EffectOptions) => Destructor;
77
+ /**
78
+ * Creates a deeply reactive proxied object or array
79
+ *
80
+ * @example const obj = reactive({ a: { b: { c: 1 } } });
81
+
82
+ const computation = computed(() => obj.a.b.c * 2});
83
+ assertEquals(computation.value, 2);
84
+
85
+ obj.a.b.c = 2;
86
+ assertEquals(computation.value, 4);
87
+ */
88
+ declare const reactive: <T>(thing: T) => T;
88
89
 
89
- export { $array, $computed, $effect, $object, $state, HandlerRegistry, ReactiveComputation, ReactiveValue, getValue, isComputed, isState };
90
+ export { HandlerRegistry, computed, effect, isState, reactive, signal };
package/client/index.js CHANGED
@@ -1,6 +1,32 @@
1
- import { Signal } from 'signal-polyfill';
1
+ import { effect as effect$1, Signal, signal as signal$1, computed as computed$1 } from '@preact/signals-core';
2
2
 
3
3
  // src/utils.ts
4
+ function type(value) {
5
+ if (value === null) {
6
+ return "null";
7
+ }
8
+ if (value === void 0) {
9
+ return "undefined";
10
+ }
11
+ const baseType = typeof value;
12
+ if (!["object", "function"].includes(baseType)) {
13
+ return baseType;
14
+ }
15
+ if (typeof value === "object" && Symbol.toStringTag in value) {
16
+ const tag = value[Symbol.toStringTag];
17
+ if (typeof tag === "string") {
18
+ return tag;
19
+ }
20
+ }
21
+ if (baseType === "function" && Function.prototype.toString.call(value).startsWith("class")) {
22
+ return "class";
23
+ }
24
+ const className = value.constructor.name;
25
+ if (typeof className === "string" && className !== "") {
26
+ return className.toLowerCase();
27
+ }
28
+ return baseType;
29
+ }
4
30
  var booleanAttributes = [
5
31
  "allowfullscreen",
6
32
  // on <iframe>
@@ -65,126 +91,47 @@ var bindingConfig = {
65
91
  event: "input"
66
92
  }
67
93
  };
68
- var ReactiveValue = class extends Signal.State {
69
- // @ts-ignore see above
70
- get;
71
- // @ts-ignore see above
72
- set;
73
- get value() {
74
- return super.get();
75
- }
76
- set value(newValue) {
77
- super.set(newValue);
78
- }
94
+ var isState = (value) => {
95
+ return value instanceof Signal;
79
96
  };
80
- var ReactiveComputation = class extends Signal.Computed {
81
- // @ts-ignore see above
82
- get;
83
- get value() {
84
- return super.get();
85
- }
97
+ var signal = (value) => {
98
+ return signal$1(value);
86
99
  };
87
- var maybeReactiveObjectType = (thing, options) => {
88
- if (typeof thing === "object") {
89
- if (Array.isArray(thing)) {
90
- return $array(thing, options);
91
- } else if (thing) {
92
- return $object(thing, options);
93
- }
94
- }
95
- return thing;
100
+ var computed = (computation) => {
101
+ return computed$1(computation);
96
102
  };
97
- var $object = (init, options = { deep: false }) => {
98
- if (options.deep === true) {
99
- for (const [key, value] of Object.entries(init)) {
100
- init[key] = maybeReactiveObjectType(value, options);
101
- }
103
+ var effect = (cb, options) => {
104
+ if (options?.signal.aborted) return () => {
105
+ };
106
+ const dispose = effect$1(cb);
107
+ options?.signal.addEventListener("abort", dispose);
108
+ return dispose;
109
+ };
110
+ var reactive = (thing) => {
111
+ if (type(thing) === "object" || type(thing) === "array") {
112
+ return object(thing);
102
113
  }
103
- const state = new Signal.State(init);
104
- const proxy = new Proxy(init, {
105
- get(_target, p, _receiver) {
106
- return state.get()[p];
107
- },
108
- set(_target, p, newValue, _receiver) {
109
- state.set({
110
- ...state.get(),
111
- [p]: maybeReactiveObjectType(newValue, options)
112
- });
113
- return true;
114
- }
115
- });
116
- return proxy;
114
+ return thing;
117
115
  };
118
- var $array = (init, options = { deep: false }) => {
119
- if (options.deep) {
120
- for (const [key, value] of Object.entries(init)) {
121
- init[key] = maybeReactiveObjectType(value, options);
122
- }
116
+ var object = (init) => {
117
+ for (const [key, value] of Object.entries(init)) {
118
+ init[key] = reactive(value);
123
119
  }
124
- const state = new Signal.State(init);
120
+ const state = signal(init);
125
121
  const proxy = new Proxy(init, {
126
122
  get(_target, p, _receiver) {
127
- return state.get()[p];
123
+ return state.value[p];
128
124
  },
129
125
  set(_target, p, newValue, _receiver) {
130
- state.set({
131
- ...state.get(),
132
- [p]: maybeReactiveObjectType(newValue, options)
133
- });
126
+ state.value = {
127
+ ...state.value,
128
+ [p]: reactive(newValue)
129
+ };
134
130
  return true;
135
131
  }
136
132
  });
137
133
  return proxy;
138
134
  };
139
- var isState = (s) => {
140
- return Signal.isState(s);
141
- };
142
- var isComputed = (s) => {
143
- return Signal.isComputed(s);
144
- };
145
- var getValue = (signal) => {
146
- if (isState(signal) || isComputed(signal)) {
147
- return signal.value;
148
- }
149
- return signal;
150
- };
151
- var $state = (initialValue, options) => {
152
- return new ReactiveValue(initialValue, options);
153
- };
154
- var $computed = (computation, options) => {
155
- return new ReactiveComputation(computation, options);
156
- };
157
- var pending = false;
158
- var watcher = new Signal.subtle.Watcher(() => {
159
- if (!pending) {
160
- pending = true;
161
- queueMicrotask(() => {
162
- pending = false;
163
- for (const s of watcher.getPending()) s.get();
164
- watcher.watch();
165
- });
166
- }
167
- });
168
- var $effect = (cb, options) => {
169
- if (options?.signal?.aborted) return () => {
170
- };
171
- let destroy;
172
- const c = new Signal.Computed(() => {
173
- destroy?.();
174
- destroy = cb() ?? void 0;
175
- });
176
- watcher.watch(c);
177
- c.get();
178
- let cleaned = false;
179
- const cleanup = () => {
180
- if (cleaned) return;
181
- destroy?.();
182
- watcher.unwatch(c);
183
- cleaned = true;
184
- };
185
- options?.signal.addEventListener("abort", cleanup);
186
- return cleanup;
187
- };
188
135
 
189
136
  // src/handler-registry.ts
190
137
  var HandlerRegistry = class extends HTMLElement {
@@ -204,19 +151,20 @@ var HandlerRegistry = class extends HTMLElement {
204
151
  *
205
152
  * An optional AbortSignal can be provided to abort the effect prematurely
206
153
  */
207
- $effect(callback, options) {
208
- const signals = [this.abortController.signal];
209
- if (options?.signal) signals.push(options.signal);
210
- $effect(callback, { ...options, signal: AbortSignal.any(signals) });
154
+ effect(callback) {
155
+ effect(callback, { signal: this.abortController.signal });
211
156
  }
212
- #get(identifier) {
157
+ /**
158
+ * Looks up an identifier on the instance
159
+ */
160
+ lookup(identifier) {
213
161
  return this[identifier];
214
162
  }
215
163
  #handleOn(e) {
216
164
  if (e instanceof CustomEvent) {
217
- const { handler, type } = e.detail;
218
- if (handler in this && typeof this.#get(handler) === "function") {
219
- e.target?.addEventListener(type, this.#get(handler).bind(this));
165
+ const { handler, type: type2 } = e.detail;
166
+ if (handler in this && typeof this.lookup(handler) === "function") {
167
+ e.target?.addEventListener(type2, this.lookup(handler).bind(this));
220
168
  e.stopPropagation();
221
169
  }
222
170
  }
@@ -226,11 +174,11 @@ var HandlerRegistry = class extends HTMLElement {
226
174
  if (e instanceof CustomEvent && target) {
227
175
  const { identifier } = e.detail;
228
176
  if (identifier in this) {
229
- this.$effect(() => {
230
- const classList = getValue(this.#get(identifier));
177
+ this.effect(() => {
178
+ const classList = this.lookup(identifier)?.valueOf();
231
179
  if (classList && typeof classList === "object") {
232
180
  for (const [k, v] of Object.entries(classList)) {
233
- const force = !!getValue(v);
181
+ const force = !!v?.valueOf();
234
182
  for (const className of k.split(" ")) {
235
183
  target.classList.toggle(
236
184
  className,
@@ -247,8 +195,8 @@ var HandlerRegistry = class extends HTMLElement {
247
195
  #handleUse(e) {
248
196
  if (e instanceof CustomEvent) {
249
197
  const { hook } = e.detail;
250
- if (hook in this && typeof this.#get(hook) === "function") {
251
- const cleanup = this.#get(hook).bind(this)(e.target);
198
+ if (hook in this && typeof this.lookup(hook) === "function") {
199
+ const cleanup = this.lookup(hook).bind(this)(e.target);
252
200
  if (typeof cleanup === "function") {
253
201
  this.#cleanup.push(cleanup);
254
202
  }
@@ -261,20 +209,14 @@ var HandlerRegistry = class extends HTMLElement {
261
209
  const { identifier, attribute } = e.detail;
262
210
  const target = e.target;
263
211
  if (identifier in this && target instanceof HTMLElement && attribute in target) {
264
- const ref = this.#get(identifier);
265
- const setAttr = () => {
266
- const value = getValue(ref);
212
+ const ref = this.lookup(identifier);
213
+ this.effect(() => {
267
214
  if (booleanAttributes.includes(attribute)) {
268
- value ? target.setAttribute(attribute, "") : target.removeAttribute(attribute);
215
+ ref.valueOf() ? target.setAttribute(attribute, "") : target.removeAttribute(attribute);
269
216
  } else {
270
- target.setAttribute(attribute, `${value}`);
217
+ target.setAttribute(attribute, `${ref}`);
271
218
  }
272
- };
273
- if (isState(ref) || isComputed(ref)) {
274
- this.$effect(() => setAttr());
275
- } else {
276
- setAttr();
277
- }
219
+ });
278
220
  e.stopPropagation();
279
221
  }
280
222
  }
@@ -284,16 +226,10 @@ var HandlerRegistry = class extends HTMLElement {
284
226
  const { identifier, property } = e.detail;
285
227
  const target = e.target;
286
228
  if (identifier in this && target && property in target) {
287
- const ref = this.#get(identifier);
288
- const setProp = () => {
289
- const value = getValue(ref);
290
- target[property] = value;
291
- };
292
- if (isState(ref) || isComputed(ref)) {
293
- this.$effect(() => setProp());
294
- } else {
295
- setProp();
296
- }
229
+ const ref = this.lookup(identifier);
230
+ this.effect(() => {
231
+ target[property] = ref.valueOf();
232
+ });
297
233
  e.stopPropagation();
298
234
  }
299
235
  }
@@ -303,16 +239,10 @@ var HandlerRegistry = class extends HTMLElement {
303
239
  const target = e.target;
304
240
  const { identifier } = e.detail;
305
241
  if (identifier in this && target instanceof HTMLElement) {
306
- const ref = this.#get(identifier);
307
- const setTextContent = () => {
308
- const value = getValue(ref);
309
- target.textContent = `${value}`;
310
- };
311
- if (isState(ref) || isComputed(ref)) {
312
- this.$effect(() => setTextContent());
313
- } else {
314
- setTextContent();
315
- }
242
+ const ref = this.lookup(identifier);
243
+ this.effect(() => {
244
+ target.textContent = `${ref}`;
245
+ });
316
246
  e.stopPropagation();
317
247
  }
318
248
  }
@@ -322,16 +252,10 @@ var HandlerRegistry = class extends HTMLElement {
322
252
  const { identifier } = e.detail;
323
253
  const target = e.target;
324
254
  if (identifier in this && target instanceof HTMLElement) {
325
- const ref = this.#get(identifier);
326
- const setInnerHTML = () => {
327
- const value = getValue(ref);
328
- target.innerHTML = `${value}`;
329
- };
330
- if (isState(ref) || isComputed(ref)) {
331
- this.$effect(() => setInnerHTML());
332
- } else {
333
- setInnerHTML();
334
- }
255
+ const ref = this.lookup(identifier);
256
+ this.effect(() => {
257
+ target.innerHTML = `${ref}`;
258
+ });
335
259
  e.stopPropagation();
336
260
  }
337
261
  }
@@ -341,13 +265,13 @@ var HandlerRegistry = class extends HTMLElement {
341
265
  const { identifier, property } = e.detail;
342
266
  const target = e.target;
343
267
  if (identifier in this && target instanceof HTMLElement && property in target) {
344
- const state = this.#get(identifier);
268
+ const state = this.lookup(identifier);
345
269
  if (isState(state)) {
346
270
  state.value = target[property];
347
271
  target.addEventListener(bindingConfig[property].event, () => {
348
272
  state.value = target[property];
349
273
  });
350
- this.$effect(() => {
274
+ this.effect(() => {
351
275
  target[property] = state.value;
352
276
  });
353
277
  }
@@ -356,15 +280,15 @@ var HandlerRegistry = class extends HTMLElement {
356
280
  }
357
281
  }
358
282
  connectedCallback() {
359
- const { signal } = this.abortController;
360
- this.addEventListener("@attr-request", this.#handleAttr, { signal });
361
- this.addEventListener("@class-request", this.#handleClass, { signal });
362
- this.addEventListener("@on-request", this.#handleOn, { signal });
363
- this.addEventListener("@use-request", this.#handleUse, { signal });
364
- this.addEventListener("@prop-request", this.#handleProp, { signal });
365
- this.addEventListener("@html-request", this.#handleHTML, { signal });
366
- this.addEventListener("@text-request", this.#handleText, { signal });
367
- this.addEventListener("@bind-request", this.#handleBind, { signal });
283
+ const { signal: signal2 } = this.abortController;
284
+ this.addEventListener("@attr-request", this.#handleAttr, { signal: signal2 });
285
+ this.addEventListener("@class-request", this.#handleClass, { signal: signal2 });
286
+ this.addEventListener("@on-request", this.#handleOn, { signal: signal2 });
287
+ this.addEventListener("@use-request", this.#handleUse, { signal: signal2 });
288
+ this.addEventListener("@prop-request", this.#handleProp, { signal: signal2 });
289
+ this.addEventListener("@html-request", this.#handleHTML, { signal: signal2 });
290
+ this.addEventListener("@text-request", this.#handleText, { signal: signal2 });
291
+ this.addEventListener("@bind-request", this.#handleBind, { signal: signal2 });
368
292
  }
369
293
  disconnectedCallback() {
370
294
  this.abortController.abort();
@@ -375,4 +299,4 @@ var HandlerRegistry = class extends HTMLElement {
375
299
  };
376
300
  customElements?.define("handler-registry", HandlerRegistry);
377
301
 
378
- export { $array, $computed, $effect, $object, $state, HandlerRegistry, ReactiveComputation, ReactiveValue, getValue, isComputed, isState };
302
+ export { HandlerRegistry, computed, effect, isState, reactive, signal };
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@radishland/runtime",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "description": "The Radish runtime",
6
6
  "author": "Frédéric Crozatier",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/radishland/radish.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/radishland/radish/issues"
13
+ },
7
14
  "license": "MIT",
8
15
  "scripts": {
9
16
  "build": "tsup",
@@ -13,6 +20,10 @@
13
20
  ".": {
14
21
  "import": "./client/index.js",
15
22
  "types": "./client/index.d.ts"
23
+ },
24
+ "./boot": {
25
+ "import": "./client/boot.js",
26
+ "types": "./client/boot.d.ts"
16
27
  }
17
28
  },
18
29
  "files": [
@@ -21,8 +32,12 @@
21
32
  "LICENCE",
22
33
  "package.json"
23
34
  ],
35
+ "keywords": [
36
+ "radish",
37
+ "runtime"
38
+ ],
24
39
  "dependencies": {
25
- "signal-polyfill": "^0.2.2"
40
+ "@preact/signals-core": "^1.8.0"
26
41
  },
27
42
  "devDependencies": {
28
43
  "tsup": "^8.4.0",