@praxisjs/runtime 0.5.5 → 0.7.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.
- package/CHANGELOG.md +31 -0
- package/dist/__tests__/children.test.js +54 -0
- package/dist/__tests__/children.test.js.map +1 -1
- package/dist/__tests__/component.test.js +60 -1
- package/dist/__tests__/component.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +100 -0
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/hydration.test.d.ts +2 -0
- package/dist/__tests__/hydration.test.d.ts.map +1 -0
- package/dist/__tests__/hydration.test.js +299 -0
- package/dist/__tests__/hydration.test.js.map +1 -0
- package/dist/__tests__/portal.test.js +22 -0
- package/dist/__tests__/portal.test.js.map +1 -1
- package/dist/__tests__/remove.test.d.ts +2 -0
- package/dist/__tests__/remove.test.d.ts.map +1 -0
- package/dist/__tests__/remove.test.js +45 -0
- package/dist/__tests__/remove.test.js.map +1 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +22 -1
- package/dist/component.js.map +1 -1
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +18 -2
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +27 -5
- package/dist/dom/props.js.map +1 -1
- package/dist/dom/remove.d.ts +2 -0
- package/dist/dom/remove.d.ts.map +1 -0
- package/dist/dom/remove.js +23 -0
- package/dist/dom/remove.js.map +1 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +5 -0
- package/dist/element.js.map +1 -1
- package/dist/hydration-context.d.ts +21 -0
- package/dist/hydration-context.d.ts.map +1 -0
- package/dist/hydration-context.js +40 -0
- package/dist/hydration-context.js.map +1 -0
- package/dist/hydration.d.ts +18 -0
- package/dist/hydration.d.ts.map +1 -0
- package/dist/hydration.js +67 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -10
- package/dist/index.js.map +1 -1
- package/dist/portal.d.ts.map +1 -1
- package/dist/portal.js +16 -7
- package/dist/portal.js.map +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +16 -10
- package/dist/reactive.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/children.test.ts +58 -0
- package/src/__tests__/component.test.ts +67 -1
- package/src/__tests__/dom.test.ts +108 -0
- package/src/__tests__/hydration.test.ts +360 -0
- package/src/__tests__/portal.test.ts +24 -0
- package/src/__tests__/remove.test.ts +51 -0
- package/src/component.ts +23 -1
- package/src/dom/events.ts +21 -2
- package/src/dom/props.ts +30 -5
- package/src/dom/remove.ts +22 -0
- package/src/element.ts +5 -0
- package/src/hydration-context.ts +53 -0
- package/src/hydration.ts +70 -0
- package/src/index.ts +57 -11
- package/src/portal.ts +16 -7
- package/src/reactive.ts +15 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/dom/remove.ts"],"names":[],"mappings":"AAIA,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAiB/C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Replaces Range.deleteContents(): creating a Range per removal is several times slower than
|
|
2
|
+
// walking the siblings, and unreachable Ranges stay registered with the document until collected.
|
|
3
|
+
// Walking first → last (rather than only the tracked nodes) also removes anything a nested reactive
|
|
4
|
+
// child inserted between them after mount, which is what the Range used to cover.
|
|
5
|
+
export function removeNodes(nodes) {
|
|
6
|
+
const first = nodes[0];
|
|
7
|
+
const last = nodes[nodes.length - 1];
|
|
8
|
+
const parent = first.parentNode;
|
|
9
|
+
if (parent === null || last.parentNode !== parent) {
|
|
10
|
+
for (const node of nodes)
|
|
11
|
+
node.parentNode?.removeChild(node);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let node = first;
|
|
15
|
+
while (node) {
|
|
16
|
+
const next = node.nextSibling;
|
|
17
|
+
parent.removeChild(node);
|
|
18
|
+
if (node === last)
|
|
19
|
+
return;
|
|
20
|
+
node = next;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=remove.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove.js","sourceRoot":"","sources":["../../src/dom/remove.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,kGAAkG;AAClG,oGAAoG;AACpG,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC;IAEhC,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;QAClD,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7D,OAAO;IACT,CAAC;IAED,IAAI,IAAI,GAAgB,KAAK,CAAC;IAC9B,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAgB,IAAI,CAAC,WAAW,CAAC;QAC3C,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAC1B,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/dist/element.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,KAAK,GACX,WAAW,GAAG,UAAU,CAiB1B"}
|
package/dist/element.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { mountChildren } from "./children";
|
|
2
2
|
import { createElement } from "./dom/create";
|
|
3
3
|
import { applyProp } from "./dom/props";
|
|
4
|
+
import { isRecording, recordProps } from "./hydration-context";
|
|
4
5
|
export function mountElement(tag, props, scope) {
|
|
5
6
|
const el = createElement(tag);
|
|
7
|
+
// During a hydration build pass, retain (props, scope) so reconcile() can
|
|
8
|
+
// replay them onto whichever real element it ends up keeping instead.
|
|
9
|
+
if (isRecording())
|
|
10
|
+
recordProps(el, props, scope);
|
|
6
11
|
for (const key in props) {
|
|
7
12
|
if (key === "children")
|
|
8
13
|
continue;
|
package/dist/element.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAI/D,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,KAA8B,EAC9B,KAAY;IAEZ,MAAM,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAE9B,0EAA0E;IAC1E,sEAAsE;IACtE,IAAI,WAAW,EAAE;QAAE,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAEjD,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,UAAU;YAAE,SAAS;QACjC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Scope } from "./scope";
|
|
2
|
+
/**
|
|
3
|
+
* Marks the synchronous "build" pass of a hydration attempt: `factory()` runs
|
|
4
|
+
* completely normally (same create-mode code as a plain client render), but
|
|
5
|
+
* every element additionally records its (props, scope) for reconcile()'s
|
|
6
|
+
* replay step, and every mountReactive() region records which of its output
|
|
7
|
+
* nodes may need to be swapped in-place if reconcile() decides to keep a real
|
|
8
|
+
* element instead of the fresh one. No-op / zero cost outside a build pass.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runInRecording<T>(fn: () => T): T;
|
|
11
|
+
export declare function isRecording(): boolean;
|
|
12
|
+
interface PropRecord {
|
|
13
|
+
props: Record<string, unknown>;
|
|
14
|
+
scope: Scope;
|
|
15
|
+
}
|
|
16
|
+
export declare function recordProps(el: Element, props: Record<string, unknown>, scope: Scope): void;
|
|
17
|
+
export declare function getRecordedProps(el: Element): PropRecord | undefined;
|
|
18
|
+
export declare function registerReactiveNode(node: Node, owner: Node[]): void;
|
|
19
|
+
export declare function getReactiveOwner(node: Node): Node[] | undefined;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=hydration-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-context.d.ts","sourceRoot":"","sources":["../src/hydration-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIrC;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQhD;AAED,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,EAAE,KAAK,CAAC;CACd;AAID,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAE3F;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAEpE;AAOD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,SAAS,CAE/D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
let _recording = false;
|
|
2
|
+
/**
|
|
3
|
+
* Marks the synchronous "build" pass of a hydration attempt: `factory()` runs
|
|
4
|
+
* completely normally (same create-mode code as a plain client render), but
|
|
5
|
+
* every element additionally records its (props, scope) for reconcile()'s
|
|
6
|
+
* replay step, and every mountReactive() region records which of its output
|
|
7
|
+
* nodes may need to be swapped in-place if reconcile() decides to keep a real
|
|
8
|
+
* element instead of the fresh one. No-op / zero cost outside a build pass.
|
|
9
|
+
*/
|
|
10
|
+
export function runInRecording(fn) {
|
|
11
|
+
const prev = _recording;
|
|
12
|
+
_recording = true;
|
|
13
|
+
try {
|
|
14
|
+
return fn();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
_recording = prev;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isRecording() {
|
|
21
|
+
return _recording;
|
|
22
|
+
}
|
|
23
|
+
const _propRecords = new WeakMap();
|
|
24
|
+
export function recordProps(el, props, scope) {
|
|
25
|
+
_propRecords.set(el, { props, scope });
|
|
26
|
+
}
|
|
27
|
+
export function getRecordedProps(el) {
|
|
28
|
+
return _propRecords.get(el);
|
|
29
|
+
}
|
|
30
|
+
// mountReactive() keeps a `currentNodes` array (by reference) for diffing its
|
|
31
|
+
// *next* update. If reconcile() replaces one of those fresh nodes with a real
|
|
32
|
+
// one, it needs to overwrite that array in place so the closure sees the swap.
|
|
33
|
+
const _reactiveOwners = new WeakMap();
|
|
34
|
+
export function registerReactiveNode(node, owner) {
|
|
35
|
+
_reactiveOwners.set(node, owner);
|
|
36
|
+
}
|
|
37
|
+
export function getReactiveOwner(node) {
|
|
38
|
+
return _reactiveOwners.get(node);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=hydration-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-context.js","sourceRoot":"","sources":["../src/hydration-context.ts"],"names":[],"mappings":"AAEA,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAI,EAAW;IAC3C,MAAM,IAAI,GAAG,UAAU,CAAC;IACxB,UAAU,GAAG,IAAI,CAAC;IAClB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,CAAC;AACpB,CAAC;AAOD,MAAM,YAAY,GAAG,IAAI,OAAO,EAAuB,CAAC;AAExD,MAAM,UAAU,WAAW,CAAC,EAAW,EAAE,KAA8B,EAAE,KAAY;IACnF,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAW;IAC1C,OAAO,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,8EAA8E;AAC9E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,eAAe,GAAG,IAAI,OAAO,EAAgB,CAAC;AAEpD,MAAM,UAAU,oBAAoB,CAAC,IAAU,EAAE,KAAa;IAC5D,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAU;IACzC,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks `freshChildren` (the complete, correctly-nested output of a normal
|
|
3
|
+
* create-mode mount into a detached scratch node) against `realParent`'s
|
|
4
|
+
* existing children (the server-rendered DOM already attached to the page),
|
|
5
|
+
* in document order — safe now that both trees are fully built, unlike trying
|
|
6
|
+
* to adopt while `freshChildren` was still being constructed.
|
|
7
|
+
*
|
|
8
|
+
* Matching elements keep the real node (props/listeners replayed onto it,
|
|
9
|
+
* via `applyProp` — the same function normal mounting uses) and recurse.
|
|
10
|
+
* Text/comment nodes are always taken from the fresh side — they carry no
|
|
11
|
+
* listeners or state, so recreating them is free and avoids having to
|
|
12
|
+
* re-point mountComponent/mountReactive's anchor comments. Anything that
|
|
13
|
+
* doesn't line up (different tag, or one side ran out) is a local mismatch:
|
|
14
|
+
* the stale real node is dropped and the fresh one takes its place — no
|
|
15
|
+
* lookahead, no reordering, and no whole-page fallback.
|
|
16
|
+
*/
|
|
17
|
+
export declare function reconcile(realParent: Node, freshChildren: Node[]): void;
|
|
18
|
+
//# sourceMappingURL=hydration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration.d.ts","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,IAAI,CA8BvE"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { applyProp } from "./dom/props";
|
|
2
|
+
import { getReactiveOwner, getRecordedProps } from "./hydration-context";
|
|
3
|
+
function replayProps(real, fresh) {
|
|
4
|
+
const record = getRecordedProps(fresh);
|
|
5
|
+
if (!record)
|
|
6
|
+
return;
|
|
7
|
+
for (const key in record.props) {
|
|
8
|
+
if (key === "children")
|
|
9
|
+
continue;
|
|
10
|
+
applyProp(real, key, record.props[key], record.scope);
|
|
11
|
+
}
|
|
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.
|
|
17
|
+
function notifyReplacement(fresh, real) {
|
|
18
|
+
const owner = getReactiveOwner(fresh);
|
|
19
|
+
if (!owner)
|
|
20
|
+
return;
|
|
21
|
+
const idx = owner.indexOf(fresh);
|
|
22
|
+
if (idx !== -1)
|
|
23
|
+
owner[idx] = real;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Walks `freshChildren` (the complete, correctly-nested output of a normal
|
|
27
|
+
* create-mode mount into a detached scratch node) against `realParent`'s
|
|
28
|
+
* existing children (the server-rendered DOM already attached to the page),
|
|
29
|
+
* in document order — safe now that both trees are fully built, unlike trying
|
|
30
|
+
* to adopt while `freshChildren` was still being constructed.
|
|
31
|
+
*
|
|
32
|
+
* Matching elements keep the real node (props/listeners replayed onto it,
|
|
33
|
+
* via `applyProp` — the same function normal mounting uses) and recurse.
|
|
34
|
+
* Text/comment nodes are always taken from the fresh side — they carry no
|
|
35
|
+
* listeners or state, so recreating them is free and avoids having to
|
|
36
|
+
* re-point mountComponent/mountReactive's anchor comments. Anything that
|
|
37
|
+
* doesn't line up (different tag, or one side ran out) is a local mismatch:
|
|
38
|
+
* the stale real node is dropped and the fresh one takes its place — no
|
|
39
|
+
* lookahead, no reordering, and no whole-page fallback.
|
|
40
|
+
*/
|
|
41
|
+
export function reconcile(realParent, freshChildren) {
|
|
42
|
+
let realCursor = realParent.firstChild;
|
|
43
|
+
for (const fresh of freshChildren) {
|
|
44
|
+
if (fresh.nodeType === Node.ELEMENT_NODE &&
|
|
45
|
+
realCursor?.nodeType === Node.ELEMENT_NODE &&
|
|
46
|
+
realCursor.localName === fresh.localName) {
|
|
47
|
+
const real = realCursor;
|
|
48
|
+
realCursor = real.nextSibling;
|
|
49
|
+
replayProps(real, fresh);
|
|
50
|
+
reconcile(real, Array.from(fresh.childNodes));
|
|
51
|
+
notifyReplacement(fresh, real);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (realCursor) {
|
|
55
|
+
const stale = realCursor;
|
|
56
|
+
realCursor = realCursor.nextSibling;
|
|
57
|
+
stale.remove();
|
|
58
|
+
}
|
|
59
|
+
realParent.insertBefore(fresh, realCursor);
|
|
60
|
+
}
|
|
61
|
+
while (realCursor) {
|
|
62
|
+
const stale = realCursor;
|
|
63
|
+
realCursor = realCursor.nextSibling;
|
|
64
|
+
stale.remove();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=hydration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEzE,SAAS,WAAW,CAAC,IAAa,EAAE,KAAc;IAChD,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,GAAG,KAAK,UAAU;YAAE,SAAS;QACjC,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,6EAA6E;AAC7E,kDAAkD;AAClD,SAAS,iBAAiB,CAAC,KAAW,EAAE,IAAU;IAChD,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,SAAS,CAAC,UAAgB,EAAE,aAAqB;IAC/D,IAAI,UAAU,GAAqB,UAAU,CAAC,UAAU,CAAC;IAEzD,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IACE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY;YACpC,UAAU,EAAE,QAAQ,KAAK,IAAI,CAAC,YAAY;YACzC,UAAsB,CAAC,SAAS,KAAM,KAAiB,CAAC,SAAS,EAClE,CAAC;YACD,MAAM,IAAI,GAAG,UAAqB,CAAC;YACnC,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC;YAC9B,WAAW,CAAC,IAAI,EAAE,KAAgB,CAAC,CAAC;YACpC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAE,KAAiB,CAAC,UAAU,CAAC,CAAC,CAAC;YAC3D,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC/B,SAAS;QACX,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,UAAU,CAAC;YACzB,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC;YACpC,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;QACD,UAAU,CAAC,YAAY,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,UAAU,EAAE,CAAC;QAClB,MAAM,KAAK,GAAG,UAAU,CAAC;QACzB,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC;QACpC,KAAK,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* The factory function is called once inside the root scope,
|
|
5
5
|
* so `jsx()` can access the current scope via `getCurrentScope()`.
|
|
6
6
|
*
|
|
7
|
+
* If `container` carries the marker `@praxisjs/ssg` stamps onto prerendered
|
|
8
|
+
* HTML, the tree is built normally into a detached scratch node and then
|
|
9
|
+
* reconciled against the existing DOM (real elements are kept and have their
|
|
10
|
+
* props/listeners replayed onto them; text/comments and any mismatched nodes
|
|
11
|
+
* are recreated) instead of clearing the container and mounting fresh.
|
|
12
|
+
*
|
|
7
13
|
* @example
|
|
8
14
|
* render(() => <App />, document.getElementById('app'));
|
|
9
15
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqDA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,EACnC,SAAS,EAAE,WAAW,GACrB,MAAM,IAAI,CAKZ;AAED,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { mountChildren } from "./children";
|
|
2
2
|
import { runInScope } from "./context";
|
|
3
|
+
import { reconcile } from "./hydration";
|
|
4
|
+
import { runInRecording } from "./hydration-context";
|
|
3
5
|
import { Scope } from "./scope";
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* The factory function is called once inside the root scope,
|
|
8
|
-
* so `jsx()` can access the current scope via `getCurrentScope()`.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* render(() => <App />, document.getElementById('app'));
|
|
12
|
-
*/
|
|
13
|
-
export function render(factory, container) {
|
|
6
|
+
/** Written by `@praxisjs/ssg` onto the mount container in its prerendered HTML. */
|
|
7
|
+
const SSG_MARKER = "data-praxis-ssg";
|
|
8
|
+
function mountCreate(factory, container) {
|
|
14
9
|
const rootScope = new Scope();
|
|
15
10
|
container.innerHTML = "";
|
|
16
11
|
runInScope(rootScope, () => {
|
|
@@ -22,6 +17,46 @@ export function render(factory, container) {
|
|
|
22
17
|
container.innerHTML = "";
|
|
23
18
|
};
|
|
24
19
|
}
|
|
20
|
+
function hydrate(factory, container) {
|
|
21
|
+
const rootScope = new Scope();
|
|
22
|
+
const scratch = document.createDocumentFragment();
|
|
23
|
+
// Build normally — same code as mountCreate, just into a detached scratch
|
|
24
|
+
// node instead of the real container, so it can be walked in document
|
|
25
|
+
// order afterwards (its own construction order is bottom-up, not useful
|
|
26
|
+
// for matching against the server-rendered DOM directly).
|
|
27
|
+
runInRecording(() => {
|
|
28
|
+
runInScope(rootScope, () => {
|
|
29
|
+
const result = factory();
|
|
30
|
+
mountChildren(scratch, result, rootScope);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
reconcile(container, Array.from(scratch.childNodes));
|
|
34
|
+
return () => {
|
|
35
|
+
rootScope.dispose();
|
|
36
|
+
container.innerHTML = "";
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Renders a component tree into a container element.
|
|
41
|
+
*
|
|
42
|
+
* The factory function is called once inside the root scope,
|
|
43
|
+
* so `jsx()` can access the current scope via `getCurrentScope()`.
|
|
44
|
+
*
|
|
45
|
+
* If `container` carries the marker `@praxisjs/ssg` stamps onto prerendered
|
|
46
|
+
* HTML, the tree is built normally into a detached scratch node and then
|
|
47
|
+
* reconciled against the existing DOM (real elements are kept and have their
|
|
48
|
+
* props/listeners replayed onto them; text/comments and any mismatched nodes
|
|
49
|
+
* are recreated) instead of clearing the container and mounting fresh.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* render(() => <App />, document.getElementById('app'));
|
|
53
|
+
*/
|
|
54
|
+
export function render(factory, container) {
|
|
55
|
+
if (container.hasAttribute(SSG_MARKER)) {
|
|
56
|
+
return hydrate(factory, container);
|
|
57
|
+
}
|
|
58
|
+
return mountCreate(factory, container);
|
|
59
|
+
}
|
|
25
60
|
export { Scope } from "./scope";
|
|
26
61
|
export { runInScope, getCurrentScope } from "./context";
|
|
27
62
|
export { mountElement } from "./element";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,mFAAmF;AACnF,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC,SAAS,WAAW,CAClB,OAAmC,EACnC,SAAsB;IAEtB,MAAM,SAAS,GAAG,IAAI,KAAK,EAAE,CAAC;IAC9B,SAAS,CAAC,SAAS,GAAG,EAAE,CAAC;IAEzB,UAAU,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC;QACzB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,SAAS,CAAC,SAAS,GAAG,EAAE,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CACd,OAAmC,EACnC,SAAsB;IAEtB,MAAM,SAAS,GAAG,IAAI,KAAK,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;IAElD,0EAA0E;IAC1E,sEAAsE;IACtE,wEAAwE;IACxE,0DAA0D;IAC1D,cAAc,CAAC,GAAG,EAAE;QAClB,UAAU,CAAC,SAAS,EAAE,GAAG,EAAE;YACzB,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC;YACzB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IAErD,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,SAAS,CAAC,SAAS,GAAG,EAAE,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,MAAM,CACpB,OAAmC,EACnC,SAAsB;IAEtB,IAAI,SAAS,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,OAAO,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACzC,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAoB,MAAM,UAAU,CAAC"}
|
package/dist/portal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../src/portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../src/portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAOpD,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,qBAAa,MAAO,SAAQ,kBAAkB,CAAC,WAAW,CAAC;IACzD,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAG,IAAI,CAAU;IAC9C,MAAM,CAAC,QAAQ,CAAC,aAAa,QAAQ;IAErC,MAAM;CA6BP;AAED,wBAAgB,mBAAmB,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAOhF"}
|
package/dist/portal.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import { StatelessComponent } from "@praxisjs/core";
|
|
2
|
+
import { isServerRenderPass } from "@praxisjs/core/internal";
|
|
2
3
|
import { mountChildren } from "./children";
|
|
3
4
|
import { getCurrentScope } from "./context";
|
|
5
|
+
import { removeNodes } from "./dom/remove";
|
|
4
6
|
export class Portal extends StatelessComponent {
|
|
5
7
|
static __isComponent = true;
|
|
6
8
|
static __isStateless = true;
|
|
7
9
|
render() {
|
|
10
|
+
// Portals escape to an arbitrary external target (default document.body),
|
|
11
|
+
// which would need a hydration cursor shared across every portal mounted at
|
|
12
|
+
// that same target — out of scope for v1. Skip during the server render pass
|
|
13
|
+
// (target content isn't part of the static HTML); the client mounts normally,
|
|
14
|
+
// create-mode, right after hydration completes — no duplication.
|
|
15
|
+
if (isServerRenderPass())
|
|
16
|
+
return null;
|
|
8
17
|
const scope = getCurrentScope();
|
|
9
18
|
const target = resolvePortalTarget(this.props.to);
|
|
10
19
|
if (!target)
|
|
11
20
|
return null;
|
|
12
21
|
const start = document.createComment("");
|
|
13
22
|
const end = document.createComment("");
|
|
14
|
-
target.
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
// Built off-document and attached once, so the target never sees a half-built subtree.
|
|
24
|
+
const fragment = document.createDocumentFragment();
|
|
25
|
+
fragment.appendChild(start);
|
|
26
|
+
mountChildren(fragment, this.props.children, scope);
|
|
27
|
+
fragment.appendChild(end);
|
|
28
|
+
target.appendChild(fragment);
|
|
17
29
|
scope.add(() => {
|
|
18
30
|
if (!start.parentNode || start.parentNode !== end.parentNode)
|
|
19
31
|
return;
|
|
20
|
-
|
|
21
|
-
range.setStartBefore(start);
|
|
22
|
-
range.setEndAfter(end);
|
|
23
|
-
range.deleteContents();
|
|
32
|
+
removeNodes([start, end]);
|
|
24
33
|
});
|
|
25
34
|
return document.createComment("portal");
|
|
26
35
|
}
|
package/dist/portal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.js","sourceRoot":"","sources":["../src/portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"portal.js","sourceRoot":"","sources":["../src/portal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAM3C,MAAM,OAAO,MAAO,SAAQ,kBAA+B;IACzD,MAAM,CAAU,aAAa,GAAG,IAAa,CAAC;IAC9C,MAAM,CAAU,aAAa,GAAG,IAAI,CAAC;IAErC,MAAM;QACJ,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,8EAA8E;QAC9E,iEAAiE;QACjE,IAAI,kBAAkB,EAAE;YAAE,OAAO,IAAI,CAAC;QAEtC,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAEvC,uFAAuF;QACvF,MAAM,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;QACnD,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5B,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpD,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1B,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAE7B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YACb,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU;gBAAE,OAAO;YACrE,WAAW,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;;AAGH,MAAM,UAAU,mBAAmB,CAAC,EAA4B;IAC9D,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;QACf,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QACjD,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IAC9D,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
package/dist/reactive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAmBhC,wBAAgB,aAAa,CAC3B,MAAM,EAAE,IAAI,EACZ,EAAE,EAAE,MAAM,OAAO,EACjB,WAAW,EAAE,KAAK,GACjB,IAAI,CAwCN"}
|
package/dist/reactive.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { runInScope } from "./context";
|
|
2
|
+
import { removeNodes } from "./dom/remove";
|
|
3
|
+
import { isRecording, registerReactiveNode } from "./hydration-context";
|
|
2
4
|
import { Scope } from "./scope";
|
|
3
5
|
function collectNodes(value, out) {
|
|
4
6
|
if (value === null || value === undefined || value === false)
|
|
@@ -36,17 +38,21 @@ export function mountReactive(parent, fn, parentScope) {
|
|
|
36
38
|
const result = runInScope(childScope, fn);
|
|
37
39
|
const newNodes = [];
|
|
38
40
|
collectNodes(result, newNodes);
|
|
41
|
+
// During a hydration build pass, remember that each of these fresh nodes
|
|
42
|
+
// belongs to *this* array — reconcile() mutates `newNodes` in place (by
|
|
43
|
+
// reference, same array `currentNodes` ends up pointing at) if it decides
|
|
44
|
+
// to keep a real element instead of one of them, so future diffs here
|
|
45
|
+
// stay correct.
|
|
46
|
+
if (isRecording()) {
|
|
47
|
+
for (const n of newNodes)
|
|
48
|
+
registerReactiveNode(n, newNodes);
|
|
49
|
+
}
|
|
39
50
|
const anchor = end.parentNode ?? parent;
|
|
40
|
-
if (currentNodes.length
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const range = document.createRange();
|
|
46
|
-
range.setStartBefore(currentNodes[0]);
|
|
47
|
-
range.setEndAfter(currentNodes[currentNodes.length - 1]);
|
|
48
|
-
range.deleteContents();
|
|
49
|
-
}
|
|
51
|
+
if (currentNodes.length === 1) {
|
|
52
|
+
anchor.removeChild(currentNodes[0]);
|
|
53
|
+
}
|
|
54
|
+
else if (currentNodes.length > 1) {
|
|
55
|
+
removeNodes(currentNodes);
|
|
50
56
|
}
|
|
51
57
|
if (newNodes.length > 0) {
|
|
52
58
|
anchor.insertBefore(nodesToFragment(newNodes), end);
|
package/dist/reactive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.js","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,SAAS,YAAY,CAAC,KAAc,EAAE,GAAW;IAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO;IACrE,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACvD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAAC,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IAClF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3D,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC/C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,MAAY,EACZ,EAAiB,EACjB,WAAkB;IAElB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAExB,IAAI,YAAY,GAAW,EAAE,CAAC;IAC9B,IAAI,UAAU,GAAG,IAAI,KAAK,EAAE,CAAC;IAE7B,WAAW,CAAC,MAAM,CAAC,GAAG,EAAE;QACtB,UAAU,CAAC,OAAO,EAAE,CAAC;QACrB,UAAU,GAAG,IAAI,KAAK,EAAE,CAAC;QAEzB,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAW,EAAE,CAAC;QAC5B,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAE/B,
|
|
1
|
+
{"version":3,"file":"reactive.js","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"typescript": "^6.0.3"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@praxisjs/core": "2.
|
|
18
|
-
"@praxisjs/
|
|
19
|
-
"@praxisjs/
|
|
17
|
+
"@praxisjs/core": "2.2.0",
|
|
18
|
+
"@praxisjs/decorators": "1.7.0",
|
|
19
|
+
"@praxisjs/shared": "0.3.1"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc",
|
|
@@ -216,3 +216,61 @@ describe("mountReactive", () => {
|
|
|
216
216
|
document.body.removeChild(parent);
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
describe("fine-grained rows", () => {
|
|
221
|
+
function setup() {
|
|
222
|
+
const ids = signal([1, 2]);
|
|
223
|
+
const titles = signal<Record<number, string>>({ 1: "a", 2: "b" });
|
|
224
|
+
let listRuns = 0;
|
|
225
|
+
const ul = container();
|
|
226
|
+
const scope = new Scope();
|
|
227
|
+
mountChildren(ul, () => {
|
|
228
|
+
listRuns++;
|
|
229
|
+
return ids().map((id) => {
|
|
230
|
+
const li = document.createElement("li");
|
|
231
|
+
mountChildren(li, () => titles()[id], scope);
|
|
232
|
+
return li;
|
|
233
|
+
});
|
|
234
|
+
}, scope);
|
|
235
|
+
return { ids, titles, ul, scope, runs: () => listRuns };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
it("changing row content updates text in place without re-running the list function", () => {
|
|
239
|
+
const { titles, ul, scope, runs } = setup();
|
|
240
|
+
const rows = [...ul.querySelectorAll("li")];
|
|
241
|
+
titles.set({ 1: "A", 2: "b" });
|
|
242
|
+
expect(runs()).toBe(1);
|
|
243
|
+
expect([...ul.querySelectorAll("li")]).toEqual(rows);
|
|
244
|
+
expect(rows.map((r) => r.textContent)).toEqual(["A", "b"]);
|
|
245
|
+
scope.dispose();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("changing the structure rebuilds the list", () => {
|
|
249
|
+
const { ids, titles, ul, scope, runs } = setup();
|
|
250
|
+
titles.set({ 1: "a", 2: "b", 3: "c" });
|
|
251
|
+
ids.set([1, 2, 3]);
|
|
252
|
+
expect(runs()).toBe(2);
|
|
253
|
+
expect(ul.querySelectorAll("li")).toHaveLength(3);
|
|
254
|
+
scope.dispose();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("mountReactive multi-node replacement", () => {
|
|
259
|
+
it("removes nodes that were inserted between the tracked ones after mount", () => {
|
|
260
|
+
const toggle = signal(0);
|
|
261
|
+
const parent = container();
|
|
262
|
+
const scope = new Scope();
|
|
263
|
+
let a!: Node;
|
|
264
|
+
mountChildren(parent, () => {
|
|
265
|
+
void toggle();
|
|
266
|
+
a = document.createElement("a");
|
|
267
|
+
return [a, document.createElement("b")];
|
|
268
|
+
}, scope);
|
|
269
|
+
const stray = document.createElement("stray");
|
|
270
|
+
parent.insertBefore(stray, a.nextSibling);
|
|
271
|
+
toggle.set(1);
|
|
272
|
+
expect(parent.contains(stray)).toBe(false);
|
|
273
|
+
expect([...parent.children].map((c) => c.localName)).toEqual(["a", "b"]);
|
|
274
|
+
scope.dispose();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { StatefulComponent } from "@praxisjs/core";
|
|
5
5
|
import { getComponentProps } from "@praxisjs/core/internal";
|
|
@@ -295,3 +295,69 @@ describe("mountComponent", () => {
|
|
|
295
295
|
scope.dispose();
|
|
296
296
|
});
|
|
297
297
|
});
|
|
298
|
+
|
|
299
|
+
describe("mountComponent onMount scheduling", () => {
|
|
300
|
+
afterEach(() => {
|
|
301
|
+
vi.unstubAllGlobals();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
function stubMicrotasks(): Array<() => void> {
|
|
305
|
+
const jobs: Array<() => void> = [];
|
|
306
|
+
vi.stubGlobal("queueMicrotask", (fn: () => void) => { jobs.push(fn); });
|
|
307
|
+
return jobs;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function lifecycleComp(onMount: () => void) {
|
|
311
|
+
return class extends StatefulComponent {
|
|
312
|
+
static __isComponent = true as const;
|
|
313
|
+
static __isStateless = false;
|
|
314
|
+
onMount() { onMount(); }
|
|
315
|
+
render() { return null; }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
it("drains every component mounted in the same pass with a single microtask, in mount order", () => {
|
|
320
|
+
const jobs = stubMicrotasks();
|
|
321
|
+
const order: string[] = [];
|
|
322
|
+
const scope = new Scope();
|
|
323
|
+
mountComponent(lifecycleComp(() => order.push("a")), {}, scope);
|
|
324
|
+
mountComponent(lifecycleComp(() => order.push("b")), {}, scope);
|
|
325
|
+
expect(jobs).toHaveLength(1);
|
|
326
|
+
jobs.shift()?.();
|
|
327
|
+
expect(order).toEqual(["a", "b"]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("schedules a new microtask for components mounted after a drain", () => {
|
|
331
|
+
const jobs = stubMicrotasks();
|
|
332
|
+
const onMount = vi.fn();
|
|
333
|
+
const scope = new Scope();
|
|
334
|
+
mountComponent(lifecycleComp(onMount), {}, scope);
|
|
335
|
+
jobs.shift()?.();
|
|
336
|
+
mountComponent(lifecycleComp(onMount), {}, scope);
|
|
337
|
+
expect(jobs).toHaveLength(1);
|
|
338
|
+
jobs.shift()?.();
|
|
339
|
+
expect(onMount).toHaveBeenCalledTimes(2);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("a throwing onMount does not stop the others and is rethrown on its own microtask", () => {
|
|
343
|
+
const jobs = stubMicrotasks();
|
|
344
|
+
const second = vi.fn();
|
|
345
|
+
const scope = new Scope();
|
|
346
|
+
mountComponent(lifecycleComp(() => { throw new Error("mount boom"); }), {}, scope);
|
|
347
|
+
mountComponent(lifecycleComp(second), {}, scope);
|
|
348
|
+
jobs.shift()?.();
|
|
349
|
+
expect(second).toHaveBeenCalledOnce();
|
|
350
|
+
expect(jobs).toHaveLength(1);
|
|
351
|
+
expect(() => { jobs.shift()?.(); }).toThrow("mount boom");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("skips onMount for a component disposed before the drain", () => {
|
|
355
|
+
const jobs = stubMicrotasks();
|
|
356
|
+
const onMount = vi.fn();
|
|
357
|
+
const scope = new Scope();
|
|
358
|
+
mountComponent(lifecycleComp(onMount), {}, scope);
|
|
359
|
+
scope.dispose();
|
|
360
|
+
jobs.shift()?.();
|
|
361
|
+
expect(onMount).not.toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
});
|