feat: use bracket matching instead of `ssr:n` comments (#10904)

* use short comments

* use bracket matching

* fix

* update snapshots

* update tests

* fix
pull/10889/head
Rich Harris 7 months ago committed by GitHub
parent f1d9afe32f
commit 8685d497e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -27,6 +27,9 @@ import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patte
import { DOMBooleanAttributes } from '../../../../constants.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
const block_open = t_string('<![>');
const block_close = t_string('<!]>');
/**
* @param {string} value
* @returns {import('./types').TemplateString}
@ -52,15 +55,6 @@ function t_statement(value) {
return { type: 'statement', value };
}
/**
* @param {import('./types').ServerTransformState} state
* @returns {[import('estree').VariableDeclaration, import('estree').Identifier]}
*/
function serialize_anchor(state) {
const id = state.scope.root.unique('anchor');
return [b.const(id, b.call('$.create_anchor', b.id('$$payload'))), id];
}
/**
* @param {import('./types').Template[]} template
* @param {import('estree').Identifier} out
@ -1237,12 +1231,10 @@ const template_visitors = {
},
HtmlTag(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const raw = /** @type {import('estree').Expression} */ (context.visit(node.expression));
context.state.template.push(t_expression(raw));
state.template.push(t_expression(id));
state.template.push(block_close);
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
@ -1273,10 +1265,8 @@ const template_visitors = {
},
RenderTag(node, context) {
const state = context.state;
const [anchor, anchor_id] = serialize_anchor(state);
state.init.push(anchor);
state.template.push(t_expression(anchor_id));
state.template.push(block_open);
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
@ -1302,7 +1292,7 @@ const template_visitors = {
)
);
state.template.push(t_expression(anchor_id));
state.template.push(block_close);
},
ClassDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
@ -1427,9 +1417,7 @@ const template_visitors = {
}
};
const [el_anchor, anchor_id] = serialize_anchor(context.state);
context.state.init.push(el_anchor);
context.state.template.push(t_expression(anchor_id));
context.state.template.push(block_open);
const main = create_block(node, node.fragment.nodes, {
...context,
@ -1465,7 +1453,7 @@ const template_visitors = {
)
)
),
t_expression(anchor_id)
block_close
);
if (context.state.options.dev) {
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
@ -1473,9 +1461,7 @@ const template_visitors = {
},
EachBlock(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression));
@ -1486,14 +1472,6 @@ const template_visitors = {
: b.id(node.index);
const children = node.body.nodes;
const [each_dec, each_id] = serialize_anchor(state);
/** @type {import('./types').Anchor} */
const anchor = {
type: 'Anchor',
id: each_id
};
const array_id = state.scope.root.unique('each_array');
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
@ -1507,11 +1485,14 @@ const template_visitors = {
each.push(b.let(node.index, index));
}
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value))));
each.push(
each_dec,
.../** @type {import('estree').Statement[]} */ (create_block(node, children, context, anchor))
.../** @type {import('estree').Statement[]} */ (create_block(node, children, context))
);
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value))));
const for_loop = b.for(
b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))),
@ -1535,13 +1516,11 @@ const template_visitors = {
} else {
state.template.push(t_statement(for_loop));
}
state.template.push(t_expression(id));
state.template.push(block_close);
},
IfBlock(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
// Insert ssr:if:true/false anchors in addition to the other anchors so that
// the if block can catch hydration mismatches (false on the server, true on the client and vice versa)
@ -1568,13 +1547,11 @@ const template_visitors = {
)
)
);
state.template.push(t_expression(id));
state.template.push(block_close);
},
AwaitBlock(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
state.template.push(
t_statement(
@ -1608,16 +1585,14 @@ const template_visitors = {
)
);
state.template.push(t_expression(id));
state.template.push(block_close);
},
KeyBlock(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const body = create_block(node, node.fragment.nodes, context);
state.template.push(t_statement(b.block(body)));
state.template.push(t_expression(id));
state.template.push(block_close);
},
SnippetBlock(node, context) {
// TODO hoist where possible
@ -1635,34 +1610,28 @@ const template_visitors = {
},
Component(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const call = serialize_inline_component(node, node.name, context);
state.template.push(t_statement(call));
state.template.push(t_expression(id));
state.template.push(block_close);
},
SvelteSelf(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const call = serialize_inline_component(node, context.state.analysis.name, context);
state.template.push(t_statement(call));
state.template.push(t_expression(id));
state.template.push(block_close);
},
SvelteComponent(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
const call = serialize_inline_component(
node,
/** @type {import('estree').Expression} */ (context.visit(node.expression)),
context
);
state.template.push(t_statement(call));
state.template.push(t_expression(id));
state.template.push(block_close);
},
LetDirective(node, { state }) {
if (node.expression && node.expression.type !== 'Identifier') {
@ -1745,9 +1714,7 @@ const template_visitors = {
},
SlotElement(node, context) {
const state = context.state;
const [dec, id] = serialize_anchor(state);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push(block_open);
/** @type {import('estree').Property[]} */
const props = [];
@ -1794,7 +1761,7 @@ const template_visitors = {
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
state.template.push(t_statement(b.stmt(slot)));
state.template.push(t_expression(id));
state.template.push(block_close);
},
SvelteHead(node, context) {
const state = context.state;

@ -39,7 +39,7 @@ export function update_hydrate_nodes(first, insert_text) {
}
/**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* Returns all nodes between the first `<![>...<!]>` comment tag pair encountered.
* @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
* @returns {import('#client').TemplateNode[] | null}
@ -50,34 +50,43 @@ function get_hydrate_nodes(node, insert_text = false) {
var current_node = /** @type {null | import('#client').TemplateNode} */ (node);
/** @type {null | string} */
var target_depth = null;
var depth = 0;
var will_start = false;
var started = false;
while (current_node !== null) {
if (current_node.nodeType === 8) {
var data = /** @type {Comment} */ (current_node).data;
if (data.startsWith('ssr:')) {
var depth = data.slice(4);
if (data === '[') {
depth += 1;
will_start = true;
} else if (data === ']') {
if (!started) {
// TODO get rid of this — it exists because each blocks are doubly wrapped
return null;
}
if (target_depth === null) {
target_depth = depth;
} else if (depth === target_depth) {
if (--depth === 0) {
if (insert_text && nodes.length === 0) {
var text = empty();
nodes.push(text);
current_node.before(text);
}
return nodes;
} else {
nodes.push(current_node);
}
}
} else if (target_depth !== null) {
}
if (started) {
nodes.push(current_node);
}
current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling);
started = will_start;
}
return null;
@ -103,7 +112,7 @@ export function hydrate_block_anchor(node) {
export function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
/** @type {Comment} */ (node).data === '[' &&
hydrate_nodes[hydrate_nodes.length - 1] !== node
) {
const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node));

@ -1,5 +1,11 @@
import { DEV } from 'esm-env';
import { append_child, create_element, empty, init_operations } from './dom/operations.js';
import {
append_child,
clear_text_content,
create_element,
empty,
init_operations
} from './dom/operations.js';
import { PassiveDelegatedEvents } from '../../constants.js';
import { remove } from './dom/reconciler.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
@ -170,9 +176,9 @@ export function hydrate(component, options) {
: ''),
error
);
remove(nodes);
first_child.remove();
nodes[nodes.length - 1]?.nextSibling?.remove();
clear_text_content(container);
set_hydrating(false);
return mount(component, options);
} else {

@ -162,13 +162,12 @@ export function element(payload, tag, attributes_fn, children_fn) {
payload.out += `>`;
if (!VoidElements.has(tag)) {
const anchor = tag !== 'textarea' ? create_anchor(payload) : null;
if (anchor !== null) {
payload.out += anchor;
if (tag !== 'textarea') {
payload.out += '<![>';
}
children_fn();
if (anchor !== null) {
payload.out += anchor;
if (tag !== 'textarea') {
payload.out += '<!]>';
}
payload.out += `</${tag}>`;
}
@ -187,12 +186,10 @@ export let on_destroy = [];
*/
export function render(component, options) {
const payload = create_payload();
const root_anchor = create_anchor(payload);
const root_head_anchor = create_anchor(payload.head);
const prev_on_destroy = on_destroy;
on_destroy = [];
payload.out += root_anchor;
payload.out += '<![>';
if (options.context) {
$.push({});
@ -203,14 +200,14 @@ export function render(component, options) {
if (options.context) {
$.pop();
}
payload.out += root_anchor;
payload.out += '<!]>';
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
return {
head:
payload.head.out || payload.head.title
? payload.head.title + root_head_anchor + payload.head.out + root_head_anchor
? payload.head.title + '<![>' + payload.head.out + '<!]>'
: '',
html: payload.out
};
@ -284,17 +281,16 @@ export function attr(name, value, boolean) {
*/
export function css_props(payload, is_html, props, component) {
const styles = style_object_to_string(props);
const anchor = create_anchor(payload);
if (is_html) {
payload.out += `<div style="display: contents; ${styles}">${anchor}`;
payload.out += `<div style="display: contents; ${styles}"><![>`;
} else {
payload.out += `<g style="${styles}">${anchor}`;
payload.out += `<g style="${styles}"><![>`;
}
component();
if (is_html) {
payload.out += `${anchor}</div>`;
payload.out += `<!]></div>`;
} else {
payload.out += `${anchor}</g>`;
payload.out += `<!]></g>`;
}
}
@ -634,12 +630,6 @@ export function ensure_array_like(array_like_or_iterator) {
: Array.from(array_like_or_iterator);
}
/** @param {{ anchor: number }} payload */
export function create_anchor(payload) {
const depth = payload.anchor++;
return `<!ssr:${depth}>`;
}
/**
* @param {number} timeout
* @returns {() => void}

@ -12,7 +12,7 @@ function get_html(ssr) {
// ssr rendered HTML has an extra newline prefixed within `<pre>` tag,
// if the <pre> tag starts with `\n`
// because when browser parses the SSR rendered HTML, it will ignore the 1st '\n' character
return `${ssr ? '<!--ssr:0-->' : ''}<pre id="pre"> A
return `${ssr ? '<!--[-->' : ''}<pre id="pre"> A
B
<span>
C
@ -35,5 +35,5 @@ function get_html(ssr) {
leading newlines</pre></div> <div id="pre-without-leading-newline"><pre>without spaces</pre> <pre> with spaces </pre> <pre>${' '}
newline after leading space</pre></div> <pre id="pre-with-multiple-leading-newlines">
multiple leading newlines</pre>${ssr ? '<!--ssr:0-->' : ''}`;
multiple leading newlines</pre>${ssr ? '<!--]-->' : ''}`;
}

@ -11,7 +11,7 @@ export default test({
if (variant === 'dom') {
assert.ok(!span.previousSibling);
} else {
assert.ok(span.previousSibling?.textContent?.startsWith('ssr:')); // ssr commment node
assert.ok(span.previousSibling?.textContent === '['); // ssr commment node
}
component.raw = '<span>bar</span>';

@ -4,17 +4,17 @@ export default test({
withoutNormalizeHtml: true,
// Unable to test `html` with `<textarea>` content
// as the textarea#value will not show within `innerHtml`
ssrHtml: `<!--ssr:0--><textarea id="textarea"> A
ssrHtml: `<!--[--><textarea id="textarea"> A
B
</textarea> <div id="div-with-textarea"><textarea> A
B
</textarea></div> <div id="textarea-with-leading-newline"><textarea>leading newline</textarea> <textarea> leading newline and spaces</textarea> <textarea>
leading newlines</textarea></div> <div id="textarea-without-leading-newline"><textarea>without spaces</textarea> <textarea> with spaces </textarea> <textarea>
leading newlines</textarea></div> <div id="textarea-without-leading-newline"><textarea>without spaces</textarea> <textarea> with spaces </textarea> <textarea>${' '}
newline after leading space</textarea></div> <textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea> <div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
multiple leading newlines</textarea></div><!--ssr:0-->`,
multiple leading newlines</textarea></div><!--]-->`,
test({ assert, target }) {
// Test for <textarea> tag
const elementTextarea = /** @type {HTMLTextAreaElement} */ (target.querySelector('#textarea'));

@ -1,5 +1 @@
<!--ssr:0-->
<p>before</p>
<!-- a comment -->
<p>after</p>
<!--ssr:0-->
<![><p>before</p><!-- a comment --><p>after</p><!]>

@ -1 +1 @@
<!ssr:0><div>Just a dummy page.</div><!ssr:0>
<![><div>Just a dummy page.</div><!]>

@ -14,4 +14,4 @@ export default function Bind_this($$anchor, $$props) {
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
$.close_frag($$anchor, fragment);
$.pop();
}
}

@ -4,11 +4,8 @@ import * as $ from "svelte/internal/server";
export default function Bind_this($$payload, $$props) {
$.push(false);
const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`;
$$payload.out += `<![>`;
Foo($$payload, {});
$$payload.out += `${anchor}`;
$$payload.out += `<!]>`;
$.pop();
}

@ -45,4 +45,4 @@ export default function Main($$anchor, $$props) {
$.close_frag($$anchor, fragment);
$.pop();
}
}

@ -28,4 +28,4 @@ export default function Each_string_template($$anchor, $$props) {
$.close_frag($$anchor, fragment);
$.pop();
}
}

@ -5,18 +5,18 @@ import * as $ from "svelte/internal/server";
export default function Each_string_template($$payload, $$props) {
$.push(false);
const anchor = $.create_anchor($$payload);
const each_array = $.ensure_array_like(['foo', 'bar', 'baz']);
$$payload.out += `${anchor}`;
$$payload.out += `<![>`;
for (let $$index = 0; $$index < each_array.length; $$index++) {
const thing = each_array[$$index];
const anchor_1 = $.create_anchor($$payload);
$$payload.out += `${anchor_1}${$.escape(thing)}, ${anchor_1}`;
$$payload.out += "<![>";
$$payload.out += `${$.escape(thing)}, `;
$$payload.out += "<!]>";
}
$$payload.out += `${anchor}`;
$$payload.out += `<!]>`;
$.pop();
}

@ -33,4 +33,4 @@ export default function Function_prop_no_getter($$anchor, $$props) {
$.close_frag($$anchor, fragment);
$.pop();
}
}

@ -12,9 +12,8 @@ export default function Function_prop_no_getter($$payload, $$props) {
}
const plusOne = (num) => num + 1;
const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`;
$$payload.out += `<![>`;
Button($$payload, {
onmousedown: () => count += 1,
@ -25,6 +24,6 @@ export default function Function_prop_no_getter($$payload, $$props) {
}
});
$$payload.out += `${anchor}`;
$$payload.out += `<!]>`;
$.pop();
}

@ -14,4 +14,4 @@ export default function Hello_world($$anchor, $$props) {
$.close($$anchor, h1);
$.pop();
}
}

@ -36,4 +36,4 @@ export default function State_proxy_literal($$anchor, $$props) {
$.pop();
}
$.delegate(["click"]);
$.delegate(["click"]);

@ -14,4 +14,4 @@ export default function Svelte_element($$anchor, $$props) {
$.element(node, tag, false);
$.close_frag($$anchor, fragment);
$.pop();
}
}

@ -6,10 +6,9 @@ export default function Svelte_element($$payload, $$props) {
$.push(true);
let { tag = 'hr' } = $$props;
const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`;
$$payload.out += `<![>`;
if (tag) $.element($$payload, tag, () => {}, () => {});
$$payload.out += `${anchor}`;
$$payload.out += `<!]>`;
$.pop();
}
Loading…
Cancel
Save