tungsten 0.2.25 → 0.3.1
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/dist/ContentNode.d.ts +7 -0
- package/dist/ContentNode.d.ts.map +1 -1
- package/dist/Instance/Instance.d.ts.map +1 -1
- package/dist/Instance/Instance.js +7 -5
- package/dist/Instance/gotoRedirect.sxyt.d.ts +2 -0
- package/dist/Instance/gotoRedirect.sxyt.d.ts.map +1 -0
- package/dist/Instance/gotoRedirect.sxyt.js +148 -0
- package/dist/Servers/DevServer.d.ts +7 -0
- package/dist/Servers/DevServer.d.ts.map +1 -1
- package/dist/Servers/DevServer.js +38 -1
- package/dist/Servers/routing.d.ts.map +1 -1
- package/dist/Servers/routing.js +10 -1
- package/dist/build/compileJavascript.d.ts.map +1 -1
- package/dist/build/compileJavascript.js +86 -1
- package/dist/build/compileStyles.d.ts +1 -1
- package/dist/build/compileStyles.d.ts.map +1 -1
- package/dist/build/compileStyles.js +10 -4
- package/dist/build/compileUserApp.d.ts.map +1 -1
- package/dist/build/compileUserApp.js +7 -2
- package/dist/components/RawHtml.d.ts +36 -0
- package/dist/components/RawHtml.d.ts.map +1 -0
- package/dist/components/RawHtml.js +30 -0
- package/dist/components/RawHtml.sxyt.d.ts +2 -0
- package/dist/components/RawHtml.sxyt.d.ts.map +1 -0
- package/dist/components/RawHtml.sxyt.js +104 -0
- package/dist/dev/liveReloadHub.d.ts +6 -0
- package/dist/dev/liveReloadHub.d.ts.map +1 -0
- package/dist/dev/liveReloadHub.js +12 -0
- package/dist/hydrate/hydrate.d.ts.map +1 -1
- package/dist/hydrate/hydrate.js +11 -4
- package/dist/hydrate/reconcileHydratedAttributes.d.ts +24 -0
- package/dist/hydrate/reconcileHydratedAttributes.d.ts.map +1 -0
- package/dist/hydrate/reconcileHydratedAttributes.js +73 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.d.ts +2 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.d.ts.map +1 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.js +173 -0
- package/dist/indexInner.d.ts +4 -0
- package/dist/indexInner.d.ts.map +1 -1
- package/dist/indexInner.js +3 -0
- package/dist/innerHtmlFromProps.d.ts +16 -0
- package/dist/innerHtmlFromProps.d.ts.map +1 -0
- package/dist/innerHtmlFromProps.js +31 -0
- package/dist/innerHtmlFromProps.sxyt.d.ts +2 -0
- package/dist/innerHtmlFromProps.sxyt.d.ts.map +1 -0
- package/dist/innerHtmlFromProps.sxyt.js +99 -0
- package/dist/rawHtml/HtmlAllowList.d.ts +17 -0
- package/dist/rawHtml/HtmlAllowList.d.ts.map +1 -0
- package/dist/rawHtml/HtmlAllowList.js +1 -0
- package/dist/rawHtml/decodeHtmlEntities.d.ts +8 -0
- package/dist/rawHtml/decodeHtmlEntities.d.ts.map +1 -0
- package/dist/rawHtml/decodeHtmlEntities.js +105 -0
- package/dist/rawHtml/htmlParsingTags.d.ts +23 -0
- package/dist/rawHtml/htmlParsingTags.d.ts.map +1 -0
- package/dist/rawHtml/htmlParsingTags.js +55 -0
- package/dist/rawHtml/parseHtml.d.ts +35 -0
- package/dist/rawHtml/parseHtml.d.ts.map +1 -0
- package/dist/rawHtml/parseHtml.js +214 -0
- package/dist/rawHtml/parseHtml.sxyt.d.ts +2 -0
- package/dist/rawHtml/parseHtml.sxyt.d.ts.map +1 -0
- package/dist/rawHtml/parseHtml.sxyt.js +239 -0
- package/dist/rawHtml/safeHtmlAllowList.d.ts +18 -0
- package/dist/rawHtml/safeHtmlAllowList.d.ts.map +1 -0
- package/dist/rawHtml/safeHtmlAllowList.js +47 -0
- package/dist/rawHtml/tokenizeHtml.d.ts +25 -0
- package/dist/rawHtml/tokenizeHtml.d.ts.map +1 -0
- package/dist/rawHtml/tokenizeHtml.js +148 -0
- package/dist/render/processHead.d.ts.map +1 -1
- package/dist/render/processHead.js +10 -0
- package/dist/render/processProps.d.ts.map +1 -1
- package/dist/render/processProps.js +14 -2
- package/dist/render/processProps.sxyt.d.ts +2 -0
- package/dist/render/processProps.sxyt.d.ts.map +1 -0
- package/dist/render/processProps.sxyt.js +90 -0
- package/dist/render/renderingTreeToHtml.d.ts.map +1 -1
- package/dist/render/renderingTreeToHtml.js +8 -0
- package/dist/routing/RouteTypes.d.ts +1 -1
- package/dist/routing/RouteTypes.d.ts.map +1 -1
- package/dist/routing/routeSymbols.d.ts +9 -0
- package/dist/routing/routeSymbols.d.ts.map +1 -0
- package/dist/routing/routeSymbols.js +4 -0
- package/dist/update/integrateUpdateVdom.d.ts.map +1 -1
- package/dist/update/integrateUpdateVdom.js +28 -7
- package/package.json +3 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
2
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
3
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
4
|
+
import { TungstenSimpleComponent } from "../Components.js";
|
|
5
|
+
import { parseHtml } from "../rawHtml/parseHtml.js";
|
|
6
|
+
import { safeHtmlAllowList } from "../rawHtml/safeHtmlAllowList.js";
|
|
7
|
+
/**
|
|
8
|
+
* Renders a string of HTML as real content.
|
|
9
|
+
*
|
|
10
|
+
* The markup is parsed into an ordinary Tungsten tree, so it renders, hydrates
|
|
11
|
+
* and reconciles like any other content — unlike `dangerouslySetInnerHTML`,
|
|
12
|
+
* which hands the browser a region Tungsten cannot see into.
|
|
13
|
+
*
|
|
14
|
+
* Note that the parsed elements are not part of any component's scoped CSS, so
|
|
15
|
+
* component styles reach them only through `:i()` / `:ignore()`.
|
|
16
|
+
*/
|
|
17
|
+
export class RawHtml extends TungstenSimpleComponent {
|
|
18
|
+
constructor(...args) {
|
|
19
|
+
super(...args);
|
|
20
|
+
_defineProperty(this, "content", ({
|
|
21
|
+
html,
|
|
22
|
+
sanitize,
|
|
23
|
+
allowList,
|
|
24
|
+
safe
|
|
25
|
+
}) => parseHtml(html, {
|
|
26
|
+
sanitize,
|
|
27
|
+
allowList: allowList ?? (safe === true ? safeHtmlAllowList : undefined)
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RawHtml.sxyt.d.ts","sourceRoot":"","sources":["../../src/components/RawHtml.sxyt.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { Window } from 'happy-dom';
|
|
3
|
+
import { describe } from 'sxy-test-runner';
|
|
4
|
+
import { RawHtml } from "./RawHtml.js";
|
|
5
|
+
import { createTestApp } from "../testing/index.js";
|
|
6
|
+
// Mounts RawHtml and returns the markup it actually put in the page. The parser
|
|
7
|
+
// itself is covered in rawHtml/parseHtml.sxyt.ts; what is under test here is
|
|
8
|
+
// that the component hands its props to the parser correctly.
|
|
9
|
+
const mount = async props => {
|
|
10
|
+
const window = new Window();
|
|
11
|
+
const testApp = await createTestApp(RawHtml, {
|
|
12
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
13
|
+
document: window.document,
|
|
14
|
+
props
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
return {
|
|
18
|
+
html: testApp.container.innerHTML,
|
|
19
|
+
text: testApp.container.textContent,
|
|
20
|
+
elements: testApp.container.querySelectorAll('*').length
|
|
21
|
+
};
|
|
22
|
+
} finally {
|
|
23
|
+
await testApp.unmount();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const render = async props => (await mount(props)).html;
|
|
27
|
+
describe('RawHtml', ({
|
|
28
|
+
it
|
|
29
|
+
}) => {
|
|
30
|
+
it('renders parsed markup as real elements', async () => {
|
|
31
|
+
const html = await render({
|
|
32
|
+
html: '<p>Bonuses for <strong>this month</strong>.</p>'
|
|
33
|
+
});
|
|
34
|
+
expect(html).to.contain('<p>');
|
|
35
|
+
expect(html).to.contain('<strong>this month</strong>');
|
|
36
|
+
});
|
|
37
|
+
it('renders nothing for empty markup', async () => {
|
|
38
|
+
// a component always leaves its delimiter comments behind, so the check
|
|
39
|
+
// is that it contributed no content, not that the container is empty
|
|
40
|
+
const {
|
|
41
|
+
text,
|
|
42
|
+
elements
|
|
43
|
+
} = await mount({
|
|
44
|
+
html: ''
|
|
45
|
+
});
|
|
46
|
+
expect(text).to.equal('');
|
|
47
|
+
expect(elements).to.equal(0);
|
|
48
|
+
});
|
|
49
|
+
it('drops scripts by default', async () => {
|
|
50
|
+
const html = await render({
|
|
51
|
+
html: '<p>a</p><script>alert(1)</script>'
|
|
52
|
+
});
|
|
53
|
+
expect(html).to.contain('<p>a</p>');
|
|
54
|
+
expect(html).to.not.contain('script');
|
|
55
|
+
expect(html).to.not.contain('alert');
|
|
56
|
+
});
|
|
57
|
+
it('keeps scripts when sanitizing is turned off', async () => {
|
|
58
|
+
expect(await render({
|
|
59
|
+
html: '<div><script>x()</script></div>',
|
|
60
|
+
sanitize: false
|
|
61
|
+
})).to.contain('<script>x()</script>');
|
|
62
|
+
});
|
|
63
|
+
// `safe` is the shortcut for Tungsten's built-in allow list
|
|
64
|
+
it('applies the built-in allow list when safe is set', async () => {
|
|
65
|
+
const html = await render({
|
|
66
|
+
html: '<p style="position:fixed">a</p><iframe src="//evil"></iframe>',
|
|
67
|
+
safe: true
|
|
68
|
+
});
|
|
69
|
+
expect(html).to.contain('a');
|
|
70
|
+
expect(html).to.not.contain('iframe');
|
|
71
|
+
// the safe list excludes style, which can disguise a page without script
|
|
72
|
+
expect(html).to.not.contain('position:fixed');
|
|
73
|
+
});
|
|
74
|
+
it('does not apply an allow list unless asked', async () => {
|
|
75
|
+
const html = await render({
|
|
76
|
+
html: '<p style="position:fixed">a</p>'
|
|
77
|
+
});
|
|
78
|
+
expect(html).to.contain('position:fixed');
|
|
79
|
+
});
|
|
80
|
+
it('lets an explicit allowList win over safe', async () => {
|
|
81
|
+
const onlyEmphasis = {
|
|
82
|
+
tags: ['em'],
|
|
83
|
+
attributes: []
|
|
84
|
+
};
|
|
85
|
+
// <strong> is on the safe list but not on this one, so if allowList is
|
|
86
|
+
// being applied it is unwrapped; if safe won, it would survive
|
|
87
|
+
const html = await render({
|
|
88
|
+
html: '<em>keep</em><strong>unwrap</strong>',
|
|
89
|
+
safe: true,
|
|
90
|
+
allowList: onlyEmphasis
|
|
91
|
+
});
|
|
92
|
+
expect(html).to.contain('<em>keep</em>');
|
|
93
|
+
expect(html).to.not.contain('<strong>');
|
|
94
|
+
expect(html).to.contain('unwrap');
|
|
95
|
+
});
|
|
96
|
+
it('renders whatever it is given without throwing', async () => {
|
|
97
|
+
const malformed = ['<p>unclosed', '</p>stray', '<p class="unterminated>x', '<<>>', '<!-- open'];
|
|
98
|
+
for (const html of malformed) {
|
|
99
|
+
expect(await render({
|
|
100
|
+
html
|
|
101
|
+
})).to.be.a('string');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ReloadEventKind = 'js' | 'css';
|
|
2
|
+
type ReloadListener = (kind: ReloadEventKind) => void;
|
|
3
|
+
export declare function onReloadEvent(listener: ReloadListener): () => void;
|
|
4
|
+
export declare function emitReloadEvent(kind: ReloadEventKind): void;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=liveReloadHub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveReloadHub.d.ts","sourceRoot":"","sources":["../../src/dev/liveReloadHub.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,KAAK,CAAA;AAE1C,KAAK,cAAc,GAAG,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;AAIrD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAGlE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAE3D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A process-wide pub/sub so the JS and CSS watchers (compileJavascript, compileStyles),
|
|
2
|
+
// which run and are started before the dev server exists, can notify DevServer's
|
|
3
|
+
// SSE clients whenever a rebuild completes and the browser should refresh. Dev mode only.
|
|
4
|
+
// This is a plain auto-reload signal, not module-level hot patching.
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
export function onReloadEvent(listener) {
|
|
7
|
+
listeners.add(listener);
|
|
8
|
+
return () => listeners.delete(listener);
|
|
9
|
+
}
|
|
10
|
+
export function emitReloadEvent(kind) {
|
|
11
|
+
for (const listener of listeners) listener(kind);
|
|
12
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../../src/hydrate/hydrate.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAQnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AA0BxD,OAAO,EAAE,yBAAyB,EAAoD,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../../src/hydrate/hydrate.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAQnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AA0BxD,OAAO,EAAE,yBAAyB,EAAoD,MAAM,mBAAmB,CAAA;AAoC/G,wBAAsB,OAAO,CACzB,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,yBAAyB,GAAG,IAAI,EAC/C,cAAc,EAAE,cAAc,iBAyDjC"}
|
package/dist/hydrate/hydrate.js
CHANGED
|
@@ -28,9 +28,10 @@ import { initStores } from 'tungsten/Servers/initStores.js';
|
|
|
28
28
|
import { MatchedRoute } from "../routing/MatchedRoute.js";
|
|
29
29
|
import { isDev } from "../isDev.js";
|
|
30
30
|
import { warn } from "../appOutput.js";
|
|
31
|
+
import { reconcileHydratedAttributes } from "./reconcileHydratedAttributes.js";
|
|
31
32
|
import { jsx as _jsx } from "tungsten/jsxRuntime.js";
|
|
32
33
|
function debug(...texts) {
|
|
33
|
-
|
|
34
|
+
console.log(...texts);
|
|
34
35
|
//console.log(getStackTrace().split('\n')[2])
|
|
35
36
|
}
|
|
36
37
|
function makeDefaultHydrateMeta() {
|
|
@@ -546,11 +547,17 @@ async function hydrateNodeComponent(node, parentElement, domPointer, instance, m
|
|
|
546
547
|
// export type HTMLBindableElement = HTMLButtonElement | HTMLDataElement | HTMLInputElement | HTMLLIElement
|
|
547
548
|
// | HTMLMeterElement | HTMLOptionElement | HTMLParamElement | HTMLProgressElement | HTMLSelectElement | HTMLTextAreaElement
|
|
548
549
|
function hydrateAttributes(node, domNode) {
|
|
549
|
-
// !!! We should check whether attributes match here
|
|
550
|
-
// Show errors in dev mode
|
|
551
|
-
// Auto fix in prod?
|
|
552
550
|
// handle bind attributes and merge together event handlers
|
|
553
551
|
processAttributes(node);
|
|
552
|
+
// Bring the server's attributes into line with what this render produced.
|
|
553
|
+
// Runs in production as well as development: leaving a known-wrong value in
|
|
554
|
+
// place is not better in production, it is only quieter. Only genuine
|
|
555
|
+
// differences are written, so a page that matches costs reads and no
|
|
556
|
+
// writes. The report is dev-only, because a mismatch is a bug in the
|
|
557
|
+
// application's rendering rather than something a user can act on.
|
|
558
|
+
if (ContentNodeElementIsHtml(node)) {
|
|
559
|
+
reconcileHydratedAttributes(node, domNode, isDev ? mismatch => warn(`hydration mismatch on <${node[$type]}>:` + ` ${mismatch.name} rendered as ${JSON.stringify(mismatch.expected)}` + ` but the server sent ${JSON.stringify(mismatch.actual)}.` + ` The rendered value has been applied.`) : undefined);
|
|
560
|
+
}
|
|
554
561
|
debug('--\nhydrateAttributes', domNode);
|
|
555
562
|
for (const key in node[$props]) {
|
|
556
563
|
if (StringIsHtmlEvent(key)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ContentNodeHtml } from '../ContentNode.js';
|
|
2
|
+
export type AttributeMismatch = {
|
|
3
|
+
name: string;
|
|
4
|
+
/** The value the client rendered, or null when it renders no such attribute. */
|
|
5
|
+
expected: string | null;
|
|
6
|
+
/** The value the server's markup carried, or null when it carried none. */
|
|
7
|
+
actual: string | null;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Brings a server-rendered element's attributes into line with what the client
|
|
11
|
+
* has just rendered, and reports every difference.
|
|
12
|
+
*
|
|
13
|
+
* Hydration attaches to markup the server produced. Any attribute whose value
|
|
14
|
+
* the client works out differently — a stored preference, a media query, a time
|
|
15
|
+
* zone — would otherwise keep the server's value for the life of the page,
|
|
16
|
+
* because nothing else revisits it until an unrelated update happens to
|
|
17
|
+
* reconcile that element.
|
|
18
|
+
*
|
|
19
|
+
* Only genuine differences are written. The comparison runs the props through
|
|
20
|
+
* `processProps()`, the same function that produced the markup being compared
|
|
21
|
+
* against, so a matching element costs one attribute read each and no writes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reconcileHydratedAttributes(node: ContentNodeHtml, domNode: HTMLElement, onMismatch?: (mismatch: AttributeMismatch) => void): void;
|
|
24
|
+
//# sourceMappingURL=reconcileHydratedAttributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcileHydratedAttributes.d.ts","sourceRoot":"","sources":["../../src/hydrate/reconcileHydratedAttributes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAIxD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,CAAA;AA6BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACvC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,WAAW,EACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,GACnD,IAAI,CAkCN"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { $props } from "../contentNodeSymbols.js";
|
|
2
|
+
import { processProps } from "../render/processProps.js";
|
|
3
|
+
/**
|
|
4
|
+
* The attribute value a render produces for a prop, or null for "no attribute".
|
|
5
|
+
*
|
|
6
|
+
* These are the rules both renderers already use — `renderingNodeToHtml()` on
|
|
7
|
+
* the server and `addAttributes()` on the client — so that a value which
|
|
8
|
+
* round-trips unchanged compares equal here.
|
|
9
|
+
*/
|
|
10
|
+
function attributeValue(value) {
|
|
11
|
+
if (typeof value === 'boolean') return value ? '' : null;
|
|
12
|
+
if (value === undefined || value === null) return null;
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
function removeAttribute(domNode, name) {
|
|
16
|
+
// checked is both an attribute and live state, and the attribute alone only
|
|
17
|
+
// sets the default. Clear both, or the box stays ticked.
|
|
18
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
19
|
+
if (name === 'checked') domNode.checked = false;
|
|
20
|
+
domNode.removeAttribute(name);
|
|
21
|
+
}
|
|
22
|
+
function setAttribute(domNode, name, value) {
|
|
23
|
+
domNode.setAttribute(name, value);
|
|
24
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
25
|
+
if (name === 'checked') domNode.checked = true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Brings a server-rendered element's attributes into line with what the client
|
|
29
|
+
* has just rendered, and reports every difference.
|
|
30
|
+
*
|
|
31
|
+
* Hydration attaches to markup the server produced. Any attribute whose value
|
|
32
|
+
* the client works out differently — a stored preference, a media query, a time
|
|
33
|
+
* zone — would otherwise keep the server's value for the life of the page,
|
|
34
|
+
* because nothing else revisits it until an unrelated update happens to
|
|
35
|
+
* reconcile that element.
|
|
36
|
+
*
|
|
37
|
+
* Only genuine differences are written. The comparison runs the props through
|
|
38
|
+
* `processProps()`, the same function that produced the markup being compared
|
|
39
|
+
* against, so a matching element costs one attribute read each and no writes.
|
|
40
|
+
*/
|
|
41
|
+
export function reconcileHydratedAttributes(node, domNode, onMismatch) {
|
|
42
|
+
// A copy: processProps() rewrites what it is given, and these props stay in
|
|
43
|
+
// the content tree for the next update to diff against.
|
|
44
|
+
const expected = processProps({
|
|
45
|
+
...node[$props]
|
|
46
|
+
});
|
|
47
|
+
const handled = new Set();
|
|
48
|
+
for (const name in expected) {
|
|
49
|
+
handled.add(name.toLowerCase());
|
|
50
|
+
const value = attributeValue(expected[name]);
|
|
51
|
+
const actual = domNode.getAttribute(name);
|
|
52
|
+
if (value === actual) continue;
|
|
53
|
+
if (value === null) removeAttribute(domNode, name);else setAttribute(domNode, name, value);
|
|
54
|
+
onMismatch?.({
|
|
55
|
+
name,
|
|
56
|
+
expected: value,
|
|
57
|
+
actual
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Anything the markup carries that this render does not produce. The list
|
|
61
|
+
// is live, so the names are taken before any of them are removed.
|
|
62
|
+
const present = [...domNode.attributes].map(attribute => attribute.name);
|
|
63
|
+
for (const name of present) {
|
|
64
|
+
if (handled.has(name.toLowerCase())) continue;
|
|
65
|
+
const actual = domNode.getAttribute(name);
|
|
66
|
+
removeAttribute(domNode, name);
|
|
67
|
+
onMismatch?.({
|
|
68
|
+
name,
|
|
69
|
+
expected: null,
|
|
70
|
+
actual
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcileHydratedAttributes.sxyt.d.ts","sourceRoot":"","sources":["../../src/hydrate/reconcileHydratedAttributes.sxyt.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe } from 'sxy-test-runner';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { Window } from 'happy-dom';
|
|
4
|
+
import { reconcileHydratedAttributes } from "./reconcileHydratedAttributes.js";
|
|
5
|
+
import { $props, $type } from "../contentNodeSymbols.js";
|
|
6
|
+
const window = new Window();
|
|
7
|
+
const node = (type, props) => ({
|
|
8
|
+
[$type]: type,
|
|
9
|
+
[$props]: props
|
|
10
|
+
});
|
|
11
|
+
/** Builds an element from markup the server could have produced. */
|
|
12
|
+
const element = html => {
|
|
13
|
+
const host = window.document.createElement('div');
|
|
14
|
+
host.innerHTML = html;
|
|
15
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
16
|
+
return host.firstElementChild;
|
|
17
|
+
};
|
|
18
|
+
const attributesOf = domNode => Object.fromEntries([...domNode.attributes].map(attribute => [attribute.name, attribute.value]));
|
|
19
|
+
/** Reconciles and returns what changed. */
|
|
20
|
+
const reconcile = (type, props, html) => {
|
|
21
|
+
const domNode = element(html);
|
|
22
|
+
const mismatches = [];
|
|
23
|
+
reconcileHydratedAttributes(node(type, props), domNode, mismatch => mismatches.push(mismatch));
|
|
24
|
+
return {
|
|
25
|
+
domNode,
|
|
26
|
+
mismatches,
|
|
27
|
+
attributes: attributesOf(domNode)
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
describe('reconcileHydratedAttributes()', ({
|
|
31
|
+
it
|
|
32
|
+
}) => {
|
|
33
|
+
it('patches an attribute whose rendered value differs from the markup', () => {
|
|
34
|
+
const {
|
|
35
|
+
attributes,
|
|
36
|
+
mismatches
|
|
37
|
+
} = reconcile('div', {
|
|
38
|
+
style: {
|
|
39
|
+
backgroundColor: '#123456'
|
|
40
|
+
}
|
|
41
|
+
}, '<div style="background-color: #bae3ff;"></div>');
|
|
42
|
+
expect(attributes.style).to.equal('background-color: #123456;');
|
|
43
|
+
expect(mismatches.map(mismatch => mismatch.name)).to.deep.equal(['style']);
|
|
44
|
+
});
|
|
45
|
+
it('leaves a matching attribute untouched and reports nothing', () => {
|
|
46
|
+
const {
|
|
47
|
+
attributes,
|
|
48
|
+
mismatches
|
|
49
|
+
} = reconcile('input', {
|
|
50
|
+
name: 'ref',
|
|
51
|
+
title: 'Reference'
|
|
52
|
+
}, '<input name="ref" title="Reference">');
|
|
53
|
+
expect(attributes).to.deep.equal({
|
|
54
|
+
name: 'ref',
|
|
55
|
+
title: 'Reference'
|
|
56
|
+
});
|
|
57
|
+
expect(mismatches).to.deep.equal([]);
|
|
58
|
+
});
|
|
59
|
+
it('adds an attribute the markup is missing', () => {
|
|
60
|
+
const {
|
|
61
|
+
attributes
|
|
62
|
+
} = reconcile('a', {
|
|
63
|
+
href: '/x',
|
|
64
|
+
title: 't'
|
|
65
|
+
}, '<a href="/x"></a>');
|
|
66
|
+
expect(attributes).to.deep.equal({
|
|
67
|
+
href: '/x',
|
|
68
|
+
title: 't'
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('removes an attribute the render no longer produces', () => {
|
|
72
|
+
const {
|
|
73
|
+
attributes,
|
|
74
|
+
mismatches
|
|
75
|
+
} = reconcile('a', {
|
|
76
|
+
href: '/x'
|
|
77
|
+
}, '<a href="/x" title="gone"></a>');
|
|
78
|
+
expect(attributes).to.deep.equal({
|
|
79
|
+
href: '/x'
|
|
80
|
+
});
|
|
81
|
+
expect(mismatches.map(mismatch => mismatch.name)).to.deep.equal(['title']);
|
|
82
|
+
});
|
|
83
|
+
it('treats a true boolean as a present, empty attribute', () => {
|
|
84
|
+
expect(reconcile('button', {
|
|
85
|
+
disabled: true
|
|
86
|
+
}, '<button></button>').attributes).to.deep.equal({
|
|
87
|
+
disabled: ''
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it('removes an attribute whose rendered value is false, null or undefined', () => {
|
|
91
|
+
expect(reconcile('button', {
|
|
92
|
+
disabled: false
|
|
93
|
+
}, '<button disabled></button>').attributes).to.deep.equal({});
|
|
94
|
+
expect(reconcile('a', {
|
|
95
|
+
href: null
|
|
96
|
+
}, '<a href="/x"></a>').attributes).to.deep.equal({});
|
|
97
|
+
expect(reconcile('a', {
|
|
98
|
+
href: undefined
|
|
99
|
+
}, '<a href="/x"></a>').attributes).to.deep.equal({});
|
|
100
|
+
});
|
|
101
|
+
// The comparison has to apply the same transformation the server used, or
|
|
102
|
+
// every element looks mismatched and the whole document gets rewritten.
|
|
103
|
+
it('applies the server prop transform, so className compares against class', () => {
|
|
104
|
+
const {
|
|
105
|
+
attributes,
|
|
106
|
+
mismatches
|
|
107
|
+
} = reconcile('div', {
|
|
108
|
+
className: 'frame Qtc_frame'
|
|
109
|
+
}, '<div class="frame Qtc_frame"></div>');
|
|
110
|
+
expect(attributes).to.deep.equal({
|
|
111
|
+
class: 'frame Qtc_frame'
|
|
112
|
+
});
|
|
113
|
+
expect(mismatches).to.deep.equal([]);
|
|
114
|
+
});
|
|
115
|
+
it('ignores event handlers rather than writing them as attributes', () => {
|
|
116
|
+
const {
|
|
117
|
+
attributes,
|
|
118
|
+
mismatches
|
|
119
|
+
} = reconcile('input', {
|
|
120
|
+
name: 'a',
|
|
121
|
+
onInput: () => undefined,
|
|
122
|
+
onClick: () => undefined
|
|
123
|
+
}, '<input name="a">');
|
|
124
|
+
expect(attributes).to.deep.equal({
|
|
125
|
+
name: 'a'
|
|
126
|
+
});
|
|
127
|
+
expect(mismatches).to.deep.equal([]);
|
|
128
|
+
});
|
|
129
|
+
it('ignores props that are instructions to Tungsten', () => {
|
|
130
|
+
const {
|
|
131
|
+
attributes,
|
|
132
|
+
mismatches
|
|
133
|
+
} = reconcile('div', {
|
|
134
|
+
__opaque: true,
|
|
135
|
+
dangerouslySetInnerHTML: {
|
|
136
|
+
__html: '<b>x</b>'
|
|
137
|
+
},
|
|
138
|
+
id: 'd'
|
|
139
|
+
}, '<div id="d"></div>');
|
|
140
|
+
expect(attributes).to.deep.equal({
|
|
141
|
+
id: 'd'
|
|
142
|
+
});
|
|
143
|
+
expect(mismatches).to.deep.equal([]);
|
|
144
|
+
});
|
|
145
|
+
it('reports what changed, for a dev-mode warning', () => {
|
|
146
|
+
const {
|
|
147
|
+
mismatches
|
|
148
|
+
} = reconcile('a', {
|
|
149
|
+
href: '/new'
|
|
150
|
+
}, '<a href="/old" title="stale"></a>');
|
|
151
|
+
expect(mismatches).to.deep.include({
|
|
152
|
+
name: 'href',
|
|
153
|
+
expected: '/new',
|
|
154
|
+
actual: '/old'
|
|
155
|
+
});
|
|
156
|
+
expect(mismatches).to.deep.include({
|
|
157
|
+
name: 'title',
|
|
158
|
+
expected: null,
|
|
159
|
+
actual: 'stale'
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
it('clears the checked property when removing a checked attribute', () => {
|
|
163
|
+
const domNode = element('<input type="checkbox" checked>');
|
|
164
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
165
|
+
const checkbox = domNode;
|
|
166
|
+
expect(checkbox.checked).to.equal(true);
|
|
167
|
+
reconcileHydratedAttributes(node('input', {
|
|
168
|
+
type: 'checkbox'
|
|
169
|
+
}), domNode);
|
|
170
|
+
expect(checkbox.checked).to.equal(false);
|
|
171
|
+
expect(domNode.hasAttribute('checked')).to.equal(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
package/dist/indexInner.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export * from './components/bindable/BindableTextarea.jsx';
|
|
|
16
16
|
export * from './components/GoToComponent.jsx';
|
|
17
17
|
export * from './components/Lazy.js';
|
|
18
18
|
export * from './components/Link.jsx';
|
|
19
|
+
export * from './components/RawHtml.jsx';
|
|
19
20
|
export * from './components/RouteRedirectComponent.jsx';
|
|
21
|
+
export { parseHtml, type ParseHtmlOptions } from './rawHtml/parseHtml.js';
|
|
22
|
+
export { safeHtmlAllowList } from './rawHtml/safeHtmlAllowList.js';
|
|
23
|
+
export type { HtmlAllowList } from './rawHtml/HtmlAllowList.js';
|
|
20
24
|
export * from './components/WebRedirectComponent.jsx';
|
|
21
25
|
export * from './nbsp.js';
|
|
22
26
|
export * from './browserStorage.js';
|
package/dist/indexInner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexInner.d.ts","sourceRoot":"","sources":["../src/indexInner.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"indexInner.d.ts","sourceRoot":"","sources":["../src/indexInner.ts"],"names":[],"mappings":"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"}
|
package/dist/indexInner.js
CHANGED
|
@@ -21,7 +21,10 @@ export * from "./components/bindable/BindableTextarea.js";
|
|
|
21
21
|
export * from "./components/GoToComponent.js";
|
|
22
22
|
export * from "./components/Lazy.js";
|
|
23
23
|
export * from "./components/Link.js";
|
|
24
|
+
export * from "./components/RawHtml.js";
|
|
24
25
|
export * from "./components/RouteRedirectComponent.js";
|
|
26
|
+
export { parseHtml } from "./rawHtml/parseHtml.js";
|
|
27
|
+
export { safeHtmlAllowList } from "./rawHtml/safeHtmlAllowList.js";
|
|
25
28
|
export * from "./components/WebRedirectComponent.js";
|
|
26
29
|
export * from "./nbsp.js";
|
|
27
30
|
export * from "./browserStorage.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NodeProps } from './ContentNode.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads React's `dangerouslySetInnerHTML` prop, returning the raw markup, or
|
|
4
|
+
* `null` when the element does not use it.
|
|
5
|
+
*
|
|
6
|
+
* The markup is the element's entire inner content: the server writes it into
|
|
7
|
+
* its HTML verbatim, the client assigns it to `innerHTML`, and the nodes the
|
|
8
|
+
* parser produces from it are never reconciled. Tungsten holds one opaque
|
|
9
|
+
* string where the DOM holds a whole subtree, so reconciling them would
|
|
10
|
+
* corrupt the document.
|
|
11
|
+
*
|
|
12
|
+
* `hasChildren` is supplied by the caller because the content and rendering
|
|
13
|
+
* trees test their children differently.
|
|
14
|
+
*/
|
|
15
|
+
export declare function innerHtmlFromProps(type: string, props: NodeProps, hasChildren: boolean): string | null;
|
|
16
|
+
//# sourceMappingURL=innerHtmlFromProps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"innerHtmlFromProps.d.ts","sourceRoot":"","sources":["../src/innerHtmlFromProps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAGjD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,GACrD,MAAM,GAAG,IAAI,CA8Bf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { throwError } from "./throwError.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads React's `dangerouslySetInnerHTML` prop, returning the raw markup, or
|
|
4
|
+
* `null` when the element does not use it.
|
|
5
|
+
*
|
|
6
|
+
* The markup is the element's entire inner content: the server writes it into
|
|
7
|
+
* its HTML verbatim, the client assigns it to `innerHTML`, and the nodes the
|
|
8
|
+
* parser produces from it are never reconciled. Tungsten holds one opaque
|
|
9
|
+
* string where the DOM holds a whole subtree, so reconciling them would
|
|
10
|
+
* corrupt the document.
|
|
11
|
+
*
|
|
12
|
+
* `hasChildren` is supplied by the caller because the content and rendering
|
|
13
|
+
* trees test their children differently.
|
|
14
|
+
*/
|
|
15
|
+
export function innerHtmlFromProps(type, props, hasChildren) {
|
|
16
|
+
const prop = props.dangerouslySetInnerHTML;
|
|
17
|
+
if (prop === undefined || prop === null) return null;
|
|
18
|
+
if (typeof prop !== 'object' || !('__html' in prop)) {
|
|
19
|
+
throwError('innerHtmlFromProps', `<${type}> dangerouslySetInnerHTML must be an object of the form { __html: '...' }.`);
|
|
20
|
+
}
|
|
21
|
+
const html = prop.__html;
|
|
22
|
+
if (html === undefined || html === null) {
|
|
23
|
+
throwError('innerHtmlFromProps', `<${type}> dangerouslySetInnerHTML.__html is ${String(html)}.`);
|
|
24
|
+
}
|
|
25
|
+
if (hasChildren) {
|
|
26
|
+
throwError('innerHtmlFromProps', `<${type}> has both dangerouslySetInnerHTML and children.` + ` An element's content must come from one or the other.`);
|
|
27
|
+
}
|
|
28
|
+
// TrustedHTML stringifies to its markup, so this accepts both forms the
|
|
29
|
+
// JSX typings allow.
|
|
30
|
+
return String(html);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"innerHtmlFromProps.sxyt.d.ts","sourceRoot":"","sources":["../src/innerHtmlFromProps.sxyt.ts"],"names":[],"mappings":""}
|