From 94bb3db9655eb0fb3aa1160218d0aa3101a934d8 Mon Sep 17 00:00:00 2001 From: webreflection Date: Thu, 18 Jan 2024 21:47:15 +0100 Subject: [PATCH 1/3] work in progress --- .gitignore | 7 +++-- esm/creator.js | 10 +----- esm/hydro.js | 64 ++++++++++++++++++++++++++++++++++++++ esm/parser.js | 28 +++++++++++------ esm/persistent-fragment.js | 12 ++++++- esm/rabbit.js | 8 ++--- esm/ssr.js | 6 ++++ esm/utils.js | 9 ++++++ package.json | 2 +- rollup/es.config.js | 18 +++++++++++ rollup/ssr.cjs | 33 ++++++++++++++++++++ test/hydro.html | 46 +++++++++++++++++++++++++++ test/hydro.mjs | 57 +++++++++++++++++++++++++++++++++ test/parser.mjs | 11 +++++++ test/ssr.mjs | 18 +++++++++++ 15 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 esm/hydro.js create mode 100644 esm/ssr.js create mode 100644 rollup/ssr.cjs create mode 100644 test/hydro.html create mode 100644 test/hydro.mjs create mode 100644 test/parser.mjs create mode 100644 test/ssr.mjs diff --git a/.gitignore b/.gitignore index 18d66a1..d1e399b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,14 @@ types/ cjs/* !cjs/package.json dom.js -esm/init.js -init.js +esm/init*.js +init*.js keyed.js !esm/keyed.js !esm/dom/keyed.js +hydro.js +!esm/hydro.js +!esm/dom/hydro.js index.js !esm/index.js !esm/dom/index.js diff --git a/esm/creator.js b/esm/creator.js index 9f68ce4..2d54f36 100644 --- a/esm/creator.js +++ b/esm/creator.js @@ -1,17 +1,9 @@ import { PersistentFragment } from './persistent-fragment.js'; import { bc, detail } from './literals.js'; import { array, hole } from './handler.js'; -import { empty } from './utils.js'; +import { empty, find } from './utils.js'; import { cache } from './literals.js'; -/** - * @param {DocumentFragment} content - * @param {number[]} path - * @returns {Element} - */ -const find = (content, path) => path.reduceRight(childNodesIndex, content); -const childNodesIndex = (node, i) => node.childNodes[i]; - /** @param {(template: TemplateStringsArray, values: any[]) => import("./parser.js").Resolved} parse */ export default parse => ( /** diff --git a/esm/hydro.js b/esm/hydro.js new file mode 100644 index 0000000..2943658 --- /dev/null +++ b/esm/hydro.js @@ -0,0 +1,64 @@ +import { PersistentFragment } from './persistent-fragment.js'; +import { abc, cache, detail } from './literals.js'; +import { empty, find, set } from './utils.js'; +import { array, hole } from './handler.js'; +import { parse } from './parser.js'; +import { + Hole, + render as _render, + html, svg, + htmlFor, svgFor, + attr +} from './keyed.js'; + +const parseHTML = parse(false, true); +const parseSVG = parse(true, true); + +const hydrate = (fragment, {s, t, v}) => { + const { b: entries, c: direct } = (s ? parseSVG : parseHTML)(t, v); + const { length } = entries; + if (length !== v.length) return noHydration; + let root = fragment, details = length ? [] : empty; + if (!direct) { + if ( + fragment.firstChild?.data !== '<>' || + fragment.lastChild?.data !== '' + ) return noHydration; + root = PersistentFragment.adopt(fragment); + } + for (let current, prev, i = 0; i < length; i++) { + const { a: path, b: update, c: name } = entries[i]; + // TODO: node should be adjusted if it's array or hole + // * if it's array, no way caching it as current helps + // * if it's a hole or attribute/text thing, current helps + let node = path === prev ? current : (current = find(root, (prev = path))); + details[i] = detail( + update, + node, + name, + // TODO: find and resolve the array via the next `` + // TODO: resolve the cache via the surrounding hole + update === array ? [] : (update === hole ? cache() : null) + ); + } + return abc(t, root, details); +}; + +const known = new WeakMap; +const noHydration = cache(); + +const render = (where, what) => { + const hole = typeof what === 'function' ? what() : what; + if (hole instanceof Hole) { + const info = known.get(where) || set(known, where, hydrate(where, hole)); + if (info.a === hole.t) { + hole.toDOM(info); + return where; + } + } + return _render(where, hole); +}; + +const { document } = globalThis; + +export { Hole, document, render, html, svg, htmlFor, svgFor, attr }; diff --git a/esm/parser.js b/esm/parser.js index 1ff4f74..ecac08b 100644 --- a/esm/parser.js +++ b/esm/parser.js @@ -33,15 +33,21 @@ const createPath = node => { const textNode = () => document.createTextNode(''); +const prefix = 'isµ'; + /** * @param {TemplateStringsArray} template * @param {boolean} xml * @returns {Resolved} */ -const resolve = (template, values, xml) => { - const content = createContent(parser(template, prefix, xml), xml); +const resolve = (template, values, xml, holed) => { + let entries = empty, markup = parser(template, prefix, xml); + if (holed) markup = markup.replace( + new RegExp(``, 'g'), + '$&' + ); + const content = createContent(markup, xml); const { length } = template; - let entries = empty; if (length > 1) { const replace = []; const tw = document.createTreeWalker(content, 1 | 128); @@ -107,15 +113,19 @@ const resolve = (template, values, xml) => { len = 0; } - return set(cache, template, abc(content, entries, len === 1)); + return abc(content, entries, len === 1); }; -/** @type {WeakMap} */ -const cache = new WeakMap; -const prefix = 'isµ'; - /** * @param {boolean} xml + * @param {boolean} holed * @returns {(template: TemplateStringsArray, values: any[]) => Resolved} */ -export default xml => (template, values) => cache.get(template) || resolve(template, values, xml); +export const parse = (xml, holed) => { + /** @type {WeakMap} */ + const cache = new WeakMap; + return (template, values) => ( + cache.get(template) || + set(cache, template, resolve(template, values, xml, holed)) + ); +}; diff --git a/esm/persistent-fragment.js b/esm/persistent-fragment.js index b2ea1eb..5c6a8f8 100644 --- a/esm/persistent-fragment.js +++ b/esm/persistent-fragment.js @@ -28,6 +28,15 @@ const comment = value => document.createComment(value); /** @extends {DocumentFragment} */ export class PersistentFragment extends custom(DocumentFragment) { + static adopt(content) { + const pf = new PersistentFragment( + document.createDocumentFragment() + ); + pf.#firstChild = content.firstChild; + pf.#lastChild = content.lastChild; + pf.#nodes = [...content.childNodes]; + return pf; + } #firstChild = comment('<>'); #lastChild = comment(''); #nodes = empty; @@ -50,7 +59,7 @@ export class PersistentFragment extends custom(DocumentFragment) { remove(this, true).replaceWith(node); } valueOf() { - let { firstChild, lastChild, parentNode } = this; + const { parentNode } = this; if (parentNode === this) { if (this.#nodes === empty) this.#nodes = [...this.childNodes]; @@ -65,6 +74,7 @@ export class PersistentFragment extends custom(DocumentFragment) { // This is a render-only specific issue but it's tested and // it's worth fixing to me to have more consistent fragments. if (parentNode) { + let { firstChild, lastChild } = this; this.#nodes = [firstChild]; while (firstChild !== lastChild) this.#nodes.push((firstChild = firstChild.nextSibling)); diff --git a/esm/rabbit.js b/esm/rabbit.js index 512e9f7..4cae51d 100644 --- a/esm/rabbit.js +++ b/esm/rabbit.js @@ -1,10 +1,10 @@ import { array, hole } from './handler.js'; import { cache } from './literals.js'; +import { parse } from './parser.js'; import create from './creator.js'; -import parser from './parser.js'; -const parseHTML = create(parser(false)); -const parseSVG = create(parser(true)); +const createHTML = create(parse(false, false)); +const createSVG = create(parse(true, false)); /** * @param {import("./literals.js").Cache} info @@ -13,7 +13,7 @@ const parseSVG = create(parser(true)); */ const unroll = (info, { s, t, v }) => { if (info.a !== t) { - const { b, c } = (s ? parseSVG : parseHTML)(t, v); + const { b, c } = (s ? createSVG : createHTML)(t, v); info.a = t; info.b = b; info.c = c; diff --git a/esm/ssr.js b/esm/ssr.js new file mode 100644 index 0000000..d8fd893 --- /dev/null +++ b/esm/ssr.js @@ -0,0 +1,6 @@ +import { Hole, render, html, svg, attr } from './index.js'; + +const htmlFor = () => html; +const svgFor = () => svg; + +export { Hole, render, html, svg, htmlFor, svgFor, attr }; diff --git a/esm/utils.js b/esm/utils.js index 1072670..7ce053e 100644 --- a/esm/utils.js +++ b/esm/utils.js @@ -34,3 +34,12 @@ export const gPD = (ref, prop) => { while(!desc && (ref = getPrototypeOf(ref))); return desc; }; + + +/** + * @param {DocumentFragment} content + * @param {number[]} path + * @returns {Element} + */ +export const find = (content, path) => path.reduceRight(childNodesIndex, content); +const childNodesIndex = (node, i) => node.childNodes[i]; diff --git a/package.json b/package.json index b9e63ba..f3bee74 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "benchmark:w3c": "node test/benchmark/linkedom.js --w3c; node test/benchmark/linkedom-cached.js --w3c; node test/benchmark/dom.js --w3c", "benchmark:dom": "node test/benchmark/linkedom.js --dom; node test/benchmark/linkedom-cached.js --dom; node test/benchmark/dom.js --dom", - "build": "npm run rollup:es && node rollup/init.cjs && npm run rollup:init && rm -rf cjs/* && npm run cjs && rm -rf types && npm run ts && npm run test && npm run size", + "build": "npm run rollup:es && node rollup/ssr.cjs && node rollup/init.cjs && npm run rollup:init && rm -rf cjs/* && npm run cjs && rm -rf types && npm run ts && npm run test && npm run size", "cjs": "ascjs --no-default esm cjs", "rollup:es": "rollup --config rollup/es.config.js", "rollup:init": "rollup --config rollup/init.config.js", diff --git a/rollup/es.config.js b/rollup/es.config.js index e27339b..1522029 100644 --- a/rollup/es.config.js +++ b/rollup/es.config.js @@ -18,6 +18,24 @@ export default [ name: 'uhtml', }, }, + { + plugins: [nodeResolve()], + input: './esm/ssr.js', + output: { + esModule: false, + file: './esm/init-ssr.js', + format: 'iife', + name: 'uhtml', + }, + }, + { + plugins, + input: './esm/hydro.js', + output: { + esModule: true, + file: './hydro.js', + }, + }, { plugins, input: './esm/index.js', diff --git a/rollup/ssr.cjs b/rollup/ssr.cjs new file mode 100644 index 0000000..1256581 --- /dev/null +++ b/rollup/ssr.cjs @@ -0,0 +1,33 @@ +const { readFileSync, writeFileSync } = require('fs'); +const { join } = require('path'); + +const init = join(__dirname, '..', 'esm', 'init-ssr.js'); +const uhtml = readFileSync(init).toString(); + +const content = [ + 'const document = content ? new DOMParser().parseFromString(content, ...rest) : new Document;', + 'const { constructor: DocumentFragment } = document.createDocumentFragment();' +]; + +writeFileSync(init, ` +// ⚠️ WARNING - THIS FILE IS AN ARTIFACT - DO NOT EDIT + +import Document from './dom/document.js'; +import DOMParser from './dom/dom-parser.js'; + +/** + * @param {Document} document + * @returns {import("./keyed.js")} + */ +export default (content, ...rest) => ${ + // tested via integration + uhtml + .replace(/const create(HTML|SVG) = create\(parse\((false|true), false\)\)/g, 'const create$1 = create(parse($2, true))') + .replace(`svg || ('ownerSVGElement' in element)`, `/* c8 ignore start */ svg || ('ownerSVGElement' in element) /* c8 ignore stop */`) + .replace(/diffFragment = \(([\S\s]+?)return /, 'diffFragment = /* c8 ignore start */($1/* c8 ignore stop */return ') + .replace(/udomdiff = \(([\S\s]+?)return /, 'udomdiff = /* c8 ignore start */($1/* c8 ignore stop */return ') + .replace(/^(\s+)replaceWith\(([^}]+?)\}/m, '$1/* c8 ignore start */\n$1replaceWith($2}\n$1/* c8 ignore stop */') + .replace(/^(\s+)(["'])use strict\2;/m, (_, tab, quote) => `${tab}${quote}use strict${quote};\n\n${tab}${content.join(`\n${tab}`)}`) + .replace(/^(\s+)(return exports;)/m, '$1exports.document = document;\n$1$2') + .replace(/^[^(]+/, '') +}`); diff --git a/test/hydro.html b/test/hydro.html new file mode 100644 index 0000000..7c36415 --- /dev/null +++ b/test/hydro.html @@ -0,0 +1,46 @@ + + + + Hello Hydro + +
+ + +
+ + + diff --git a/test/hydro.mjs b/test/hydro.mjs new file mode 100644 index 0000000..aeae8ea --- /dev/null +++ b/test/hydro.mjs @@ -0,0 +1,57 @@ +import init from '../esm/init-ssr.js'; + +function App(state) { + return html` +
+ + +
+ `; +} + +const component = (target, Callback) => { + const effect = { + target, + update(...args) { + render(target, Callback.apply(effect, args)); + } + }; + return Callback.bind(effect); +}; + +const state = { title: 'Hello Hydro', count: 0 }; + +const { document, render, html } = init(` + + + + + + ${state.title} + + + +`); + +const { body } = document; + +const Body = component(body, App); + +render(body, Body(state)); + +console.log(document.toString()); diff --git a/test/parser.mjs b/test/parser.mjs new file mode 100644 index 0000000..af86d92 --- /dev/null +++ b/test/parser.mjs @@ -0,0 +1,11 @@ +import parser from '@webreflection/uparser'; + +const prefix = 'isµ'; +const re = new RegExp(``, 'g'); + +const template = t => t; + +console.log( + parser(template`a${1}b`, prefix, false) + .replace(re, '$&') +); diff --git a/test/ssr.mjs b/test/ssr.mjs new file mode 100644 index 0000000..4584d8a --- /dev/null +++ b/test/ssr.mjs @@ -0,0 +1,18 @@ +import init from '../esm/init-ssr.js'; + +const { document, render, html } = init(` + + + ${'Hello SSR'} + +
+`.trim() +); + +render(document.getElementById('test'), html` +

+ !!! ${'Hello SSR'} !!! +

+`); + +console.log(document.toString()); From ed8e7f95a8e4bade5a1bf832b2b9d66571563378 Mon Sep 17 00:00:00 2001 From: webreflection Date: Sun, 21 Jan 2024 15:09:59 +0100 Subject: [PATCH 2/3] up to the point I need to reverse-loop the container to have clear paths out of the template --- esm/dom/document.js | 3 +- esm/dom/symbols.js | 1 + esm/dom/text.js | 8 ++- esm/hydro.js | 78 ++++++++++++++++++++---- esm/parser.js | 13 +++- esm/persistent-fragment.js | 9 --- package.json | 2 +- rollup/ssr.cjs | 4 +- test/hydro.html | 12 ++-- test/hydro.mjs | 2 + test/parser.mjs | 2 +- test/ssr.mjs | 2 +- test/virtual.mjs | 118 +++++++++++++++++++++++++++++++++++++ 13 files changed, 220 insertions(+), 34 deletions(-) create mode 100644 test/virtual.mjs diff --git a/esm/dom/document.js b/esm/dom/document.js index 53a1604..a837a83 100644 --- a/esm/dom/document.js +++ b/esm/dom/document.js @@ -2,7 +2,7 @@ import { DOCUMENT_NODE } from 'domconstants/constants'; import { setParentNode } from './utils.js'; -import { childNodes, documentElement, nodeName, ownerDocument } from './symbols.js'; +import { childNodes, documentElement, nodeName, ownerDocument, __chunks__ } from './symbols.js'; import Attribute from './attribute.js'; import Comment from './comment.js'; @@ -33,6 +33,7 @@ export default class Document extends Parent { this[doctype] = null; this[head] = null; this[body] = null; + this[__chunks__] = false; if (type === 'html') { const html = (this[documentElement] = new Element(type, this)); this[childNodes] = [ diff --git a/esm/dom/symbols.js b/esm/dom/symbols.js index 6e5bd52..2855b1a 100644 --- a/esm/dom/symbols.js +++ b/esm/dom/symbols.js @@ -9,3 +9,4 @@ export const parentNode = Symbol('parentNode'); export const attributes = Symbol('attributes'); export const name = Symbol('name'); export const value = Symbol('value'); +export const __chunks__ = Symbol(); diff --git a/esm/dom/text.js b/esm/dom/text.js index 7815238..ed78dc6 100644 --- a/esm/dom/text.js +++ b/esm/dom/text.js @@ -3,7 +3,7 @@ import { TEXT_ELEMENTS } from 'domconstants/re'; import { escape } from 'html-escaper'; import CharacterData from './character-data.js'; -import { parentNode, localName, ownerDocument, value } from './symbols.js'; +import { parentNode, localName, ownerDocument, value, __chunks__ } from './symbols.js'; export default class Text extends CharacterData { constructor(data = '', owner = null) { @@ -17,6 +17,10 @@ export default class Text extends CharacterData { toString() { const { [parentNode]: parent, [value]: data } = this; return parent && TEXT_ELEMENTS.test(parent[localName]) ? - data : escape(data); + data : + (this[ownerDocument]?.[__chunks__] && this.previousSibling?.nodeType === TEXT_NODE ? + `${escape(data)}` : + escape(data) + ); } } diff --git a/esm/hydro.js b/esm/hydro.js index 2943658..432a229 100644 --- a/esm/hydro.js +++ b/esm/hydro.js @@ -1,4 +1,4 @@ -import { PersistentFragment } from './persistent-fragment.js'; +import { COMMENT_NODE, TEXT_NODE } from 'domconstants/constants'; import { abc, cache, detail } from './literals.js'; import { empty, find, set } from './utils.js'; import { array, hole } from './handler.js'; @@ -14,24 +14,79 @@ import { const parseHTML = parse(false, true); const parseSVG = parse(true, true); -const hydrate = (fragment, {s, t, v}) => { +const parent = () => ({ childNodes: [] }); + +const skip = (node, data) => { + +}; + +const reMap = (parentNode, { childNodes }) => { + for (let first = true, { length } = childNodes; length--;) { + let node = childNodes[length]; + switch (node.nodeType) { + case COMMENT_NODE: + if (node.data === '') { + let nested = 0; + while (node = node.previousSibling) { + length--; + if (node.nodeType === COMMENT_NODE) { + if (node.data === '') nested++; + else if (node.data === '<>') { + if (!nested--) break; + } + } + else + parentNode.childNodes.unshift(node); + } + } + else if (/\[(\d+)\]/.test(node.data)) { + let many = +RegExp.$1; + parentNode.childNodes.unshift(node); + while (many--) { + node = node.previousSibling; + if (node.nodeType === COMMENT_NODE && node.data === '}') { + node = skip(node, '{'); + } + } + } + break; + case TEXT_NODE: + // ignore browser artifacts on closing fragments + if (first && !node.data.trim()) break; + default: + parentNode.childNodes.unshift(node); + break; + } + first = false; + } + return parentNode; +}; + +const hydrate = (root, {s, t, v}) => { + debugger; const { b: entries, c: direct } = (s ? parseSVG : parseHTML)(t, v); const { length } = entries; - if (length !== v.length) return noHydration; - let root = fragment, details = length ? [] : empty; - if (!direct) { - if ( - fragment.firstChild?.data !== '<>' || - fragment.lastChild?.data !== '' - ) return noHydration; - root = PersistentFragment.adopt(fragment); - } + // let's assume hydro is used on purpose with valid templates + // to use entries meaningfully re-map the container. + // This is complicated yet possible. + // * fragments are allowed only top-level + // * nested fragments will likely be wrapped in holes + // * arrays can point at either fragments, DOM nodes, or holes + // * arrays can't be path-addressed if not for the comment itself + // * ideally their previous content should be pre-populated with nodes, holes and fragments + // * it is possible that the whole dance is inside-out so that nested normalized content + // can be then addressed (as already live) by the outer content + const fake = reMap(parent(), root, direct); + const details = length ? [] : empty; for (let current, prev, i = 0; i < length; i++) { const { a: path, b: update, c: name } = entries[i]; + // adjust the length of the first path node + if (!direct) path[path.length - 1]++; // TODO: node should be adjusted if it's array or hole // * if it's array, no way caching it as current helps // * if it's a hole or attribute/text thing, current helps let node = path === prev ? current : (current = find(root, (prev = path))); + if (!direct) path[path.length - 1]--; details[i] = detail( update, node, @@ -45,7 +100,6 @@ const hydrate = (fragment, {s, t, v}) => { }; const known = new WeakMap; -const noHydration = cache(); const render = (where, what) => { const hole = typeof what === 'function' ? what() : what; diff --git a/esm/parser.js b/esm/parser.js index ecac08b..6ceb9bf 100644 --- a/esm/parser.js +++ b/esm/parser.js @@ -44,7 +44,7 @@ const resolve = (template, values, xml, holed) => { let entries = empty, markup = parser(template, prefix, xml); if (holed) markup = markup.replace( new RegExp(``, 'g'), - '$&' + '$&' ); const content = createContent(markup, xml); const { length } = template; @@ -54,16 +54,25 @@ const resolve = (template, values, xml, holed) => { let i = 0, search = `${prefix}${i++}`; entries = []; while (i < length) { - const node = tw.nextNode(); + let node = tw.nextNode(); // these are holes or arrays if (node.nodeType === COMMENT_NODE) { if (node.data === search) { // ⚠️ once array, always array! const update = isArray(values[i - 1]) ? array : hole; if (update === hole) replace.push(node); + else if (holed) { + // ⚠️ this operation works only with uhtml/dom + // it would bail out native TreeWalker + const { previousSibling, nextSibling } = node; + previousSibling.data = '[]'; + nextSibling.remove(); + } entries.push(abc(createPath(node), update, null)); search = `${prefix}${i++}`; } + // ⚠️ this operation works only with uhtml/dom + else if (holed && node.data === '#') node.remove(); } else { let path; diff --git a/esm/persistent-fragment.js b/esm/persistent-fragment.js index 5c6a8f8..7a90d06 100644 --- a/esm/persistent-fragment.js +++ b/esm/persistent-fragment.js @@ -28,15 +28,6 @@ const comment = value => document.createComment(value); /** @extends {DocumentFragment} */ export class PersistentFragment extends custom(DocumentFragment) { - static adopt(content) { - const pf = new PersistentFragment( - document.createDocumentFragment() - ); - pf.#firstChild = content.firstChild; - pf.#lastChild = content.lastChild; - pf.#nodes = [...content.childNodes]; - return pf; - } #firstChild = comment('<>'); #lastChild = comment(''); #nodes = empty; diff --git a/package.json b/package.json index f3bee74..8034565 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "rollup:es": "rollup --config rollup/es.config.js", "rollup:init": "rollup --config rollup/init.config.js", "server": "npx static-handler .", - "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"signal $(cat signal.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";", + "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"signal $(cat signal.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";echo \"hydro $(cat hydro.js | brotli | wc -c)\";", "test": "c8 node test/coverage.js && node test/modern.mjs", "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info", "ts": "tsc -p ." diff --git a/rollup/ssr.cjs b/rollup/ssr.cjs index 1256581..29e1d45 100644 --- a/rollup/ssr.cjs +++ b/rollup/ssr.cjs @@ -6,7 +6,8 @@ const uhtml = readFileSync(init).toString(); const content = [ 'const document = content ? new DOMParser().parseFromString(content, ...rest) : new Document;', - 'const { constructor: DocumentFragment } = document.createDocumentFragment();' + 'const { constructor: DocumentFragment } = document.createDocumentFragment();', + 'document[__chunks__] = true;', ]; writeFileSync(init, ` @@ -14,6 +15,7 @@ writeFileSync(init, ` import Document from './dom/document.js'; import DOMParser from './dom/dom-parser.js'; +import { __chunks__ } from './dom/symbols.js'; /** * @param {Document} document diff --git a/test/hydro.html b/test/hydro.html index 7c36415..faf5cbc 100644 --- a/test/hydro.html +++ b/test/hydro.html @@ -7,7 +7,9 @@ import { render, html } from '../hydro.js'; function App(state) { return html` +

${state.title}

+
    ${[]}
-
- + + diff --git a/test/hydro.mjs b/test/hydro.mjs index aeae8ea..4a5dcce 100644 --- a/test/hydro.mjs +++ b/test/hydro.mjs @@ -2,7 +2,9 @@ import init from '../esm/init-ssr.js'; function App(state) { return html` +

${state.title}

+
    ${[]}