@b9g/crank 0.6.0 → 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/dom.cjs CHANGED
@@ -1,12 +1,53 @@
1
1
  'use strict';
2
2
 
3
3
  var crank = require('./crank.cjs');
4
+ require('./event-target.cjs');
4
5
 
5
6
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
6
- const impl = {
7
- scope(xmlns, tag, props) {
7
+ function isWritableProperty(element, name) {
8
+ // walk up the object's prototype chain to find the owner
9
+ let propOwner = element;
10
+ do {
11
+ if (Object.prototype.hasOwnProperty.call(propOwner, name)) {
12
+ break;
13
+ }
14
+ } while ((propOwner = Object.getPrototypeOf(propOwner)));
15
+ if (propOwner === null) {
16
+ return false;
17
+ }
18
+ // get the descriptor for the named property and check whether it implies
19
+ // that the property is writable
20
+ const descriptor = Object.getOwnPropertyDescriptor(propOwner, name);
21
+ if (descriptor != null &&
22
+ (descriptor.writable === true || descriptor.set !== undefined)) {
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ function emitHydrationWarning(propName, quietProps, expectedValue, actualValue, element, displayName) {
28
+ const checkName = propName;
29
+ const showName = displayName || propName;
30
+ if (!quietProps || !quietProps.has(checkName)) {
31
+ if (expectedValue === null || expectedValue === false) {
32
+ console.warn(`Expected "${showName}" to be missing but found ${String(actualValue)} while hydrating:`, element);
33
+ }
34
+ else if (expectedValue === true || expectedValue === "") {
35
+ console.warn(`Expected "${showName}" to be ${expectedValue === true ? "present" : '""'} but found ${String(actualValue)} while hydrating:`, element);
36
+ }
37
+ else if (typeof window !== "undefined" &&
38
+ window.location &&
39
+ new URL(expectedValue, window.location.origin).href ===
40
+ new URL(actualValue, window.location.origin).href) ;
41
+ else {
42
+ console.warn(`Expected "${showName}" to be "${String(expectedValue)}" but found ${String(actualValue)} while hydrating:`, element);
43
+ }
44
+ }
45
+ }
46
+ const adapter = {
47
+ scope({ scope: xmlns, tag, props, }) {
8
48
  switch (tag) {
9
49
  case crank.Portal:
50
+ // TODO: read the namespace from the portal root element
10
51
  xmlns = undefined;
11
52
  break;
12
53
  case "svg":
@@ -15,9 +56,9 @@ const impl = {
15
56
  }
16
57
  return props.xmlns || xmlns;
17
58
  },
18
- create(tag, _props, xmlns) {
59
+ create({ tag, tagName, scope: xmlns, }) {
19
60
  if (typeof tag !== "string") {
20
- throw new Error(`Unknown tag: ${tag.toString()}`);
61
+ throw new Error(`Unknown tag: ${tagName}`);
21
62
  }
22
63
  else if (tag.toLowerCase() === "svg") {
23
64
  xmlns = SVG_NAMESPACE;
@@ -26,262 +67,362 @@ const impl = {
26
67
  ? document.createElementNS(xmlns, tag)
27
68
  : document.createElement(tag);
28
69
  },
29
- hydrate(tag, node, props) {
70
+ adopt({ tag, tagName, node, }) {
30
71
  if (typeof tag !== "string" && tag !== crank.Portal) {
31
- throw new Error(`Unknown tag: ${tag.toString()}`);
72
+ throw new Error(`Unknown tag: ${tagName}`);
32
73
  }
33
- if (typeof tag === "string" &&
34
- tag.toUpperCase() !== node.tagName) {
35
- // TODO: consider pros and cons of hydration warnings
36
- //console.error(`Expected <${tag}> while hydrating but found:`, node);
37
- return undefined;
74
+ if (node === document.body ||
75
+ node === document.head ||
76
+ node === document.documentElement ||
77
+ node === document) {
78
+ console.warn(`Hydrating ${node.nodeName.toLowerCase()} is discouraged as it is destructive and may remove unknown nodes.`);
38
79
  }
39
- const children = [];
40
- for (let i = 0; i < node.childNodes.length; i++) {
41
- const child = node.childNodes[i];
42
- if (child.nodeType === Node.TEXT_NODE) {
43
- children.push(child.data);
44
- }
45
- else if (child.nodeType === Node.ELEMENT_NODE) {
46
- children.push(child);
47
- }
80
+ if (node == null ||
81
+ (typeof tag === "string" &&
82
+ (node.nodeType !== Node.ELEMENT_NODE ||
83
+ tag.toLowerCase() !== node.tagName.toLowerCase()))) {
84
+ console.warn(`Expected <${tagName}> while hydrating but found: `, node);
85
+ return;
48
86
  }
49
- // TODO: extract props from nodes
50
- return { props, children };
87
+ return Array.from(node.childNodes);
51
88
  },
52
- patch(_tag,
53
- // TODO: Why does this assignment work?
54
- node, name,
55
- // TODO: Stricter typings?
56
- value, oldValue, xmlns) {
89
+ patch({ tagName, node, props, oldProps, scope: xmlns, copyProps, quietProps, isHydrating, }) {
90
+ if (node.nodeType !== Node.ELEMENT_NODE) {
91
+ throw new TypeError(`Cannot patch node: ${String(node)}`);
92
+ }
93
+ else if (props.class && props.className) {
94
+ console.error(`Both "class" and "className" set in props for <${tagName}>. Use one or the other.`);
95
+ }
96
+ const element = node;
57
97
  const isSVG = xmlns === SVG_NAMESPACE;
58
- switch (name) {
59
- case "style": {
60
- const style = node.style;
61
- if (style == null) {
62
- node.setAttribute("style", value);
63
- }
64
- else if (value == null || value === false) {
65
- node.removeAttribute("style");
98
+ for (let name in { ...oldProps, ...props }) {
99
+ let value = props[name];
100
+ const oldValue = oldProps ? oldProps[name] : undefined;
101
+ {
102
+ if (copyProps != null && copyProps.has(name)) {
103
+ continue;
66
104
  }
67
- else if (value === true) {
68
- node.setAttribute("style", "");
69
- }
70
- else if (typeof value === "string") {
71
- if (style.cssText !== value) {
72
- style.cssText = value;
105
+ // handle prop:name or attr:name properties
106
+ const colonIndex = name.indexOf(":");
107
+ if (colonIndex !== -1) {
108
+ const [ns, name1] = [
109
+ name.slice(0, colonIndex),
110
+ name.slice(colonIndex + 1),
111
+ ];
112
+ switch (ns) {
113
+ case "prop":
114
+ node[name1] = value;
115
+ continue;
116
+ case "attr":
117
+ if (value == null || value === false) {
118
+ if (isHydrating && element.hasAttribute(name1)) {
119
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
120
+ }
121
+ element.removeAttribute(name1);
122
+ }
123
+ else if (value === true) {
124
+ if (isHydrating && !element.hasAttribute(name1)) {
125
+ emitHydrationWarning(name, quietProps, value, null, element);
126
+ }
127
+ element.setAttribute(name1, "");
128
+ }
129
+ else if (typeof value !== "string") {
130
+ value = String(value);
131
+ }
132
+ if (isHydrating && element.getAttribute(name1) !== value) {
133
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
134
+ }
135
+ element.setAttribute(name1, String(value));
136
+ continue;
73
137
  }
74
138
  }
75
- else {
76
- if (typeof oldValue === "string") {
77
- style.cssText = "";
139
+ }
140
+ switch (name) {
141
+ // TODO: fix hydration warnings for the style prop
142
+ case "style": {
143
+ const style = element.style;
144
+ if (value == null || value === false) {
145
+ if (isHydrating && style.cssText !== "") {
146
+ emitHydrationWarning(name, quietProps, value, style.cssText, element);
147
+ }
148
+ element.removeAttribute("style");
78
149
  }
79
- for (const styleName in { ...oldValue, ...value }) {
80
- const styleValue = value && value[styleName];
81
- if (styleValue == null) {
82
- style.removeProperty(styleName);
150
+ else if (value === true) {
151
+ if (isHydrating && style.cssText !== "") {
152
+ emitHydrationWarning(name, quietProps, "", style.cssText, element);
83
153
  }
84
- else if (style.getPropertyValue(styleName) !== styleValue) {
85
- style.setProperty(styleName, styleValue);
154
+ element.setAttribute("style", "");
155
+ }
156
+ else if (typeof value === "string") {
157
+ if (style.cssText !== value) {
158
+ // TODO: Fix hydration warnings for styles
159
+ //if (isHydrating) {
160
+ // emitHydrationWarning(
161
+ // name,
162
+ // quietProps,
163
+ // value,
164
+ // style.cssText,
165
+ // element,
166
+ // );
167
+ //}
168
+ style.cssText = value;
86
169
  }
87
170
  }
88
- }
89
- break;
90
- }
91
- case "class":
92
- case "className":
93
- if (value === true) {
94
- node.setAttribute("class", "");
95
- }
96
- else if (value == null) {
97
- node.removeAttribute("class");
98
- }
99
- else if (!isSVG) {
100
- if (node.className !== value) {
101
- node["className"] = value;
171
+ else {
172
+ if (typeof oldValue === "string") {
173
+ // if the old value was a string, we need to clear the style
174
+ // TODO: only clear the styles enumerated in the old value
175
+ style.cssText = "";
176
+ }
177
+ for (const styleName in { ...oldValue, ...value }) {
178
+ const styleValue = value && value[styleName];
179
+ if (styleValue == null) {
180
+ if (isHydrating && style.getPropertyValue(styleName) !== "") {
181
+ emitHydrationWarning(name, quietProps, null, style.getPropertyValue(styleName), element, `style.${styleName}`);
182
+ }
183
+ style.removeProperty(styleName);
184
+ }
185
+ else if (style.getPropertyValue(styleName) !== styleValue) {
186
+ // TODO: hydration warnings for style props
187
+ //if (isHydrating) {
188
+ // emitHydrationWarning(
189
+ // name,
190
+ // quietProps,
191
+ // styleValue,
192
+ // style.getPropertyValue(styleName),
193
+ // element,
194
+ // `style.${styleName}`,
195
+ // );
196
+ //}
197
+ style.setProperty(styleName, styleValue);
198
+ }
199
+ }
102
200
  }
201
+ break;
103
202
  }
104
- else if (node.getAttribute("class") !== value) {
105
- node.setAttribute("class", value);
106
- }
107
- break;
108
- case "innerHTML":
109
- if (value !== oldValue) {
110
- node.innerHTML = value;
111
- }
112
- break;
113
- default: {
114
- if (name[0] === "o" &&
115
- name[1] === "n" &&
116
- name[2] === name[2].toUpperCase() &&
117
- typeof value === "function") {
118
- // Support React-style event names (onClick, onChange, etc.)
119
- name = name.toLowerCase();
120
- }
121
- if (name in node &&
122
- // boolean properties will coerce strings, but sometimes they map to
123
- // enumerated attributes, where truthy strings ("false", "no") map to
124
- // falsy properties, so we use attributes in this case.
125
- !(typeof value === "string" &&
126
- typeof node[name] === "boolean")) {
127
- // walk up the object's prototype chain to find the owner of the
128
- // named property
129
- let obj = node;
130
- do {
131
- if (Object.prototype.hasOwnProperty.call(obj, name)) {
132
- break;
203
+ case "class":
204
+ case "className":
205
+ if (value === true) {
206
+ if (isHydrating && element.getAttribute("class") !== "") {
207
+ emitHydrationWarning(name, quietProps, "", element.getAttribute("class"), element);
133
208
  }
134
- } while ((obj = Object.getPrototypeOf(obj)));
135
- // get the descriptor for the named property and check whether it
136
- // implies that the property is writable
137
- const descriptor = Object.getOwnPropertyDescriptor(obj, name);
138
- if (descriptor != null &&
139
- (descriptor.writable === true || descriptor.set !== undefined)) {
140
- if (node[name] !== value || oldValue === undefined) {
141
- node[name] = value;
209
+ element.setAttribute("class", "");
210
+ }
211
+ else if (value == null) {
212
+ if (isHydrating && element.hasAttribute("class")) {
213
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
142
214
  }
143
- return;
215
+ element.removeAttribute("class");
144
216
  }
145
- // if the property wasn't writable, fall through to the code below
146
- // which uses setAttribute() instead of assigning directly.
147
- }
148
- if (value === true) {
149
- value = "";
150
- }
151
- else if (value == null || value === false) {
152
- node.removeAttribute(name);
153
- return;
154
- }
155
- if (node.getAttribute(name) !== value) {
156
- node.setAttribute(name, value);
157
- }
158
- }
159
- }
160
- },
161
- arrange(tag, node, props, children, _oldProps, oldChildren) {
162
- if (tag === crank.Portal && (node == null || typeof node.nodeType !== "number")) {
163
- throw new TypeError(`Portal root is not a node. Received: ${JSON.stringify(node && node.toString())}`);
164
- }
165
- if (!("innerHTML" in props) &&
166
- // We don’t want to update elements without explicit children (<div/>),
167
- // because these elements sometimes have child nodes added via raw
168
- // DOM manipulations.
169
- // However, if an element has previously rendered children, we clear the
170
- // them because it would be surprising not to clear Crank managed
171
- // children, even if the new element does not have explicit children.
172
- ("children" in props || (oldChildren && oldChildren.length))) {
173
- if (children.length === 0) {
174
- node.textContent = "";
175
- }
176
- else {
177
- let oldChild = node.firstChild;
178
- let i = 0;
179
- while (oldChild !== null && i < children.length) {
180
- const newChild = children[i];
181
- if (oldChild === newChild) {
182
- oldChild = oldChild.nextSibling;
183
- i++;
217
+ else if (typeof value === "object") {
218
+ // class={{"included-class": true, "excluded-class": false}} syntax
219
+ if (typeof oldValue === "string") {
220
+ // if the old value was a string, we need to clear all classes
221
+ element.setAttribute("class", "");
222
+ }
223
+ let shouldIssueWarning = false;
224
+ const hydratingClasses = isHydrating
225
+ ? new Set(Array.from(element.classList))
226
+ : undefined;
227
+ const hydratingClassName = isHydrating
228
+ ? element.getAttribute("class")
229
+ : undefined;
230
+ for (const className in { ...oldValue, ...value }) {
231
+ const classValue = value && value[className];
232
+ if (classValue) {
233
+ element.classList.add(className);
234
+ if (hydratingClasses && hydratingClasses.has(className)) {
235
+ hydratingClasses.delete(className);
236
+ }
237
+ else if (isHydrating) {
238
+ shouldIssueWarning = true;
239
+ }
240
+ }
241
+ else {
242
+ element.classList.remove(className);
243
+ }
244
+ }
245
+ if (shouldIssueWarning ||
246
+ (hydratingClasses && hydratingClasses.size > 0)) {
247
+ emitHydrationWarning(name, quietProps, Object.keys(value)
248
+ .filter((k) => value[k])
249
+ .join(" "), hydratingClassName || "", element);
250
+ }
184
251
  }
185
- else if (typeof newChild === "string") {
186
- if (oldChild.nodeType === Node.TEXT_NODE) {
187
- if (oldChild.data !== newChild) {
188
- oldChild.data = newChild;
252
+ else if (!isSVG) {
253
+ if (element.className !== value) {
254
+ if (isHydrating) {
255
+ emitHydrationWarning(name, quietProps, value, element.className, element);
189
256
  }
190
- oldChild = oldChild.nextSibling;
257
+ element.className = value;
191
258
  }
192
- else {
193
- node.insertBefore(document.createTextNode(newChild), oldChild);
259
+ }
260
+ else if (element.getAttribute("class") !== value) {
261
+ if (isHydrating) {
262
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
194
263
  }
195
- i++;
264
+ element.setAttribute("class", value);
196
265
  }
197
- else if (oldChild.nodeType === Node.TEXT_NODE) {
198
- const nextSibling = oldChild.nextSibling;
199
- node.removeChild(oldChild);
200
- oldChild = nextSibling;
266
+ break;
267
+ case "innerHTML":
268
+ if (value !== oldValue) {
269
+ if (isHydrating) {
270
+ emitHydrationWarning(name, quietProps, value, element.innerHTML, element);
271
+ }
272
+ element.innerHTML = value;
201
273
  }
202
- else {
203
- node.insertBefore(newChild, oldChild);
204
- i++;
205
- // TODO: This is an optimization but we need to think a little more about other cases like prepending.
206
- if (oldChild !== children[i]) {
207
- const nextSibling = oldChild.nextSibling;
208
- node.removeChild(oldChild);
209
- oldChild = nextSibling;
274
+ break;
275
+ default: {
276
+ if (name[0] === "o" &&
277
+ name[1] === "n" &&
278
+ name[2] === name[2].toUpperCase() &&
279
+ typeof value === "function") {
280
+ // Support React-style event names (onClick, onChange, etc.)
281
+ name = name.toLowerCase();
282
+ }
283
+ // try to set the property directly
284
+ if (name in element &&
285
+ // boolean properties will coerce strings, but sometimes they map to
286
+ // enumerated attributes, where truthy strings ("false", "no") map to
287
+ // falsy properties, so we force using setAttribute.
288
+ !(typeof value === "string" &&
289
+ typeof element[name] === "boolean") &&
290
+ isWritableProperty(element, name)) {
291
+ if (element[name] !== value || oldValue === undefined) {
292
+ if (isHydrating &&
293
+ typeof element[name] === "string" &&
294
+ element[name] !== value) {
295
+ emitHydrationWarning(name, quietProps, value, element[name], element);
296
+ }
297
+ // if the property is writable, assign it directly
298
+ element[name] = value;
299
+ }
300
+ continue;
301
+ }
302
+ if (value === true) {
303
+ value = "";
304
+ }
305
+ else if (value == null || value === false) {
306
+ if (isHydrating && element.hasAttribute(name)) {
307
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
210
308
  }
309
+ element.removeAttribute(name);
310
+ continue;
311
+ }
312
+ else if (typeof value !== "string") {
313
+ value = String(value);
314
+ }
315
+ if (element.getAttribute(name) !== value) {
316
+ if (isHydrating) {
317
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
318
+ }
319
+ element.setAttribute(name, value);
211
320
  }
212
321
  }
213
- // remove excess DOM nodes
214
- while (oldChild !== null) {
215
- const nextSibling = oldChild.nextSibling;
216
- node.removeChild(oldChild);
217
- oldChild = nextSibling;
322
+ }
323
+ }
324
+ },
325
+ arrange({ tag, node, props, children, }) {
326
+ if (tag === crank.Portal && (node == null || typeof node.nodeType !== "number")) {
327
+ throw new TypeError(`<Portal> root is not a node. Received: ${String(node)}`);
328
+ }
329
+ if (!("innerHTML" in props)) {
330
+ let oldChild = node.firstChild;
331
+ for (let i = 0; i < children.length; i++) {
332
+ const newChild = children[i];
333
+ if (oldChild === newChild) {
334
+ // the child is already in the right place, so we can skip it
335
+ oldChild = oldChild.nextSibling;
218
336
  }
219
- // append excess children
220
- for (; i < children.length; i++) {
221
- const newChild = children[i];
222
- node.appendChild(typeof newChild === "string"
223
- ? document.createTextNode(newChild)
224
- : newChild);
337
+ else {
338
+ node.insertBefore(newChild, oldChild);
339
+ if (tag !== crank.Portal &&
340
+ oldChild &&
341
+ i + 1 < children.length &&
342
+ oldChild !== children[i + 1]) {
343
+ oldChild = oldChild.nextSibling;
344
+ }
225
345
  }
226
346
  }
227
347
  }
228
348
  },
229
- text(text, _scope, hydrationData) {
230
- if (hydrationData != null) {
231
- let value = hydrationData.children.shift();
232
- if (typeof value !== "string" || !value.startsWith(text)) ;
233
- else if (text.length < value.length) {
234
- value = value.slice(text.length);
235
- hydrationData.children.unshift(value);
349
+ remove({ node, parentNode, isNested, }) {
350
+ if (!isNested && node.parentNode === parentNode) {
351
+ parentNode.removeChild(node);
352
+ }
353
+ },
354
+ text({ value, oldNode, hydrationNodes, }) {
355
+ if (hydrationNodes != null) {
356
+ let node = hydrationNodes.shift();
357
+ if (!node || node.nodeType !== Node.TEXT_NODE) {
358
+ console.warn(`Expected "${value}" while hydrating but found:`, node);
236
359
  }
360
+ else {
361
+ // value is a text node, check if it matches the expected text
362
+ const textData = node.data;
363
+ if (textData.length > value.length) {
364
+ if (textData.startsWith(value)) {
365
+ // the text node is longer than the expected text, so we
366
+ // reuse the existing text node, but truncate it and unshift the rest
367
+ node.data = value;
368
+ hydrationNodes.unshift(document.createTextNode(textData.slice(value.length)));
369
+ return node;
370
+ }
371
+ }
372
+ else if (textData === value) {
373
+ return node;
374
+ }
375
+ // We log textData and not node because node will be mutated
376
+ console.warn(`Expected "${value}" while hydrating but found:`, textData);
377
+ oldNode = node;
378
+ }
379
+ }
380
+ if (oldNode != null) {
381
+ if (oldNode.data !== value) {
382
+ oldNode.data = value;
383
+ }
384
+ return oldNode;
237
385
  }
238
- return text;
386
+ return document.createTextNode(value);
239
387
  },
240
- raw(value, xmlns, hydrationData) {
241
- let result;
388
+ raw({ value, scope: xmlns, hydrationNodes, }) {
389
+ let nodes;
242
390
  if (typeof value === "string") {
243
391
  const el = xmlns == null
244
392
  ? document.createElement("div")
245
393
  : document.createElementNS(xmlns, "svg");
246
394
  el.innerHTML = value;
247
- if (el.childNodes.length === 0) {
248
- result = undefined;
249
- }
250
- else if (el.childNodes.length === 1) {
251
- result = el.childNodes[0];
252
- }
253
- else {
254
- result = Array.from(el.childNodes);
255
- }
395
+ nodes = Array.from(el.childNodes);
256
396
  }
257
397
  else {
258
- result = value;
398
+ nodes = value == null ? [] : Array.isArray(value) ? [...value] : [value];
259
399
  }
260
- if (hydrationData != null) {
261
- // TODO: maybe we should warn on incorrect values
262
- if (Array.isArray(result)) {
263
- for (let i = 0; i < result.length; i++) {
264
- const node = result[i];
265
- if (typeof node !== "string" &&
266
- (node.nodeType === Node.ELEMENT_NODE ||
267
- node.nodeType === Node.TEXT_NODE)) {
268
- hydrationData.children.shift();
269
- }
400
+ if (hydrationNodes != null) {
401
+ for (let i = 0; i < nodes.length; i++) {
402
+ const node = nodes[i];
403
+ // check if node is equal to the next node in the hydration array
404
+ const hydrationNode = hydrationNodes.shift();
405
+ if (hydrationNode &&
406
+ typeof hydrationNode === "object" &&
407
+ typeof hydrationNode.nodeType === "number" &&
408
+ node.isEqualNode(hydrationNode)) {
409
+ nodes[i] = hydrationNode;
270
410
  }
271
- }
272
- else if (result != null && typeof result !== "string") {
273
- if (result.nodeType === Node.ELEMENT_NODE ||
274
- result.nodeType === Node.TEXT_NODE) {
275
- hydrationData.children.shift();
411
+ else {
412
+ console.warn(`Expected <Raw value="${String(value)}"> while hydrating but found:`, hydrationNode);
276
413
  }
277
414
  }
278
415
  }
279
- return result;
416
+ return nodes.length === 0
417
+ ? undefined
418
+ : nodes.length === 1
419
+ ? nodes[0]
420
+ : nodes;
280
421
  },
281
422
  };
282
423
  class DOMRenderer extends crank.Renderer {
283
424
  constructor() {
284
- super(impl);
425
+ super(adapter);
285
426
  }
286
427
  render(children, root, ctx) {
287
428
  validateRoot(root);
@@ -293,14 +434,17 @@ class DOMRenderer extends crank.Renderer {
293
434
  }
294
435
  }
295
436
  function validateRoot(root) {
296
- if (root === null ||
437
+ if (root == null ||
297
438
  (typeof root === "object" && typeof root.nodeType !== "number")) {
298
- throw new TypeError(`Render root is not a node. Received: ${JSON.stringify(root && root.toString())}`);
439
+ throw new TypeError(`Render root is not a node. Received: ${String(root)}`);
440
+ }
441
+ else if (root.nodeType !== Node.ELEMENT_NODE) {
442
+ throw new TypeError(`Render root must be an element node. Received: ${String(root)}`);
299
443
  }
300
444
  }
301
445
  const renderer = new DOMRenderer();
302
446
 
303
447
  exports.DOMRenderer = DOMRenderer;
304
- exports.impl = impl;
448
+ exports.adapter = adapter;
305
449
  exports.renderer = renderer;
306
450
  //# sourceMappingURL=dom.cjs.map