@praxisjs/runtime 0.7.1 → 0.8.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 (138) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/children.d.ts.map +1 -1
  3. package/dist/children.js +2 -1
  4. package/dist/children.js.map +1 -1
  5. package/dist/component.d.ts +2 -3
  6. package/dist/component.d.ts.map +1 -1
  7. package/dist/component.js +133 -60
  8. package/dist/component.js.map +1 -1
  9. package/dist/context.d.ts +13 -0
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js +24 -2
  12. package/dist/context.js.map +1 -1
  13. package/dist/dom/constants.d.ts +1 -1
  14. package/dist/dom/constants.d.ts.map +1 -1
  15. package/dist/dom/constants.js +24 -83
  16. package/dist/dom/constants.js.map +1 -1
  17. package/dist/dom/create.d.ts +1 -1
  18. package/dist/dom/create.d.ts.map +1 -1
  19. package/dist/dom/create.js +3 -3
  20. package/dist/dom/create.js.map +1 -1
  21. package/dist/dom/events.d.ts +28 -0
  22. package/dist/dom/events.d.ts.map +1 -1
  23. package/dist/dom/events.js +163 -17
  24. package/dist/dom/events.js.map +1 -1
  25. package/dist/dom/props.d.ts +4 -0
  26. package/dist/dom/props.d.ts.map +1 -1
  27. package/dist/dom/props.js +55 -7
  28. package/dist/dom/props.js.map +1 -1
  29. package/dist/dom/range.d.ts +4 -0
  30. package/dist/dom/range.d.ts.map +1 -0
  31. package/dist/dom/range.js +43 -0
  32. package/dist/dom/range.js.map +1 -0
  33. package/dist/dom/remove.d.ts.map +1 -1
  34. package/dist/dom/remove.js +2 -8
  35. package/dist/dom/remove.js.map +1 -1
  36. package/dist/element.d.ts.map +1 -1
  37. package/dist/element.js +7 -5
  38. package/dist/element.js.map +1 -1
  39. package/dist/hmr.d.ts +35 -0
  40. package/dist/hmr.d.ts.map +1 -0
  41. package/dist/hmr.js +85 -0
  42. package/dist/hmr.js.map +1 -0
  43. package/dist/hydration-context.d.ts +9 -1
  44. package/dist/hydration-context.d.ts.map +1 -1
  45. package/dist/hydration-context.js +34 -13
  46. package/dist/hydration-context.js.map +1 -1
  47. package/dist/hydration.d.ts.map +1 -1
  48. package/dist/hydration.js +4 -11
  49. package/dist/hydration.js.map +1 -1
  50. package/dist/index.d.ts +16 -5
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +31 -7
  53. package/dist/index.js.map +1 -1
  54. package/dist/list.d.ts +72 -0
  55. package/dist/list.d.ts.map +1 -0
  56. package/dist/list.js +786 -0
  57. package/dist/list.js.map +1 -0
  58. package/dist/portal.d.ts.map +1 -1
  59. package/dist/portal.js +3 -0
  60. package/dist/portal.js.map +1 -1
  61. package/dist/reactive.d.ts +2 -2
  62. package/dist/reactive.d.ts.map +1 -1
  63. package/dist/reactive.js +109 -34
  64. package/dist/reactive.js.map +1 -1
  65. package/dist/scope.d.ts +14 -4
  66. package/dist/scope.d.ts.map +1 -1
  67. package/dist/scope.js +55 -15
  68. package/dist/scope.js.map +1 -1
  69. package/dist/template.d.ts +48 -0
  70. package/dist/template.d.ts.map +1 -0
  71. package/dist/template.js +270 -0
  72. package/dist/template.js.map +1 -0
  73. package/package.json +15 -6
  74. package/src/children.ts +2 -1
  75. package/src/component.ts +135 -63
  76. package/src/context.ts +35 -4
  77. package/src/dom/constants.ts +25 -82
  78. package/src/dom/create.ts +3 -3
  79. package/src/dom/events.ts +184 -16
  80. package/src/dom/props.ts +65 -8
  81. package/src/dom/range.ts +45 -0
  82. package/src/dom/remove.ts +3 -7
  83. package/src/element.ts +8 -6
  84. package/src/hmr.ts +110 -0
  85. package/src/hydration-context.ts +34 -13
  86. package/src/hydration.ts +4 -9
  87. package/src/index.ts +54 -7
  88. package/src/list.ts +886 -0
  89. package/src/portal.ts +3 -0
  90. package/src/reactive.ts +147 -40
  91. package/src/scope.ts +63 -16
  92. package/src/template.ts +271 -0
  93. package/dist/__tests__/children.test.d.ts +0 -2
  94. package/dist/__tests__/children.test.d.ts.map +0 -1
  95. package/dist/__tests__/children.test.js +0 -241
  96. package/dist/__tests__/children.test.js.map +0 -1
  97. package/dist/__tests__/component.test.d.ts +0 -2
  98. package/dist/__tests__/component.test.d.ts.map +0 -1
  99. package/dist/__tests__/component.test.js +0 -347
  100. package/dist/__tests__/component.test.js.map +0 -1
  101. package/dist/__tests__/context.test.d.ts +0 -2
  102. package/dist/__tests__/context.test.d.ts.map +0 -1
  103. package/dist/__tests__/context.test.js +0 -111
  104. package/dist/__tests__/context.test.js.map +0 -1
  105. package/dist/__tests__/dom.test.d.ts +0 -2
  106. package/dist/__tests__/dom.test.d.ts.map +0 -1
  107. package/dist/__tests__/dom.test.js +0 -429
  108. package/dist/__tests__/dom.test.js.map +0 -1
  109. package/dist/__tests__/hydration.test.d.ts +0 -2
  110. package/dist/__tests__/hydration.test.d.ts.map +0 -1
  111. package/dist/__tests__/hydration.test.js +0 -299
  112. package/dist/__tests__/hydration.test.js.map +0 -1
  113. package/dist/__tests__/portal.test.d.ts +0 -2
  114. package/dist/__tests__/portal.test.d.ts.map +0 -1
  115. package/dist/__tests__/portal.test.js +0 -157
  116. package/dist/__tests__/portal.test.js.map +0 -1
  117. package/dist/__tests__/remove.test.d.ts +0 -2
  118. package/dist/__tests__/remove.test.d.ts.map +0 -1
  119. package/dist/__tests__/remove.test.js +0 -45
  120. package/dist/__tests__/remove.test.js.map +0 -1
  121. package/dist/__tests__/render.test.d.ts +0 -2
  122. package/dist/__tests__/render.test.d.ts.map +0 -1
  123. package/dist/__tests__/render.test.js +0 -97
  124. package/dist/__tests__/render.test.js.map +0 -1
  125. package/dist/__tests__/scope.test.d.ts +0 -2
  126. package/dist/__tests__/scope.test.d.ts.map +0 -1
  127. package/dist/__tests__/scope.test.js +0 -110
  128. package/dist/__tests__/scope.test.js.map +0 -1
  129. package/src/__tests__/children.test.ts +0 -276
  130. package/src/__tests__/component.test.ts +0 -381
  131. package/src/__tests__/context.test.ts +0 -128
  132. package/src/__tests__/dom.test.ts +0 -483
  133. package/src/__tests__/hydration.test.ts +0 -360
  134. package/src/__tests__/portal.test.ts +0 -177
  135. package/src/__tests__/remove.test.ts +0 -51
  136. package/src/__tests__/render.test.ts +0 -119
  137. package/src/__tests__/scope.test.ts +0 -120
  138. package/tsconfig.json +0 -8
@@ -1,6 +1,11 @@
1
1
  import type { Scope } from "./scope";
2
2
 
3
- let _recording = false;
3
+ /**
4
+ * True during a hydration build pass (see runInRecording). Exported as a live binding so the hot
5
+ * render paths read a variable instead of calling isRecording() for every binding.
6
+ */
7
+
8
+ export let recording = false;
4
9
 
5
10
  /**
6
11
  * Marks the synchronous "build" pass of a hydration attempt: `factory()` runs
@@ -11,17 +16,17 @@ let _recording = false;
11
16
  * element instead of the fresh one. No-op / zero cost outside a build pass.
12
17
  */
13
18
  export function runInRecording<T>(fn: () => T): T {
14
- const prev = _recording;
15
- _recording = true;
19
+ const prev = recording;
20
+ recording = true;
16
21
  try {
17
22
  return fn();
18
23
  } finally {
19
- _recording = prev;
24
+ recording = prev;
20
25
  }
21
26
  }
22
27
 
23
28
  export function isRecording(): boolean {
24
- return _recording;
29
+ return recording;
25
30
  }
26
31
 
27
32
  interface PropRecord {
@@ -35,19 +40,35 @@ export function recordProps(el: Element, props: Record<string, unknown>, scope:
35
40
  _propRecords.set(el, { props, scope });
36
41
  }
37
42
 
43
+ /** Adds one prop to an element's record, for elements whose props are applied one at a time. */
44
+ export function recordProp(el: Element, key: string, value: unknown, scope: Scope): void {
45
+ const record = _propRecords.get(el);
46
+ if (record === undefined) _propRecords.set(el, { props: { [key]: value }, scope });
47
+ else record.props[key] = value;
48
+ }
49
+
38
50
  export function getRecordedProps(el: Element): PropRecord | undefined {
39
51
  return _propRecords.get(el);
40
52
  }
41
53
 
42
- // mountReactive() keeps a `currentNodes` array (by reference) for diffing its
43
- // *next* update. If reconcile() replaces one of those fresh nodes with a real
44
- // one, it needs to overwrite that array in place so the closure sees the swap.
45
- const _reactiveOwners = new WeakMap<Node, Node[]>();
54
+ // Code that keeps references to fresh nodes for its *next* update (mountReactive's
55
+ // `currentNodes`, a list row's boundaries) registers how to repoint them, so reconcile()
56
+ // can tell it when it keeps a real element in place of a fresh one. One node can have several
57
+ // owners (a list row's element is also part of the nodes an enclosing reactive child returned).
58
+ const _replacers = new WeakMap<Node, (real: Node) => void>();
46
59
 
47
- export function registerReactiveNode(node: Node, owner: Node[]): void {
48
- _reactiveOwners.set(node, owner);
60
+ export function onHydrationReplace(node: Node, replace: (real: Node) => void): void {
61
+ const prev = _replacers.get(node);
62
+ _replacers.set(node, prev === undefined ? replace : (real) => { prev(real); replace(real); });
63
+ }
64
+
65
+ export function getHydrationReplacer(node: Node): ((real: Node) => void) | undefined {
66
+ return _replacers.get(node);
49
67
  }
50
68
 
51
- export function getReactiveOwner(node: Node): Node[] | undefined {
52
- return _reactiveOwners.get(node);
69
+ export function registerReactiveNode(node: Node, owner: Node[]): void {
70
+ onHydrationReplace(node, (real) => {
71
+ const idx = owner.indexOf(node);
72
+ if (idx !== -1) owner[idx] = real;
73
+ });
53
74
  }
package/src/hydration.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { applyProp } from "./dom/props";
2
- import { getReactiveOwner, getRecordedProps } from "./hydration-context";
2
+ import { getHydrationReplacer, getRecordedProps } from "./hydration-context";
3
3
 
4
4
  function replayProps(real: Element, fresh: Element): void {
5
5
  const record = getRecordedProps(fresh);
@@ -10,15 +10,10 @@ function replayProps(real: Element, fresh: Element): void {
10
10
  }
11
11
  }
12
12
 
13
- // mountReactive()'s `currentNodes` array holds fresh node references for its
14
- // *next* diff. If we're keeping `real` instead of `fresh` here, that array
15
- // needs to see the swap too — it's registered by reference, so this mutation
16
- // is visible to whichever effect closure owns it.
13
+ // mountReactive()'s `currentNodes` and list rows keep fresh node references for their
14
+ // *next* update. If we're keeping `real` instead of `fresh` here, they need to see the swap.
17
15
  function notifyReplacement(fresh: Node, real: Node): void {
18
- const owner = getReactiveOwner(fresh);
19
- if (!owner) return;
20
- const idx = owner.indexOf(fresh);
21
- if (idx !== -1) owner[idx] = real;
16
+ getHydrationReplacer(fresh)?.(real);
22
17
  }
23
18
 
24
19
  /**
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { mountChildren } from "./children";
2
2
  import { runInScope } from "./context";
3
+ import { addEventRoot } from "./dom/events";
3
4
  import { reconcile } from "./hydration";
4
5
  import { runInRecording } from "./hydration-context";
5
6
  import { Scope } from "./scope";
7
+ import { syncTemplateDocument } from "./template";
6
8
 
7
9
  /** Written by `@praxisjs/ssg` onto the mount container in its prerendered HTML. */
8
10
  const SSG_MARKER = "data-praxis-ssg";
@@ -11,8 +13,10 @@ function mountCreate(
11
13
  factory: () => Node | Node[] | null,
12
14
  container: HTMLElement,
13
15
  ): () => void {
16
+ syncTemplateDocument();
14
17
  const rootScope = new Scope();
15
18
  container.innerHTML = "";
19
+ const removeEventRoot = addEventRoot(container);
16
20
 
17
21
  runInScope(rootScope, () => {
18
22
  const result = factory();
@@ -21,6 +25,7 @@ function mountCreate(
21
25
 
22
26
  return () => {
23
27
  rootScope.dispose();
28
+ removeEventRoot();
24
29
  container.innerHTML = "";
25
30
  };
26
31
  }
@@ -29,6 +34,7 @@ function hydrate(
29
34
  factory: () => Node | Node[] | null,
30
35
  container: HTMLElement,
31
36
  ): () => void {
37
+ syncTemplateDocument();
32
38
  const rootScope = new Scope();
33
39
  const scratch = document.createDocumentFragment();
34
40
 
@@ -44,24 +50,40 @@ function hydrate(
44
50
  });
45
51
 
46
52
  reconcile(container, Array.from(scratch.childNodes));
53
+ const removeEventRoot = addEventRoot(container);
47
54
 
48
55
  return () => {
49
56
  rootScope.dispose();
57
+ removeEventRoot();
50
58
  container.innerHTML = "";
51
59
  };
52
60
  }
53
61
 
62
+ // Reached only through enableHydration(), so an app that never calls it leaves the reconciliation
63
+ // and every recording branch of the render paths out of its bundle.
64
+ let hydrateRoot: typeof hydrate | null = null;
65
+
66
+ /**
67
+ * Lets render() hydrate the HTML `@praxisjs/ssg` prerendered. `ssgPlugin()` calls it at the start of
68
+ * the client bundle, so apps it builds need nothing more; a page prerendered another way calls it
69
+ * once before render(). Without it, a container carrying the hydration marker is cleared and
70
+ * mounted fresh.
71
+ */
72
+ export function enableHydration(): void {
73
+ hydrateRoot = hydrate;
74
+ }
75
+
54
76
  /**
55
77
  * Renders a component tree into a container element.
56
78
  *
57
79
  * The factory function is called once inside the root scope,
58
80
  * so `jsx()` can access the current scope via `getCurrentScope()`.
59
81
  *
60
- * If `container` carries the marker `@praxisjs/ssg` stamps onto prerendered
61
- * HTML, the tree is built normally into a detached scratch node and then
62
- * reconciled against the existing DOM (real elements are kept and have their
63
- * props/listeners replayed onto them; text/comments and any mismatched nodes
64
- * are recreated) instead of clearing the container and mounting fresh.
82
+ * Once hydration is enabled (see `enableHydration()`), a `container` carrying the marker
83
+ * `@praxisjs/ssg` stamps onto prerendered HTML has its tree built normally into a detached
84
+ * scratch node and then reconciled against the existing DOM (real elements are kept and have their
85
+ * props/listeners replayed onto them; text/comments and any mismatched nodes are recreated)
86
+ * instead of being cleared and mounted fresh.
65
87
  *
66
88
  * @example
67
89
  * render(() => <App />, document.getElementById('app'));
@@ -70,8 +92,8 @@ export function render(
70
92
  factory: () => Node | Node[] | null,
71
93
  container: HTMLElement,
72
94
  ): () => void {
73
- if (container.hasAttribute(SSG_MARKER)) {
74
- return hydrate(factory, container);
95
+ if (hydrateRoot !== null && container.hasAttribute(SSG_MARKER)) {
96
+ return hydrateRoot(factory, container);
75
97
  }
76
98
  return mountCreate(factory, container);
77
99
  }
@@ -79,5 +101,30 @@ export function render(
79
101
  export { Scope } from "./scope";
80
102
  export { runInScope, getCurrentScope } from "./context";
81
103
  export { mountElement } from "./element";
104
+ export { mountChildren } from "./children";
105
+ export {
106
+ template as _$template,
107
+ html as _$html,
108
+ element as _$element,
109
+ prop as _$prop,
110
+ reactive as _$reactive,
111
+ insert as _$insert,
112
+ reactiveOnly as _$reactiveOnly,
113
+ insertOnly as _$insertOnly,
114
+ on as _$on,
115
+ event as _$event,
116
+ onEvent as _$onEvent,
117
+ className as _$className,
118
+ scope as _$scope,
119
+ } from "./template";
82
120
  export { mountComponent } from "./component";
121
+ export { registerHot as _$hmrRegister, acceptsHotUpdate as _$hmrAccepts } from "./hmr";
83
122
  export { Portal, type PortalProps } from "./portal";
123
+ export {
124
+ For,
125
+ Index,
126
+ type ForProps,
127
+ type IndexProps,
128
+ type ListSource,
129
+ type ListTransition,
130
+ } from "./list";