fix: use operations functions everywhere

pull/18058/head
paoloricciuti 4 months ago
parent ff5e835bf0
commit f91c2fd6d8

@ -69,7 +69,6 @@ export const STALE_REACTION = new (class StaleReactionError extends Error {
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
// TODO: DOM access
export const IS_XHTML =
// We gotta write it like this because after downleveling the pure comment may end up in the wrong location
!!globalThis.document?.contentType &&

@ -1,3 +1,5 @@
import { remove_node } from '../dom/operations';
/** @type {Map<String, Set<HTMLStyleElement>>} */
var all_styles = new Map();
@ -24,7 +26,7 @@ export function cleanup_styles(hash) {
if (!styles) return;
for (const style of styles) {
style.remove();
remove_node(style);
}
all_styles.delete(hash);

@ -3,6 +3,7 @@ import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/cons
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
import { get_first_child, get_next_sibling, get_node_value, node_type } from '../dom/operations.js';
/**
* @param {any} fn
@ -14,7 +15,12 @@ export function add_locations(fn, filename, locations) {
return (/** @type {any[]} */ ...args) => {
const dom = fn(...args);
var node = hydrating ? dom : dom.nodeType === DOCUMENT_FRAGMENT_NODE ? dom.firstChild : dom;
// check here
var node = hydrating
? dom
: node_type(dom) === DOCUMENT_FRAGMENT_NODE
? get_first_child(dom)
: dom;
assign_locations(node, filename, locations);
return dom;
@ -28,13 +34,14 @@ export function add_locations(fn, filename, locations) {
*/
function assign_location(element, filename, location) {
// @ts-expect-error
// TODO RENDERER: what if the element is not an object?
element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] }
};
if (location[2]) {
assign_locations(element.firstChild, filename, location[2]);
assign_locations(get_first_child(element), filename, location[2]);
}
}
@ -48,16 +55,17 @@ function assign_locations(node, filename, locations) {
var depth = 0;
while (node && i < locations.length) {
if (hydrating && node.nodeType === COMMENT_NODE) {
if (hydrating && node_type(node) === COMMENT_NODE) {
var comment = /** @type {Comment} */ (node);
if (comment.data[0] === HYDRATION_START) depth += 1;
else if (comment.data[0] === HYDRATION_END) depth -= 1;
const data = get_node_value(comment) ?? '';
if (data[0] === HYDRATION_START) depth += 1;
else if (data[0] === HYDRATION_END) depth -= 1;
}
if (depth === 0 && node.nodeType === ELEMENT_NODE) {
if (depth === 0 && node_type(node) === ELEMENT_NODE) {
assign_location(/** @type {Element} */ (node), filename, locations[i++]);
}
node = node.nextSibling;
node = get_next_sibling(node);
}
}

@ -4,6 +4,7 @@ import * as e from '../errors.js';
* @param {...(()=>any)[]} args
*/
export function validate_snippet_args(anchor, ...args) {
// TODO RENDERER: don't emit validate args for custom renderers
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
e.invalid_snippet_arguments();
}

@ -15,6 +15,7 @@ import { is_runes } from '../../context.js';
import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
import { get_node_value } from '../operations.js';
const PENDING = 0;
const THEN = 1;
@ -57,7 +58,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
let mismatch =
hydrating && is_promise(input) === (get_node_value(node) === HYDRATION_START_ELSE);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh

@ -1,6 +1,6 @@
import { render_effect } from '../../reactivity/effects.js';
import { hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
import { get_first_child, style_set_property, style_remove_property } from '../operations.js';
/**
* @param {HTMLDivElement | SVGGElement} element
@ -19,9 +19,9 @@ export function css_props(element, get_styles) {
var value = styles[key];
if (value) {
element.style.setProperty(key, value);
style_set_property(/** @type {HTMLElement} */ (element), key, value);
} else {
element.style.removeProperty(key);
style_remove_property(/** @type {HTMLElement} */ (element), key);
}
}
});

@ -14,7 +14,17 @@ import * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js';
import { create_element, get_first_child, get_next_sibling } from '../operations.js';
import {
create_element,
get_first_child,
get_next_sibling,
get_last_child,
set_inner_html,
insert_before,
get_parent_node,
node_type,
get_node_value
} from '../operations.js';
import { active_effect } from '../../runtime.js';
import { COMMENT_NODE } from '#client/constants';
@ -81,12 +91,12 @@ export function html(
// When @html is the only child, use innerHTML directly.
// This also handles contenteditable, where the user may delete the anchor comment.
effect.nodes = null;
parent_node.innerHTML = /** @type {string} */ (value);
set_inner_html(parent_node, /** @type {string} */ (value));
if (value !== '') {
assign_nodes(
/** @type {TemplateNode} */ (get_first_child(parent_node)),
/** @type {TemplateNode} */ (parent_node.lastChild)
/** @type {TemplateNode} */ (get_last_child(parent_node))
);
}
@ -103,16 +113,13 @@ export function html(
if (hydrating) {
// We're deliberately not trying to repair mismatches between server and client,
// as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
var hash = /** @type {Comment} */ (hydrate_node).data;
var hash = get_node_value(hydrate_node);
/** @type {TemplateNode | null} */
var next = hydrate_next();
var last = next;
while (
next !== null &&
(next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
) {
while (next !== null && (node_type(next) !== COMMENT_NODE || get_node_value(next) !== '')) {
last = next;
next = get_next_sibling(next);
}
@ -123,7 +130,7 @@ export function html(
}
if (DEV && !skip_warning) {
check_hash(/** @type {Element} */ (next.parentNode), hash, value);
check_hash(/** @type {Element} */ (get_parent_node(next)), hash, value);
}
assign_nodes(hydrate_node, last);
@ -139,22 +146,22 @@ export function html(
var wrapper = /** @type {HTMLTemplateElement | SVGElement | MathMLElement} */ (
create_element(svg ? 'svg' : mathml ? 'math' : 'template', ns)
);
wrapper.innerHTML = /** @type {any} */ (value);
set_inner_html(wrapper, /** @type {any} */ (value));
/** @type {DocumentFragment | Element} */
var node = svg || mathml ? wrapper : /** @type {HTMLTemplateElement} */ (wrapper).content;
assign_nodes(
/** @type {TemplateNode} */ (get_first_child(node)),
/** @type {TemplateNode} */ (node.lastChild)
/** @type {TemplateNode} */ (get_last_child(node))
);
if (svg || mathml) {
while (get_first_child(node)) {
anchor.before(/** @type {TemplateNode} */ (get_first_child(node)));
insert_before(anchor, /** @type {TemplateNode} */ (get_first_child(node)));
}
} else {
anchor.before(node);
insert_before(anchor, node);
}
});
}

@ -13,7 +13,7 @@ import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { get_first_child, get_next_sibling, insert_before, node_type } from '../operations.js';
import { prevent_snippet_stringification } from '../../../shared/validate.js';
import { BranchManager } from './branches.js';
@ -71,6 +71,7 @@ export function wrap_snippet(component, fn) {
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
// TODO RENDERER: throw an error for createRawSnippet in a non-browser environment, as it relies on DOM APIs
// @ts-expect-error the types are a lie
return (/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
@ -86,11 +87,11 @@ export function createRawSnippet(fn) {
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {
if (DEV && (get_next_sibling(element) !== null || node_type(element) !== ELEMENT_NODE)) {
w.invalid_raw_snippet_render();
}
anchor.before(element);
insert_before(anchor, element);
}
const result = snippet.setup?.(element);

@ -7,7 +7,15 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { create_element, create_text, get_first_child } from '../operations.js';
import {
create_element,
create_text,
get_first_child,
append_child,
create_comment,
insert_before,
node_type
} from '../operations.js';
import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { active_effect } from '../../runtime.js';
@ -40,7 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {null | Element} */
var element = null;
if (hydrating && hydrate_node.nodeType === ELEMENT_NODE) {
if (hydrating && node_type(hydrate_node) === ELEMENT_NODE) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
}
@ -90,14 +98,14 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (render_fn) {
if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches
element.append(document.createComment(''));
append_child(element, create_comment(''));
}
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? get_first_child(element)
: element.appendChild(create_text());
: /** @type {Text} */ (append_child(element, create_text()));
if (hydrating) {
if (child_anchor === null) {
@ -121,7 +129,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// we do this after calling `render_fn` so that child effects don't override `nodes.end`
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
anchor.before(element);
insert_before(anchor, element);
}
if (hydrating) {

@ -1,6 +1,14 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import {
create_text,
get_first_child,
get_next_sibling,
remove_node,
append_child,
node_type,
get_node_value
} from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, EFFECT_PRESERVED, HEAD_EFFECT } from '#client/constants';
@ -10,6 +18,7 @@ import { COMMENT_NODE, EFFECT_PRESERVED, HEAD_EFFECT } from '#client/constants';
* @returns {void}
*/
export function head(hash, render_fn) {
// TODO RENDERER: throw in case we are using custom renderers as this rely on the head element
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_node = null;
@ -27,7 +36,7 @@ export function head(hash, render_fn) {
// rendered in an arbitrary order — find one corresponding to this component
while (
head_anchor !== null &&
(head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
(node_type(head_anchor) !== COMMENT_NODE || get_node_value(head_anchor) !== hash)
) {
head_anchor = get_next_sibling(head_anchor);
}
@ -38,14 +47,14 @@ export function head(hash, render_fn) {
set_hydrating(false);
} else {
var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
head_anchor.remove(); // in case this component is repeated
remove_node(/** @type {ChildNode} */ (head_anchor)); // in case this component is repeated
set_hydrate_node(start);
}
}
if (!hydrating) {
anchor = document.head.appendChild(create_text());
anchor = /** @type {Comment | Text} */ (append_child(document.head, create_text()));
}
try {

@ -1,29 +1,39 @@
import { DEV } from 'esm-env';
import { register_style } from '../dev/css.js';
import { effect } from '../reactivity/effects.js';
import { create_element } from './operations.js';
import {
create_element,
get_root_node,
query_selector,
append_child,
set_text_content
} from './operations.js';
/**
* @param {Node} anchor
* @param {{ hash: string, code: string }} css
*/
export function append_styles(anchor, css) {
// TODO RENDERER: disallow css inject with custom renderer?
// Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
effect(() => {
var root = anchor.getRootNode();
var root = get_root_node(anchor);
// TODO: DOM access
var target = /** @type {ShadowRoot} */ (root).host
? /** @type {ShadowRoot} */ (root)
: /** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;
: // TODO: DOM access
/** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;
// Always querying the DOM is roughly the same perf as additionally checking for presence in a map first assuming
// that you'll get cache hits half of the time, so we just always query the dom for simplicity and code savings.
if (!target.querySelector('#' + css.hash)) {
if (!query_selector(/** @type {Element} */ (target), '#' + css.hash)) {
const style = create_element('style');
// TODO: DOM access
style.id = css.hash;
style.textContent = css.code;
set_text_content(style, css.code);
target.appendChild(style);
append_child(target, style);
if (DEV) {
register_style(css.hash, style);

@ -1,5 +1,5 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { clear_text_content, get_first_child, focus, add_event_listener } from '../operations.js';
import { queue_micro_task } from '../task.js';
/**
@ -14,7 +14,7 @@ export function autofocus(dom, value) {
queue_micro_task(() => {
if (document.activeElement === body) {
dom.focus();
focus(dom);
}
});
}
@ -27,6 +27,7 @@ export function autofocus(dom, value) {
* @returns {void}
*/
export function remove_textarea_child(dom) {
// TODO RENDERER: never output in custom render mode
if (hydrating && get_first_child(dom) !== null) {
clear_text_content(dom);
}
@ -35,15 +36,19 @@ export function remove_textarea_child(dom) {
let listening_to_form_reset = false;
export function add_form_reset_listener() {
// TODO RENDERER: never output in custom render mode
if (!listening_to_form_reset) {
listening_to_form_reset = true;
document.addEventListener(
// TODO: DOM access
add_event_listener(
document,
'reset',
(evt) => {
// Needs to happen one tick later or else the dom properties of the form
// elements have not updated to their reset values yet
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
// TODO: DOM access
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();

@ -8,7 +8,7 @@ import {
HYDRATION_START_ELSE
} from '../../../constants.js';
import * as w from '../warnings.js';
import { get_next_sibling } from './operations.js';
import { get_next_sibling, get_node_value, node_type, remove_node } from './operations.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@ -89,8 +89,8 @@ export function skip_nodes(remove = true) {
var node = hydrate_node;
while (true) {
if (node.nodeType === COMMENT_NODE) {
var data = /** @type {Comment} */ (node).data;
if (node_type(node) === COMMENT_NODE) {
var data = get_node_value(node) ?? '';
if (data === HYDRATION_END) {
if (depth === 0) return node;
@ -106,7 +106,7 @@ export function skip_nodes(remove = true) {
}
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
if (remove) node.remove();
if (remove) remove_node(node);
node = next;
}
}
@ -116,10 +116,10 @@ export function skip_nodes(remove = true) {
* @param {TemplateNode} node
*/
export function read_hydration_instruction(node) {
if (!node || node.nodeType !== COMMENT_NODE) {
if (!node || node_type(node) !== COMMENT_NODE) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return /** @type {Comment} */ (node).data;
return get_node_value(node) ?? '';
}

@ -1,4 +1,4 @@
import { create_element } from './operations.js';
import { create_element, set_inner_html } from './operations.js';
const policy =
// We gotta write it like this because after downleveling the pure comment may end up in the wrong location
@ -20,6 +20,6 @@ export function create_trusted_html(html) {
*/
export function create_fragment_from_html(html) {
var elem = create_element('template');
elem.innerHTML = create_trusted_html(html.replaceAll('<!>', '<!---->')); // XHTML compliance
set_inner_html(elem, create_trusted_html(html.replaceAll('<!>', '<!---->'))); // XHTML compliance
return elem.content;
}

@ -208,7 +208,7 @@ function fragment_from_tree(structure, ns) {
if (children.length > 0) {
var target =
node_name(element) === TEMPLATE_TAG
? // TODO: DOM access
? // this is safe for custom renderers because the name will never be `template` due to how `node_name` works
/** @type {HTMLTemplateElement} */ (element).content
: element;
@ -283,6 +283,7 @@ export function from_tree(structure, flags) {
* @param {() => Element | DocumentFragment} fn
*/
export function with_script(fn) {
// TODO RENDERER: never emit this for custom renderers
return () => run_scripts(fn());
}
@ -293,6 +294,7 @@ export function with_script(fn) {
* @returns {Node | Node[]}
*/
function run_scripts(node) {
// this should be custom renderer safe since we never emit `with_script` in that case
// scripts were SSR'd, in which case they will run
if (hydrating) return node;
@ -307,12 +309,11 @@ function run_scripts(node) {
for (const script of scripts) {
const clone = create_element('script');
// TODO: DOM access
for (var attribute of script.attributes) {
set_attribute(clone, attribute.name, attribute.value);
}
// TODO: DOM access
set_text_content(clone, script.textContent ?? '');
// The script has changed - if it's at the edges, the effect now points at dead nodes

@ -6,7 +6,15 @@ import {
create_text,
get_first_child,
get_next_sibling,
init_operations
init_operations,
append_child,
add_event_listener,
remove_event_listener,
get_parent_node,
remove_child,
set_node_value,
get_node_value,
node_type
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js';
@ -47,10 +55,10 @@ export function set_text(text, value) {
// For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
var str = value == null ? '' : typeof value === 'object' ? `${value}` : value;
// @ts-expect-error
if (str !== (text.__t ??= text.nodeValue)) {
if (str !== (text.__t ??= get_node_value(text))) {
// @ts-expect-error
text.__t = str;
text.nodeValue = `${str}`;
set_node_value(text, `${str}`);
}
}
@ -105,7 +113,7 @@ export function hydrate(component, options) {
while (
anchor &&
(anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
(node_type(anchor) !== COMMENT_NODE || get_node_value(anchor) !== HYDRATION_START)
) {
anchor = get_next_sibling(anchor);
}
@ -171,7 +179,7 @@ function _mount(
var component = undefined;
var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text());
var anchor_node = anchor ?? /** @type {Text} */ (append_child(target, create_text()));
boundary(
/** @type {TemplateNode} */ (anchor_node),
@ -202,8 +210,8 @@ function _mount(
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
node_type(hydrate_node) !== COMMENT_NODE ||
get_node_value(hydrate_node) !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
@ -246,7 +254,7 @@ function _mount(
var count = counts.get(event_name);
if (count === undefined) {
node.addEventListener(event_name, handle_event_propagation, { passive });
add_event_listener(node, event_name, handle_event_propagation, { passive });
counts.set(event_name, 1);
} else {
counts.set(event_name, count + 1);
@ -265,7 +273,7 @@ function _mount(
var count = /** @type {number} */ (counts.get(event_name));
if (--count == 0) {
node.removeEventListener(event_name, handle_event_propagation);
remove_event_listener(node, event_name, handle_event_propagation);
counts.delete(event_name);
if (counts.size === 0) {
@ -280,7 +288,8 @@ function _mount(
root_event_handles.delete(event_handle);
if (anchor_node !== anchor) {
anchor_node.parentNode?.removeChild(anchor_node);
var parent = get_parent_node(anchor_node);
if (parent) remove_child(parent, /** @type {ChildNode} */ (anchor_node));
}
};
});

Loading…
Cancel
Save