@praxisjs/runtime 0.7.0 → 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 +39 -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 +29 -1
  22. package/dist/dom/events.d.ts.map +1 -1
  23. package/dist/dom/events.js +167 -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 +57 -8
  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 +188 -17
  80. package/src/dom/props.ts +66 -9
  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 -331
  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 -385
  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 -363
  131. package/src/__tests__/context.test.ts +0 -128
  132. package/src/__tests__/dom.test.ts +0 -434
  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
@@ -0,0 +1,270 @@
1
+ import { mountChildren } from "./children";
2
+ import { getCurrentScope } from "./context";
3
+ import { createElement } from "./dom/create";
4
+ import { addDelegatedEvent, delegatedProp } from "./dom/events";
5
+ import { applyProp, bindClass, hasWritableProperty, setProp } from "./dom/props";
6
+ import { recording, recordProp } from "./hydration-context";
7
+ import { mountReactive } from "./reactive";
8
+ /** A reactive child's position: the comment it inserts its nodes before. */
9
+ export const MARKER = 0;
10
+ let pendingPatches = null;
11
+ // Templates are built in the inert document that <template> contents live in: cloning there and
12
+ // letting the insertion adopt the clone is cheaper than cloning inside the live document. Custom
13
+ // elements are the exception, since only the live document upgrades them, so a template that
14
+ // contains one is built (and cloned) there instead.
15
+ let inertDocument = null;
16
+ // null is the live document, looked up when used: the module is imported before any DOM exists
17
+ // when a server renderer installs one afterwards.
18
+ let buildDocument = null;
19
+ let sawCustomElement = false;
20
+ // The global document the cached templates were built for, and a counter that tells every template
21
+ // to build again. A server renderer may install a new window (a new global document) for each run:
22
+ // nodes cloned from another window's templates fail every `instanceof Node` check and are dropped.
23
+ let templatesDocument = null;
24
+ let generation = 0;
25
+ function inert() {
26
+ templatesDocument ??= document;
27
+ return (inertDocument ??= document.createElement("template").content.ownerDocument);
28
+ }
29
+ /** Called by render(): templates built for another global document are built again on next use. */
30
+ export function syncTemplateDocument() {
31
+ if (templatesDocument === null || templatesDocument === document)
32
+ return;
33
+ templatesDocument = null;
34
+ inertDocument = null;
35
+ generation++;
36
+ }
37
+ // Attribute-level props survive cloneNode, so they are applied to the template itself. A prop that
38
+ // lives only in a property (an expando, a custom element's field) would be lost by cloning, so it is
39
+ // replayed on every clone instead. Which is which is checked on a probe element, not guessed.
40
+ function bakeProp(el, key, value) {
41
+ const name = key === "htmlFor" ? "for" : key;
42
+ if (name === "class" || name === "className" || name === "style" || value === null || value === undefined) {
43
+ setProp(el, name, value);
44
+ return;
45
+ }
46
+ // Deep, so state held by the children (a <select>'s chosen option) is part of the check.
47
+ const probe = el.cloneNode(true);
48
+ setProp(probe, name, value);
49
+ const survives = !hasWritableProperty(probe, name) ||
50
+ Object.is(probe.cloneNode(true)[name], probe[name]);
51
+ if (survives)
52
+ setProp(el, name, value);
53
+ else
54
+ pendingPatches.push([el, name, value]);
55
+ }
56
+ function appendStatic(parent, child) {
57
+ if (child === MARKER) {
58
+ parent.appendChild((buildDocument ?? document).createComment(""));
59
+ }
60
+ else if (typeof child === "string") {
61
+ parent.appendChild((buildDocument ?? document).createTextNode(child));
62
+ }
63
+ else if (Array.isArray(child)) {
64
+ for (const c of child)
65
+ appendStatic(parent, c);
66
+ }
67
+ else if (child instanceof Node) {
68
+ parent.appendChild(child);
69
+ }
70
+ }
71
+ /** Builds one static element of a template. */
72
+ export function element(tag, props, children) {
73
+ if (tag.includes("-"))
74
+ sawCustomElement = true;
75
+ const el = createElement(tag, buildDocument ?? document);
76
+ // Children first, as mountElement does, so a prop that depends on them (a <select> value) sees them.
77
+ if (children !== null)
78
+ for (const child of children)
79
+ appendStatic(el, child);
80
+ if (props !== null)
81
+ for (const key in props)
82
+ bakeProp(el, key, props[key]);
83
+ return el;
84
+ }
85
+ function pathOf(root, node) {
86
+ const path = [];
87
+ let current = node;
88
+ while (current !== root) {
89
+ let index = 0;
90
+ let sibling = current.previousSibling;
91
+ while (sibling !== null) {
92
+ index++;
93
+ sibling = sibling.previousSibling;
94
+ }
95
+ path.unshift(index);
96
+ current = current.parentNode;
97
+ }
98
+ return path;
99
+ }
100
+ function nodeAt(root, path) {
101
+ let node = root;
102
+ for (const index of path)
103
+ node = node.childNodes[index];
104
+ return node;
105
+ }
106
+ // Builds the template root in `doc` (null: the live document) and lists the props to replay on
107
+ // every clone.
108
+ function buildIn(doc, build) {
109
+ const pending = [];
110
+ pendingPatches = pending;
111
+ buildDocument = doc;
112
+ sawCustomElement = false;
113
+ const built = build();
114
+ return [built, pending.length > 0 ? pending.map(([el, key, value]) => [pathOf(built, el), key, value]) : null];
115
+ }
116
+ /** Returns a function that clones the template `build` creates (built on first use). */
117
+ export function template(build) {
118
+ let root = null;
119
+ let patches = null;
120
+ let live = false;
121
+ let built = -1;
122
+ return () => {
123
+ if (built !== generation) {
124
+ const outerPatches = pendingPatches;
125
+ const outerDocument = buildDocument;
126
+ const outerSaw = sawCustomElement;
127
+ try {
128
+ let result = buildIn(inert(), build);
129
+ live = sawCustomElement;
130
+ if (live)
131
+ result = buildIn(null, build);
132
+ [root, patches] = result;
133
+ built = generation;
134
+ }
135
+ finally {
136
+ pendingPatches = outerPatches;
137
+ buildDocument = outerDocument;
138
+ sawCustomElement = outerSaw;
139
+ }
140
+ }
141
+ // A clone of a live-document template taken while another template builds forces that one
142
+ // into the live document too, or its own clones would hold never-upgraded custom elements.
143
+ if (live)
144
+ sawCustomElement = true;
145
+ const node = root.cloneNode(true);
146
+ if (patches !== null) {
147
+ for (const [path, key, value] of patches) {
148
+ const el = nodeAt(node, path);
149
+ setProp(el, key, value);
150
+ // Cloned while another template is being built: that template's clones need it too.
151
+ if (pendingPatches !== null)
152
+ pendingPatches.push([el, key, value]);
153
+ if (recording)
154
+ recordProp(el, key, value, getCurrentScope());
155
+ }
156
+ }
157
+ return node;
158
+ };
159
+ }
160
+ /**
161
+ * Returns a function that clones the tree `markup` parses to (parsed on first use). The compiler
162
+ * emits it only for markup the parser reads back as the tree element() would build, with no custom
163
+ * element in it, so it is parsed and cloned in the inert document like a built template.
164
+ */
165
+ export function html(markup) {
166
+ let root = null;
167
+ let built = -1;
168
+ return () => {
169
+ if (built !== generation) {
170
+ const parser = inert().createElement("template");
171
+ parser.innerHTML = markup;
172
+ root = parser.content.firstChild;
173
+ built = generation;
174
+ }
175
+ return root.cloneNode(true);
176
+ };
177
+ }
178
+ /** Applies a dynamic prop to a cloned element. */
179
+ export function prop(el, key, value, scope) {
180
+ if (recording)
181
+ recordProp(el, key, value, scope);
182
+ applyProp(el, key, value, scope);
183
+ }
184
+ /** Binds an event prop of a compiled template element through the delegated path (dom/events). */
185
+ export function on(el, key, value, scope) {
186
+ if (recording)
187
+ recordProp(el, key, value, scope);
188
+ const entry = delegatedProp(key);
189
+ if (entry !== null)
190
+ addDelegatedEvent(el, entry, value, scope);
191
+ else
192
+ applyProp(el, key, value, scope);
193
+ }
194
+ /**
195
+ * The binding target of an event prop, resolved once per module by the compiler: the delegated
196
+ * entry, or the key itself when `key` is not a known event (it is then bound as a plain prop).
197
+ */
198
+ export function event(key) {
199
+ return delegatedProp(key) ?? key;
200
+ }
201
+ /** Binds an event prop through the target `event()` resolved, with no lookup per binding. */
202
+ export function onEvent(el, target, value, scope) {
203
+ if (typeof target === "string") {
204
+ prop(el, target, value, scope);
205
+ return;
206
+ }
207
+ if (recording)
208
+ recordProp(el, target.key, value, scope);
209
+ addDelegatedEvent(el, target, value, scope);
210
+ }
211
+ /**
212
+ * Binds a reactive `class` (or `className`) prop: the compiler emits it for a function literal
213
+ * value, which applyProp would bind the same way after its key checks.
214
+ */
215
+ export function className(el, read, scope) {
216
+ if (recording)
217
+ recordProp(el, "class", read, scope);
218
+ bindClass(el, read, scope);
219
+ }
220
+ /** Mounts a reactive child before its marker comment from the template. */
221
+ export function reactive(marker, fn, scope) {
222
+ mountReactive(marker.parentNode, fn, scope, marker);
223
+ }
224
+ /**
225
+ * Binds a reactive child that is the only content of `parent`, which the template leaves empty: no
226
+ * anchor comment is needed. Templates compiled by earlier versions hold an empty text node there,
227
+ * which a text result takes over; a hydration build pass gets one too, as mountReactive registers
228
+ * fresh nodes of other results only.
229
+ */
230
+ export function reactiveOnly(parent, fn, scope) {
231
+ let placeholder = null;
232
+ if (parent.hasChildNodes())
233
+ placeholder = parent.firstChild;
234
+ else if (recording)
235
+ placeholder = parent.appendChild(document.createTextNode(""));
236
+ mountReactive(parent, fn, scope, null, placeholder);
237
+ }
238
+ /** Inserts the only child of `parent`, which the template leaves empty. */
239
+ export function insertOnly(parent, value, scope) {
240
+ // textContent builds the text node without a JS wrapper for it. An empty string still gets its
241
+ // (empty) text node, as mountChildren gives it one.
242
+ if ((typeof value === "string" && value !== "") || typeof value === "number") {
243
+ parent.textContent = value;
244
+ return;
245
+ }
246
+ mountChildren(parent, value, scope);
247
+ }
248
+ /** Inserts a dynamic child before `before`, or at the end of `parent` when it is null. */
249
+ export function insert(parent, value, before, scope) {
250
+ if (before === null) {
251
+ mountChildren(parent, value, scope);
252
+ }
253
+ else if (typeof value === "function") {
254
+ const end = parent.insertBefore(document.createComment(""), before);
255
+ mountReactive(parent, value, scope, end);
256
+ }
257
+ else if (value instanceof Node) {
258
+ parent.insertBefore(value, before);
259
+ }
260
+ else if (typeof value === "string" || typeof value === "number") {
261
+ before.before(String(value));
262
+ }
263
+ else if (Array.isArray(value)) {
264
+ // Each item goes straight in front of `before`, in order: no fragment to fill and empty again.
265
+ for (const item of value)
266
+ insert(parent, item, before, scope);
267
+ }
268
+ }
269
+ export { getCurrentScope as scope };
270
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@praxisjs/runtime",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
+ "sideEffects": false,
5
6
  "main": "./dist/index.js",
6
7
  "types": "./dist/index.d.ts",
7
8
  "exports": {
@@ -10,16 +11,24 @@
10
11
  "types": "./dist/index.d.ts"
11
12
  }
12
13
  },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!dist/**/__tests__",
18
+ "!src/**/__tests__",
19
+ "CHANGELOG.md"
20
+ ],
13
21
  "devDependencies": {
14
22
  "typescript": "^6.0.3"
15
23
  },
16
24
  "dependencies": {
17
- "@praxisjs/core": "2.2.0",
18
- "@praxisjs/decorators": "1.7.0",
19
- "@praxisjs/shared": "0.3.1"
25
+ "@praxisjs/core": "2.3.0",
26
+ "@praxisjs/decorators": "2.0.0",
27
+ "@praxisjs/shared": "0.3.2"
20
28
  },
21
29
  "scripts": {
22
- "build": "tsc",
23
- "dev": "tsc --watch"
30
+ "build": "tsc -p tsconfig.build.json",
31
+ "dev": "tsc -p tsconfig.build.json --watch",
32
+ "typecheck": "tsc --noEmit"
24
33
  }
25
34
  }
package/src/children.ts CHANGED
@@ -10,7 +10,8 @@ export function mountChildren(
10
10
  if (children === null || children === undefined || children === false) return;
11
11
 
12
12
  if (typeof children === "string" || typeof children === "number") {
13
- parent.appendChild(document.createTextNode(String(children)));
13
+ // append() builds the text node natively: no JS wrapper is created for it.
14
+ (parent as ParentNode).append(String(children));
14
15
  return;
15
16
  }
16
17
 
package/src/component.ts CHANGED
@@ -1,100 +1,172 @@
1
1
  import {
2
+ getComponentStart,
3
+ pauseTracking,
4
+ resumeTracking,
5
+ setCurrentOwner,
2
6
  setComponentAnchor,
3
7
  setComponentMounted,
4
- untrack,
5
8
  } from "@praxisjs/core/internal";
6
9
  import { initSlots } from "@praxisjs/decorators";
7
- import { isComponent, type ComponentConstructor } from "@praxisjs/shared/internal";
10
+ import type { ComponentConstructor } from "@praxisjs/shared/internal";
8
11
 
9
12
  import { mountChildren } from "./children";
10
- import { runInScope } from "./context";
13
+ import { swapScope } from "./context";
14
+ import { hotEnabled, isHotClass, latestClass, setHotRemount, trackHotInstance, type HotInstance } from "./hmr";
15
+ import { recording, onHydrationReplace } from "./hydration-context";
11
16
 
12
17
  import type { Scope } from "./scope";
13
18
 
14
- let pendingMounts: Array<() => void> = [];
19
+ type Instance = InstanceType<ComponentConstructor>;
20
+
21
+ // One object per component carries both lifecycle steps: mounting (drained in a shared microtask)
22
+ // and disposal (registered in the component's scope), instead of a closure for each.
23
+ class ComponentLife {
24
+ declare disposed: boolean;
25
+ declare readonly instance: Instance;
26
+ declare readonly ref: ((instance: object | null) => void) | undefined;
27
+
28
+ constructor(instance: Instance, ref: ((instance: object | null) => void) | undefined) {
29
+ this.disposed = false;
30
+ this.instance = instance;
31
+ this.ref = ref;
32
+ }
33
+
34
+ mount(): void {
35
+ if (this.disposed) return;
36
+ setComponentMounted(this.instance, true);
37
+ this.instance.onMount?.();
38
+ this.ref?.(this.instance);
39
+ }
40
+
41
+ dispose(): void {
42
+ this.disposed = true;
43
+ this.instance.onUnmount?.();
44
+ setComponentMounted(this.instance, false);
45
+ this.ref?.(null);
46
+ }
47
+ }
48
+
49
+ let pendingMounts: ComponentLife[] = [];
15
50
 
16
51
  // One shared microtask drains every component mounted in the same synchronous pass, in mount
17
52
  // order (children before parents). A throwing onMount is rethrown on its own microtask so it
18
53
  // can't stop the others, as it couldn't when each component had a microtask of its own.
19
- function scheduleMount(run: () => void): void {
20
- if (pendingMounts.length === 0) {
21
- queueMicrotask(() => {
22
- const queue = pendingMounts;
23
- pendingMounts = [];
24
- for (const job of queue) {
25
- try {
26
- job();
27
- } catch (e) {
28
- queueMicrotask(() => { throw e; });
29
- }
30
- }
31
- });
54
+ function drainMounts(): void {
55
+ const queue = pendingMounts;
56
+ pendingMounts = [];
57
+ for (const life of queue) {
58
+ try {
59
+ life.mount();
60
+ } catch (e) {
61
+ queueMicrotask(() => { throw e; });
62
+ }
32
63
  }
33
- pendingMounts.push(run);
64
+ }
65
+
66
+ function scheduleMount(life: ComponentLife): void {
67
+ if (pendingMounts.length === 0) queueMicrotask(drainMounts);
68
+ pendingMounts.push(life);
34
69
  }
35
70
 
36
71
  export function mountComponent(
37
72
  ctor: ComponentConstructor,
38
73
  props: Record<string, unknown>,
39
74
  parentScope: Scope,
75
+ // Hot remount only: the comments of the instance being replaced, kept as the new one's bounds.
76
+ bounds: [Comment, Comment] | null = null,
40
77
  ): Node[] {
41
- return untrack(() => {
78
+ const prevSub = pauseTracking();
79
+ try {
80
+ // A registered class (development, see hmr.ts) always mounts its newest version, and every
81
+ // instance gets comment bounds, which stay in place when it is mounted again.
82
+ let hot = false;
83
+ if (hotEnabled) {
84
+ ctor = latestClass(ctor);
85
+ hot = isHotClass(ctor);
86
+ }
42
87
  const scope = parentScope.fork();
43
88
 
44
- // Strip ref — managed by the runtime, not forwarded to the component.
45
- const { ref: refFn, ...instanceProps } = props;
46
- const ref = typeof refFn === "function"
47
- ? refFn as (instance: object | null) => void
48
- : undefined;
89
+ // Strip ref — managed by the runtime, not forwarded to the component. The component copies its
90
+ // props itself, so they are only copied here when there is a ref to leave out.
91
+ let instanceProps = props;
92
+ let ref: ((instance: object | null) => void) | undefined;
93
+ if ("ref" in props) {
94
+ const { ref: refFn, ...rest } = props;
95
+ instanceProps = rest;
96
+ if (typeof refFn === "function") ref = refFn as (instance: object | null) => void;
97
+ }
49
98
 
50
- const instance = new ctor({ ...instanceProps });
99
+ const instance = new ctor(instanceProps);
51
100
 
52
101
  const rawChildren = instanceProps.children;
53
102
  if (rawChildren != null) {
54
103
  initSlots(instance, rawChildren);
55
104
  }
56
105
 
57
- const start = document.createComment(`[${ctor.name}]`);
58
- const end = document.createComment(`[/${ctor.name}]`);
59
- let disposed = false;
60
-
61
- setComponentAnchor(instance, end);
62
-
63
106
  instance.onBeforeMount?.();
64
107
 
65
- const container = document.createDocumentFragment();
66
- container.appendChild(start);
67
-
68
108
  let dom: Node | Node[] | null = null;
69
- runInScope(scope, () => {
70
- try {
71
- dom = instance.render();
72
- } catch (e) {
73
- const fallback = instance.onError?.(e instanceof Error ? e : new Error(String(e)));
74
- if (fallback != null) dom = fallback;
109
+ const prevScope = swapScope(scope);
110
+ const prevOwner = setCurrentOwner(instance);
111
+ try {
112
+ dom = instance.render();
113
+ } catch (e) {
114
+ const fallback = instance.onError?.(e instanceof Error ? e : new Error(String(e)));
115
+ if (fallback != null) dom = fallback;
116
+ } finally {
117
+ setCurrentOwner(prevOwner);
118
+ swapScope(prevScope);
119
+ }
120
+
121
+ let nodes: Node[];
122
+ if (hot) {
123
+ const container = document.createDocumentFragment();
124
+ const start = bounds === null ? container.appendChild(document.createComment(`[${ctor.name}]`)) : bounds[0];
125
+ mountChildren(container, dom, scope);
126
+ const end = bounds === null ? container.appendChild(document.createComment(`[/${ctor.name}]`)) : bounds[1];
127
+ setComponentAnchor(instance, end);
128
+ nodes = Array.from(container.childNodes);
129
+ trackHotInstance({ ctor, props, parentScope, scope, start, end });
130
+ } else if (dom instanceof Node && dom.nodeType !== 11) {
131
+ // A single root node can't change, so it bounds the component by itself: no comments.
132
+ nodes = [dom];
133
+ setComponentAnchor(instance, dom);
134
+ if (recording) onHydrationReplace(dom, (real) => { setComponentAnchor(instance, real); });
135
+ } else {
136
+ // Several nodes, or ones a reactive child may replace: comments mark where the component
137
+ // starts and ends, unless its render already brought them (see getComponentStart).
138
+ const bounded = getComponentStart(instance) !== undefined;
139
+ const container = document.createDocumentFragment();
140
+ if (!bounded) container.appendChild(document.createComment(`[${ctor.name}]`));
141
+ mountChildren(container, dom, scope);
142
+ if (!bounded) {
143
+ const end = document.createComment(`[/${ctor.name}]`);
144
+ container.appendChild(end);
145
+ setComponentAnchor(instance, end);
75
146
  }
76
- });
77
-
78
- mountChildren(container, dom, scope);
79
- container.appendChild(end);
80
-
81
- scheduleMount(() => {
82
- if (disposed) return;
83
- setComponentMounted(instance, true);
84
- instance.onMount?.();
85
- ref?.(instance);
86
- });
87
-
88
- scope.add(() => {
89
- disposed = true;
90
- instance.onUnmount?.();
91
- setComponentMounted(instance, false);
92
- ref?.(null);
93
- });
94
-
95
- // Return the nodes from the fragment as an array so the caller can append them
96
- return Array.from(container.childNodes);
97
- });
147
+ nodes = Array.from(container.childNodes);
148
+ }
149
+
150
+ const life = new ComponentLife(instance, ref);
151
+ scheduleMount(life);
152
+ scope.add(life);
153
+ return nodes;
154
+ } finally {
155
+ resumeTracking(prevSub);
156
+ }
157
+ }
158
+
159
+ // Mounts `hot` again with `ctor` between the comments it already has: the reactive children and
160
+ // lists around it keep their references to those comments.
161
+ function remountHot(hot: HotInstance, ctor: ComponentConstructor): void {
162
+ const { start, end } = hot;
163
+ const parent = end.parentNode;
164
+ // Detached (kept aside by its owner): there is no place to render into, so it is left as it is.
165
+ if (parent === null || start.parentNode !== parent) return;
166
+ hot.scope.dispose();
167
+ for (let node = start.nextSibling; node !== null && node !== end; node = start.nextSibling) parent.removeChild(node);
168
+ const nodes = mountComponent(ctor, hot.props, hot.parentScope, [start, end]);
169
+ for (const node of nodes) parent.insertBefore(node, end);
98
170
  }
99
171
 
100
- export { isComponent };
172
+ setHotRemount(remountHot);
package/src/context.ts CHANGED
@@ -2,15 +2,38 @@ import type { Scope } from "./scope";
2
2
 
3
3
  let _currentScope: Scope | null = null;
4
4
 
5
+ /** Owner of a child scope that is created only once something asks for it (see swapLazyScope). */
6
+ export interface LazyScopeHost {
7
+ parentScope: Scope;
8
+ scope: Scope | null;
9
+ }
10
+
11
+ let lazyHost: LazyScopeHost | null = null;
12
+
5
13
  export function getCurrentScope(): Scope {
6
- if (!_currentScope) {
7
- throw new Error(
8
- "[PraxisJS] jsx() called outside of a render context. Make sure to call render() with a factory function.",
9
- );
14
+ if (_currentScope === null) {
15
+ if (lazyHost === null) {
16
+ throw new Error(
17
+ "[PraxisJS] jsx() called outside of a render context. Make sure to call render() with a factory function.",
18
+ );
19
+ }
20
+ _currentScope = lazyHost.scope ??= lazyHost.parentScope.fork();
10
21
  }
11
22
  return _currentScope;
12
23
  }
13
24
 
25
+ /**
26
+ * Makes `host` the scope source while no scope is current: its child scope is forked from
27
+ * `host.parentScope` the first time code asks for the current scope, so a run that registers
28
+ * nothing (a reactive text binding) creates no scope at all. Returns the previous host, which the
29
+ * caller must restore in a `finally`, together with the current scope (set it to `host.scope`).
30
+ */
31
+ export function swapLazyScope(host: LazyScopeHost | null): LazyScopeHost | null {
32
+ const prev = lazyHost;
33
+ lazyHost = host;
34
+ return prev;
35
+ }
36
+
14
37
  export function runInScope<T>(scope: Scope, fn: () => T): T {
15
38
  const prev = _currentScope;
16
39
  _currentScope = scope;
@@ -20,3 +43,11 @@ export function runInScope<T>(scope: Scope, fn: () => T): T {
20
43
  _currentScope = prev;
21
44
  }
22
45
  }
46
+
47
+ // Same as runInScope without a closure per call, for hot paths that create many scopes in a row.
48
+ // Callers must restore the returned scope in a `finally`.
49
+ export function swapScope(scope: Scope | null): Scope | null {
50
+ const prev = _currentScope;
51
+ _currentScope = scope;
52
+ return prev;
53
+ }