sinho 0.2.2 → 0.3.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 (49) hide show
  1. package/dist/array_mutation.js +1 -1
  2. package/dist/array_mutation.js.map +1 -1
  3. package/dist/bundle.d.ts +16 -5
  4. package/dist/bundle.js +99 -80
  5. package/dist/bundle.min.js +1 -1
  6. package/dist/component.js +4 -1
  7. package/dist/component.js.map +1 -1
  8. package/dist/intrinsic/Dynamic.d.ts +33 -0
  9. package/dist/intrinsic/Dynamic.js +53 -0
  10. package/dist/intrinsic/Dynamic.js.map +1 -0
  11. package/dist/intrinsic/ErrorBoundary.d.ts +14 -0
  12. package/dist/intrinsic/ErrorBoundary.js +36 -0
  13. package/dist/intrinsic/ErrorBoundary.js.map +1 -0
  14. package/dist/intrinsic/For.js +21 -36
  15. package/dist/intrinsic/For.js.map +1 -1
  16. package/dist/intrinsic/Fragment.d.ts +1 -1
  17. package/dist/intrinsic/Fragment.js +5 -3
  18. package/dist/intrinsic/Fragment.js.map +1 -1
  19. package/dist/intrinsic/If.js +11 -7
  20. package/dist/intrinsic/If.js.map +1 -1
  21. package/dist/intrinsic/Portal.js +4 -4
  22. package/dist/intrinsic/Portal.js.map +1 -1
  23. package/dist/intrinsic/TagComponent.js +3 -3
  24. package/dist/intrinsic/TagComponent.js.map +1 -1
  25. package/dist/mod.d.ts +2 -2
  26. package/dist/mod.js +1 -0
  27. package/dist/mod.js.map +1 -1
  28. package/dist/scope.d.ts +9 -3
  29. package/dist/scope.js +32 -22
  30. package/dist/scope.js.map +1 -1
  31. package/dist/template.d.ts +7 -2
  32. package/dist/template.js +16 -0
  33. package/dist/template.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/array_mutation.ts +7 -4
  36. package/src/component.ts +4 -2
  37. package/src/intrinsic/For.ts +26 -43
  38. package/src/intrinsic/Fragment.ts +6 -4
  39. package/src/intrinsic/If.ts +22 -8
  40. package/src/intrinsic/Portal.ts +6 -4
  41. package/src/intrinsic/TagComponent.ts +4 -3
  42. package/src/mod.ts +2 -2
  43. package/src/scope.ts +52 -28
  44. package/src/template.ts +31 -2
  45. package/tsconfig.json +2 -1
  46. package/web/dist/shingo.min.d.ts +1131 -0
  47. package/web/dist/shingo.min.js +1 -0
  48. package/web/static/dist/bundle.d.ts +1126 -0
  49. package/web/static/dist/bundle.min.js +1 -0
@@ -4,14 +4,15 @@ import {
4
4
  Signal,
5
5
  SignalLike,
6
6
  useEffect,
7
+ useMemo,
7
8
  useSignal,
8
9
  useSubscope,
9
10
  } from "../scope.js";
10
11
  import { useRenderer } from "../renderer.js";
11
- import { createTemplate, Template } from "../template.js";
12
+ import { createTemplate, Template, TemplateNodes } from "../template.js";
12
13
 
13
14
  interface KeyMeta {
14
- _subnodes: Node[];
15
+ _subnodes: TemplateNodes;
15
16
  _destroy: () => void;
16
17
  }
17
18
 
@@ -32,38 +33,27 @@ export const For = <T>(props: {
32
33
  const items = MaybeSignal.upgrade(props.each ?? []);
33
34
  const anchor = renderer._node(() => document.createComment(""));
34
35
  const keyFn = props.key ?? ((_, i) => i);
35
- const nodes: Node[] = [anchor];
36
+ const nodes: [Comment, TemplateNodes[]] = [anchor, []];
36
37
  const keyMap = new Map<unknown, KeyMeta>();
37
38
  const mutationResult = useArrayMutation(items, keyFn);
38
39
 
39
- const lookForAnchor = (index: number): Node => {
40
- for (let i = index - 1; i >= 0; i--) {
41
- const key = keyFn(items()[index - 1], index - 1);
42
- const nodes = keyMap.get(key)?._subnodes ?? [];
43
-
44
- if (nodes.length > 0) {
45
- return nodes[nodes.length - 1];
46
- }
47
- }
48
-
49
- return anchor;
50
- };
40
+ const lookForAnchor = (index: number): Node =>
41
+ TemplateNodes.last(nodes[1], index - 1) ?? anchor;
51
42
 
52
43
  useEffect(() => {
53
44
  for (const mutation of mutationResult()._mutations) {
54
45
  if (mutation._type == "r") {
55
- const { _subnodes = [], _destroy } = keyMap.get(mutation._key) ?? {};
46
+ const { _subnodes, _destroy } = keyMap.get(mutation._key) ?? {};
56
47
  _destroy?.();
57
48
 
58
- const index = nodes.indexOf(_subnodes[0]);
59
- if (index > 0) {
60
- nodes.splice(index, _subnodes.length);
61
- }
49
+ nodes[1].splice(mutation._index, 1);
62
50
 
63
- _subnodes.forEach((node) => node.parentNode?.removeChild(node));
51
+ TemplateNodes.forEach(_subnodes ?? [], (node) =>
52
+ node.parentNode?.removeChild(node),
53
+ );
64
54
  keyMap.delete(mutation._key);
65
55
  } else if (mutation._type == "a") {
66
- let _subnodes: Node[] = [];
56
+ let _subnodes!: TemplateNodes;
67
57
 
68
58
  const [, destroy] = useSubscope(() => {
69
59
  const [index, setIndex] = useSignal(mutation._index);
@@ -84,36 +74,29 @@ export const For = <T>(props: {
84
74
  });
85
75
 
86
76
  _subnodes = props.children?.(item, index, items).build() ?? [];
77
+ nodes[1].splice(mutation._index, 0, _subnodes);
87
78
 
88
- const itemAnchor = lookForAnchor(mutation._index);
89
- const anchorIndex = nodes.indexOf(itemAnchor);
90
- if (anchorIndex >= 0) {
91
- nodes.splice(anchorIndex + 1, 0, ..._subnodes);
92
- }
79
+ let itemAnchor = lookForAnchor(mutation._index);
93
80
 
94
- _subnodes.forEach((node) =>
95
- itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling),
96
- );
81
+ TemplateNodes.forEach(_subnodes, (node) => {
82
+ itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling);
83
+ itemAnchor = node;
84
+ });
97
85
  });
98
86
 
99
87
  keyMap.set(mutation._key, { _subnodes, _destroy: destroy });
100
88
  } else if (mutation._type == "m") {
101
- const { _subnodes = [] } = keyMap.get(mutation._key) ?? {};
89
+ const { _subnodes } = keyMap.get(mutation._key) ?? {};
102
90
 
103
- const index = nodes.indexOf(_subnodes[0]);
104
- if (index >= 0) {
105
- nodes.splice(index, _subnodes.length);
106
- }
91
+ nodes[1].splice(mutation._from, 1);
92
+ nodes[1].splice(mutation._to, 0, _subnodes ?? []);
107
93
 
108
- const itemAnchor = lookForAnchor(mutation._to);
109
- const anchorIndex = nodes.indexOf(itemAnchor);
110
- if (anchorIndex >= 0) {
111
- nodes.splice(anchorIndex + 1, 0, ..._subnodes);
112
- }
94
+ let itemAnchor = lookForAnchor(mutation._to);
113
95
 
114
- _subnodes.forEach((node) =>
115
- itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling),
116
- );
96
+ TemplateNodes.forEach(_subnodes ?? [], (node) => {
97
+ itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling);
98
+ itemAnchor = node;
99
+ });
117
100
  }
118
101
  }
119
102
  }, [mutationResult]);
@@ -1,7 +1,7 @@
1
1
  import { Text } from "./Text.js";
2
2
  import { FunctionalComponent } from "../component.js";
3
3
  import { createTemplate, Template } from "../template.js";
4
- import type { MaybeSignal } from "../scope.js";
4
+ import { useMemo, type MaybeSignal } from "../scope.js";
5
5
 
6
6
  export type Children =
7
7
  | Template
@@ -31,8 +31,10 @@ export const Fragment: FunctionalComponent<{
31
31
  return !Array.isArray(children)
32
32
  ? children == null
33
33
  ? []
34
- : typeof children == "object"
35
- ? children
36
- : Text({ text: children })
34
+ : [
35
+ typeof children == "object"
36
+ ? children.build()
37
+ : Text({ text: children }).build(),
38
+ ]
37
39
  : children.flatMap((children) => Fragment({ children }).build());
38
40
  });
@@ -1,6 +1,13 @@
1
1
  import type { FunctionalComponent } from "../component.js";
2
- import { createTemplate, type Template } from "../template.js";
3
- import { MaybeSignal, useEffect, useMemo, useSubscope } from "../scope.js";
2
+ import { TemplateNodes, createTemplate, type Template } from "../template.js";
3
+ import {
4
+ MaybeSignal,
5
+ SignalLike,
6
+ useEffect,
7
+ useMemo,
8
+ useSignal,
9
+ useSubscope,
10
+ } from "../scope.js";
4
11
  import { runWithRenderer, useRenderer } from "../renderer.js";
5
12
  import { Children, Fragment } from "./Fragment.js";
6
13
 
@@ -38,21 +45,28 @@ export const ElseIf: FunctionalComponent<{
38
45
  return runWithRenderer({ _ifConditions: [] }, () =>
39
46
  createTemplate(() => {
40
47
  const anchor = renderer._node(() => document.createComment(""));
41
- const nodes: Node[] = [anchor];
48
+ const nodes: [Comment, TemplateNodes] = [anchor, []];
42
49
  const template = useMemo(() =>
43
50
  condition() ? Fragment({ children: props.children }) : null,
44
51
  );
45
52
 
46
- let subnodes: Node[] = [];
53
+ let subnodes: TemplateNodes = [];
47
54
 
48
55
  useEffect(() => {
49
- subnodes.forEach((node) => node.parentNode?.removeChild(node));
50
- nodes.length = 1;
56
+ TemplateNodes.forEach(subnodes, (node) =>
57
+ node.parentNode?.removeChild(node),
58
+ );
59
+ nodes[1] = [];
51
60
 
52
61
  const [, destroy] = useSubscope(() => {
53
62
  subnodes = template()?.build() ?? [];
54
- anchor.after(...subnodes);
55
- nodes.push(...subnodes);
63
+ nodes[1] = subnodes;
64
+
65
+ let before: Node = anchor;
66
+ TemplateNodes.forEach(subnodes, (node) => {
67
+ before.parentNode?.insertBefore(node, before.nextSibling);
68
+ before = node;
69
+ });
56
70
  });
57
71
 
58
72
  return destroy;
@@ -1,7 +1,7 @@
1
1
  import { FunctionalComponent } from "../component.js";
2
2
  import { runWithRenderer } from "../renderer.js";
3
3
  import { useEffect } from "../scope.js";
4
- import { createTemplate } from "../template.js";
4
+ import { TemplateNodes, createTemplate } from "../template.js";
5
5
  import { Children, Fragment } from "./Fragment.js";
6
6
 
7
7
  export const Portal: FunctionalComponent<{
@@ -13,12 +13,14 @@ export const Portal: FunctionalComponent<{
13
13
  const nodes = Fragment({ children }).build();
14
14
 
15
15
  useEffect(() => {
16
- nodes.forEach((node) => mount.appendChild(node));
16
+ TemplateNodes.forEach(nodes, (node) => mount.appendChild(node));
17
17
 
18
18
  return () => {
19
- nodes.forEach((node) => node.parentNode?.removeChild(node));
19
+ TemplateNodes.forEach(nodes, (node) =>
20
+ node.parentNode?.removeChild(node),
21
+ );
20
22
  };
21
- });
23
+ }, []);
22
24
 
23
25
  return [];
24
26
  }),
@@ -3,7 +3,7 @@ import { jsxPropNameToEventName } from "../utils.js";
3
3
  import { MaybeSignal, useBatch, useEffect, useScope } from "../scope.js";
4
4
  import { Fragment } from "./Fragment.js";
5
5
  import { runWithRenderer, useRenderer } from "../renderer.js";
6
- import { createTemplate, Template } from "../template.js";
6
+ import { createTemplate, Template, TemplateNodes } from "../template.js";
7
7
 
8
8
  export const hydrateElement = <E extends HTMLElement | SVGElement>(
9
9
  node: E,
@@ -66,14 +66,15 @@ export const hydrateElement = <E extends HTMLElement | SVGElement>(
66
66
  }
67
67
 
68
68
  if (props.children != null) {
69
- node.append(
70
- ...runWithRenderer(
69
+ TemplateNodes.forEach(
70
+ runWithRenderer(
71
71
  {
72
72
  _svg: svg,
73
73
  _nodes: node.childNodes.values(),
74
74
  },
75
75
  () => Fragment({ children: props.children }).build(),
76
76
  ),
77
+ (subnode) => node.append(subnode),
77
78
  );
78
79
  }
79
80
 
package/src/mod.ts CHANGED
@@ -15,8 +15,8 @@ export {
15
15
  } from "./component.js";
16
16
  export { type Context, createContext, useContext } from "./context.js";
17
17
  export { createElement, h } from "./create_element.js";
18
- export { DangerousHtml, Styles } from "./dom.js";
19
- export { type Template } from "./template.js";
18
+ export { type DangerousHtml, type Styles } from "./dom.js";
19
+ export { type Template, TemplateNodes } from "./template.js";
20
20
  export {
21
21
  type Cleanup,
22
22
  MaybeSignal,
package/src/scope.ts CHANGED
@@ -21,6 +21,13 @@ export interface Signal<out T> extends SignalLike<T> {
21
21
  peek(): T;
22
22
  }
23
23
 
24
+ export interface SignalOptions<T> extends SetSignalOptions {
25
+ /**
26
+ * A custom equality function to compare the new value with the old value.
27
+ */
28
+ equals?: (a: T, b: T) => boolean;
29
+ }
30
+
24
31
  export interface SetSignalOptions {
25
32
  /**
26
33
  * Whether to force the update of the signal even if the new value has the
@@ -116,6 +123,7 @@ let currBatch:
116
123
  | {
117
124
  _setters: (() => void)[];
118
125
  _effects: Set<Effect>;
126
+ _pureEffects: Set<Effect>;
119
127
  }
120
128
  | undefined;
121
129
 
@@ -128,14 +136,14 @@ export const useScope = <T = {}>(): Scope<T> => currScope as Scope<T>;
128
136
  */
129
137
  export const useSignal: (<T>(
130
138
  value: T,
131
- opts?: SetSignalOptions,
139
+ opts?: SignalOptions<T>,
132
140
  ) => readonly [Signal<T>, SignalSetter<T>]) &
133
141
  (<T>(
134
142
  value?: T,
135
- opts?: SetSignalOptions,
143
+ opts?: SignalOptions<T | undefined>,
136
144
  ) => readonly [Signal<T | undefined>, SignalSetter<T | undefined>]) = <T>(
137
145
  value: T,
138
- opts?: SetSignalOptions,
146
+ opts?: SignalOptions<T>,
139
147
  ): readonly [Signal<T>, SignalSetter<T>] => {
140
148
  const signal: Signal<T> = () => {
141
149
  if (!currUntracked && currEffect) {
@@ -150,7 +158,8 @@ export const useSignal: (<T>(
150
158
  signal.peek = () => value;
151
159
 
152
160
  const setter = (arg: T | ((value: T) => T), innerOpts?: SetSignalOptions) => {
153
- innerOpts = { ...opts, ...innerOpts };
161
+ const allOpts = { ...opts, ...innerOpts };
162
+ allOpts.equals ??= (a, b) => a === b;
154
163
 
155
164
  if (currBatch) {
156
165
  const newValue =
@@ -158,19 +167,25 @@ export const useSignal: (<T>(
158
167
  ? (arg as (value: T) => T)(signal.peek())
159
168
  : arg;
160
169
 
161
- if (innerOpts?.force || newValue !== signal.peek()) {
162
- if (innerOpts?.force) {
170
+ if (allOpts?.force || !allOpts.equals(newValue, signal.peek())) {
171
+ if (allOpts?.force) {
163
172
  value = newValue;
164
173
  } else {
165
174
  currBatch._setters.push(() => (value = newValue));
166
175
  }
167
176
 
168
- if (!innerOpts?.silent) {
169
- signal._effects.forEach((effect) => currBatch!._effects.add(effect));
177
+ if (!allOpts?.silent) {
178
+ signal._effects.forEach((effect) => {
179
+ if (effect._pure) {
180
+ currBatch!._pureEffects.add(effect);
181
+ } else {
182
+ currBatch!._effects.add(effect);
183
+ }
184
+ });
170
185
  }
171
186
  }
172
187
  } else {
173
- useBatch(() => setter(arg, innerOpts));
188
+ useBatch(() => setter(arg, allOpts));
174
189
  }
175
190
  };
176
191
 
@@ -186,7 +201,11 @@ export const useSignal: (<T>(
186
201
  export const useBatch = <T>(fn: () => T): T => {
187
202
  if (currBatch) return fn();
188
203
 
189
- currBatch = { _setters: [], _effects: new Set() };
204
+ currBatch = {
205
+ _setters: [],
206
+ _effects: new Set(),
207
+ _pureEffects: new Set(),
208
+ };
190
209
 
191
210
  try {
192
211
  const result = fn();
@@ -198,9 +217,12 @@ export const useBatch = <T>(fn: () => T): T => {
198
217
  };
199
218
 
200
219
  export const flushBatch = (): void => {
201
- a: while (
220
+ while (
202
221
  currBatch &&
203
- (currBatch._setters.length > 0 || currBatch._effects.size > 0)
222
+ currBatch._setters.length +
223
+ currBatch._effects.size +
224
+ currBatch._pureEffects.size >
225
+ 0
204
226
  ) {
205
227
  // Clean effect subscope
206
228
 
@@ -213,18 +235,14 @@ export const flushBatch = (): void => {
213
235
 
214
236
  // Run next effect
215
237
 
216
- for (const effect of currBatch._effects) {
217
- if (effect._pure) {
218
- effect._run();
219
- currBatch._effects.delete(effect);
220
- continue a;
221
- }
222
- }
238
+ const effect: Effect | undefined =
239
+ currBatch._pureEffects.values().next().value ??
240
+ currBatch._effects.values().next().value;
223
241
 
224
- for (const effect of currBatch._effects) {
242
+ if (effect) {
225
243
  effect._run();
244
+ currBatch._pureEffects.delete(effect);
226
245
  currBatch._effects.delete(effect);
227
- break;
228
246
  }
229
247
  }
230
248
  };
@@ -305,15 +323,18 @@ export const useEffect = (
305
323
  *
306
324
  * @param fn The computation function.
307
325
  */
308
- export const useMemo = <T>(fn: () => T, opts?: SetSignalOptions): Signal<T> => {
309
- const [memo, setMemo] = useSignal<T>();
326
+ export const useMemo = <T>(fn: () => T, opts?: SignalOptions<T>): Signal<T> => {
327
+ const [memo, setMemo] = useSignal<T>(
328
+ undefined,
329
+ opts as SignalOptions<T | undefined>,
330
+ );
310
331
 
311
332
  let firstTime = true;
312
333
  pureEffectFlag = true;
313
334
 
314
335
  try {
315
336
  useEffect(() => {
316
- setMemo(fn, firstTime ? { ...opts, force: true } : opts);
337
+ setMemo(fn, firstTime ? { force: true } : {});
317
338
 
318
339
  firstTime = false;
319
340
  });
@@ -384,15 +405,18 @@ export interface RefSignalSetter<in T> {
384
405
  /**
385
406
  * Creates a new signal with write capabilities.
386
407
  */
387
- export const useRef: (<T>(value: T, opts?: SetSignalOptions) => RefSignal<T>) &
388
- (<T>(value?: T, opts?: SetSignalOptions) => RefSignal<T | undefined>) = <T>(
408
+ export const useRef: (<T>(value: T, opts?: SignalOptions<T>) => RefSignal<T>) &
409
+ (<T>(
410
+ value?: T,
411
+ opts?: SignalOptions<T | undefined>,
412
+ ) => RefSignal<T | undefined>) = (<T>(
389
413
  value?: T,
390
- opts?: SetSignalOptions,
414
+ opts?: SignalOptions<T | undefined>,
391
415
  ): RefSignal<T> & RefSignal<T | undefined> => {
392
416
  const [signal, setter] = useSignal(value, opts);
393
417
  (signal as RefSignal<T | undefined>).set = setter;
394
418
  return signal as RefSignal<T> & RefSignal<T | undefined>;
395
- };
419
+ }) as any;
396
420
 
397
421
  /**
398
422
  * Represents a value that can be a signal or a constant value.
package/src/template.ts CHANGED
@@ -1,3 +1,30 @@
1
+ export type TemplateNodes = (Node | TemplateNodes)[];
2
+
3
+ export namespace TemplateNodes {
4
+ export const forEach = (
5
+ nodes: TemplateNodes,
6
+ fn: (node: Node) => void,
7
+ ): void =>
8
+ nodes.forEach((node) =>
9
+ Array.isArray(node) ? TemplateNodes.forEach(node, fn) : fn(node),
10
+ );
11
+
12
+ export const last = (
13
+ nodes: TemplateNodes,
14
+ lastIndex: number = nodes.length - 1,
15
+ ): Node | undefined => {
16
+ if (!nodes.length) return;
17
+
18
+ for (let i = lastIndex; i >= 0; i--) {
19
+ const last = nodes[i];
20
+ if (!Array.isArray(last)) return last;
21
+
22
+ const lastNode = TemplateNodes.last(last);
23
+ if (lastNode) return lastNode;
24
+ }
25
+ };
26
+ }
27
+
1
28
  /**
2
29
  * Represents a render result of a component.
3
30
  */
@@ -5,10 +32,12 @@ export interface Template {
5
32
  /**
6
33
  * Build the DOM elements represented by this template.
7
34
  */
8
- build(): Node[];
35
+ build(): TemplateNodes;
9
36
  }
10
37
 
11
- export const createTemplate = (build: () => Template | Node[]): Template => ({
38
+ export const createTemplate = (
39
+ build: () => Template | TemplateNodes,
40
+ ): Template => ({
12
41
  build() {
13
42
  const nodes = build();
14
43
  return (nodes as Template).build?.() ?? nodes;
package/tsconfig.json CHANGED
@@ -11,7 +11,8 @@
11
11
  "declaration": true,
12
12
  "sourceMap": true,
13
13
  "strict": true,
14
- "skipLibCheck": true
14
+ "skipLibCheck": true,
15
+ "isolatedModules": true
15
16
  },
16
17
  "include": ["./src/**/*"],
17
18
  "exclude": []