diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 42427dd9c4..d3527460b1 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -20,6 +20,7 @@ export { default as preprocess } from './preprocess/index.js'; * @returns {CompileResult} */ export function compile(source, options) { + options.customRenderer = true; source = remove_bom(source); state.reset_warning_filter(options.warningFilter); const validated = validate_component_options(options, ''); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 3ba81767cc..05c8a99ae5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -67,7 +67,7 @@ export function Attribute(node, context) { const expression = get_attribute_expression(node); const delegated_event = get_delegated_event(node.name.slice(2), expression, context); - if (delegated_event !== null) { + if (delegated_event !== null && !context.state.options.customRenderer) { if (delegated_event.hoisted) { delegated_event.function.metadata.hoisted = true; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index da0a89fb2f..0f4f1f2bf2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -167,7 +167,7 @@ export function client_component(analysis, options) { in_constructor: false, instance_level_snippets: [], module_level_snippets: [], - is_functional_template_mode: options.templatingMode === 'functional', + is_functional_template_mode: options.customRenderer || options.templatingMode === 'functional', // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index 2667a96f6a..e140a9dfeb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -68,7 +68,8 @@ export function visit_event_attribute(node, context) { context.state.node, handler, capture, - is_passive_event(event_name) ? true : undefined + is_passive_event(event_name) ? true : undefined, + context.state.options.customRenderer ) ); @@ -90,13 +91,15 @@ export function visit_event_attribute(node, context) { * @param {Expression} handler * @param {boolean} capture * @param {boolean | undefined} passive + * @param {boolean | undefined} custom_renderer */ -export function build_event(event_name, node, handler, capture, passive) { +export function build_event(event_name, node, handler, capture, passive, custom_renderer) { return b.call( '$.event', b.literal(event_name), node, handler, + custom_renderer && b.true, capture && b.true, passive === undefined ? undefined : b.literal(passive) ); diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 4aa244ad51..513fa0caa1 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -119,6 +119,10 @@ export interface CompileOptions extends ModuleCompileOptions { * @default 'string' */ templatingMode?: 'string' | 'functional'; + /** + * If `true` the output will be adapted to accept a custom renderer. + */ + customRenderer?: boolean; /** * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage. diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index 1d67951fd8..187e80875b 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -112,6 +112,8 @@ export const validate_component_options = templatingMode: list(['string', 'functional']), + customRenderer: boolean(false), + preserveWhitespace: boolean(false), runes: boolean(undefined), diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 38e6086689..a6d573fd8d 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -338,6 +338,10 @@ export type MountOptions = Record * @default true */ intro?: boolean; + /** + * The custom renderer to use to mount the component. + */ + customRenderer?: any; } & ({} extends Props ? { /** diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 0c1bb1dada..4b2392ba72 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -51,12 +51,21 @@ export function replay_events(dom) { * @param {EventTarget} dom * @param {EventListener} [handler] * @param {AddEventListenerOptions} [options] + * @param {boolean} [custom_renderer] */ -export function create_event(event_name, dom, handler, options = {}) { +export function create_event(event_name, dom, handler, options = {}, custom_renderer = false) { /** * @this {EventTarget} */ function target_handler(/** @type {Event} */ event) { + // if we have a custom renderer we just want to call the function + // without a reactive context because we don't know if event propagation + // is even a thing in the target renderer + if (custom_renderer) { + return without_reactive_context(() => { + return handler?.call(this, event); + }); + } if (!options.capture) { // Only call in the bubble phase, else delegated events would be called before the capturing events handle_event_propagation.call(dom, event); @@ -109,13 +118,14 @@ export function on(element, type, handler, options = {}) { * @param {string} event_name * @param {Element} dom * @param {EventListener} [handler] + * @param {boolean} [custom_renderer] * @param {boolean} [capture] * @param {boolean} [passive] * @returns {void} */ -export function event(event_name, dom, handler, capture, passive) { +export function event(event_name, dom, handler, custom_renderer, capture, passive) { var options = { capture, passive }; - var target_handler = create_event(event_name, dom, handler, options); + var target_handler = create_event(event_name, dom, handler, options, custom_renderer); // @ts-ignore if (dom === document.body || dom === window || dom === document) { diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 97062f04e3..1416723726 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -19,6 +19,8 @@ var first_child_getter; /** @type {() => Node | null} */ var next_sibling_getter; +var renderer; + /** * Initialize these lazily to avoid issues when using the runtime in a server context * where these globals are not available while avoiding a separate server entry point @@ -28,20 +30,23 @@ export function init_operations() { return; } - $window = window; - $document = document; - is_firefox = /Firefox/.test(navigator.userAgent); + $window = typeof window === 'undefined' ? /** @type {Window} */ ({}) : window; + $document = typeof document === 'undefined' ? /** @type {Document} */ ({}) : document; + is_firefox = typeof navigator === 'undefined' ? true : /Firefox/.test(navigator.userAgent); - var element_prototype = Element.prototype; - var node_prototype = Node.prototype; - var text_prototype = Text.prototype; + var element_prototype; + if (window.Element) element_prototype = Element.prototype; + var node_prototype; + if (window.Node) node_prototype = Node.prototype; + var text_prototype; + if (window.Text) text_prototype = Text.prototype; // @ts-ignore - first_child_getter = get_descriptor(node_prototype, 'firstChild').get; + first_child_getter = get_descriptor(node_prototype, 'firstChild')?.get; // @ts-ignore - next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get; + next_sibling_getter = get_descriptor(node_prototype, 'nextSibling')?.get; - if (is_extensible(element_prototype)) { + if (element_prototype && is_extensible(element_prototype)) { // the following assignments improve perf of lookups on DOM nodes // @ts-expect-error element_prototype.__click = undefined; @@ -73,6 +78,9 @@ export function init_operations() { * @returns {Text} */ export function create_text(value = '') { + if (renderer) { + return renderer.document.createTextNode(value); + } return document.createTextNode(value); } @@ -83,6 +91,9 @@ export function create_text(value = '') { */ /*@__NO_SIDE_EFFECTS__*/ export function get_first_child(node) { + if (renderer_first_child) { + return renderer_first_child.call(node); + } return first_child_getter.call(node); } @@ -93,6 +104,9 @@ export function get_first_child(node) { */ /*@__NO_SIDE_EFFECTS__*/ export function get_next_sibling(node) { + if (renderer_next_sibling) { + return renderer_next_sibling.call(node); + } return next_sibling_getter.call(node); } @@ -213,6 +227,9 @@ export function clear_text_content(node) { * @returns */ export function create_element(tag, namespace, is) { + if (renderer) { + return renderer.document.createElement(tag); + } let options = is ? { is } : undefined; if (namespace) { return document.createElementNS(namespace, tag, options); @@ -221,6 +238,9 @@ export function create_element(tag, namespace, is) { } export function create_fragment() { + if (renderer) { + return renderer.document.createDocumentFragment(); + } return document.createDocumentFragment(); } @@ -229,6 +249,9 @@ export function create_fragment() { * @returns */ export function create_comment(data = '') { + if (renderer) { + return renderer.document.createComment(data); + } return document.createComment(data); } @@ -245,3 +268,24 @@ export function set_attribute(element, key, value = '') { } return element.setAttribute(key, value); } + +var renderer_next_sibling; +var renderer_first_child; + +export function push_renderer(custom_renderer) { + var old_next_sibling = renderer_next_sibling; + var old_first_child = renderer_first_child; + var old_renderer = renderer; + renderer_next_sibling = get_descriptor(custom_renderer.Node.prototype, 'nextSibling')?.get; + renderer_first_child = get_descriptor(custom_renderer.Node.prototype, 'firstChild')?.get; + renderer = custom_renderer; + return () => { + renderer_next_sibling = old_next_sibling; + renderer_first_child = old_first_child; + renderer = old_renderer; + }; +} + +export function get_renderer() { + return renderer; +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 468bb94ab4..7022e9903d 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -38,7 +38,7 @@ import { set } from './sources.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; -import { get_next_sibling } from '../dom/operations.js'; +import { get_next_sibling, get_renderer } from '../dom/operations.js'; import { derived } from './deriveds.js'; import { component_context, dev_current_component_function } from '../context.js'; @@ -99,6 +99,8 @@ function create_effect(type, fn, sync, push = true) { nodes_end: null, f: type | DIRTY, first: null, + // we only need to update the renderer for render effects + renderer: (type & RENDER_EFFECT) !== 0 ? get_renderer() : undefined, fn, last: null, next: null, diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 5ef0097649..a76cef9fe2 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -67,6 +67,8 @@ export interface Effect extends Reaction { last: null | Effect; /** Parent effect */ parent: Effect | null; + /** The original renderer used when mounting the root component */ + renderer?: any; /** Dev only */ component_function?: any; } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 3256fe8274..4985ca9a89 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -6,7 +6,8 @@ import { create_text, get_first_child, get_next_sibling, - init_operations + init_operations, + push_renderer } from './dom/operations.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { active_effect } from './runtime.js'; @@ -86,6 +87,7 @@ export function mount(component, options) { * context?: Map; * intro?: boolean; * recover?: boolean; + * customRenderer?: any; * } : { * target: Document | Element | ShadowRoot; * props: Props; @@ -93,10 +95,15 @@ export function mount(component, options) { * context?: Map; * intro?: boolean; * recover?: boolean; + * customRenderer?: any; * }} options * @returns {Exports} */ export function hydrate(component, options) { + let cleanup_renderer = undefined; + if (options.customRenderer) { + cleanup_renderer = push_renderer(options.customRenderer); + } init_operations(); options.intro = options.intro ?? false; const target = options.target; @@ -153,6 +160,7 @@ export function hydrate(component, options) { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); reset_head_anchor(); + cleanup_renderer?.(); } } @@ -165,7 +173,14 @@ const document_listeners = new Map(); * @param {MountOptions} options * @returns {Exports} */ -function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) { +function _mount( + Component, + { target, anchor, props = {}, events, context, intro = true, customRenderer } +) { + let cleanup_renderer = undefined; + if (customRenderer) { + cleanup_renderer = push_renderer(customRenderer); + } init_operations(); var registered_events = new Set(); @@ -261,6 +276,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro }); mounted_components.set(component, unmount); + cleanup_renderer?.(); return component; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a5d26412a4..c7dc3a8294 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,7 +39,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { is_firefox } from './dom/operations.js'; +import { is_firefox, push_renderer } from './dom/operations.js'; // Used for DEV time error handling /** @param {WeakSet} value */ @@ -421,6 +421,13 @@ export function update_reaction(reaction) { reaction_sources = null; set_component_context(reaction.ctx); + var cleanup_renderer = undefined; + if ((reaction.f & DERIVED) === 0) { + var effect = /** @type {Effect} */ (reaction); + if (effect.renderer) { + cleanup_renderer = push_renderer(effect.renderer); + } + } untracking = false; read_version++; @@ -498,6 +505,7 @@ export function update_reaction(reaction) { reaction_sources = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + cleanup_renderer?.(); reaction.f ^= EFFECT_IS_UPDATING; } diff --git a/packages/svelte/src/renderer/index.ts b/packages/svelte/src/renderer/index.ts new file mode 100644 index 0000000000..8a26007f8d --- /dev/null +++ b/packages/svelte/src/renderer/index.ts @@ -0,0 +1,145 @@ +const ELEMENT_NODE_TYPE = 1; +const TEXT_NODE_TYPE = 3; +const COMMENT_NODE_TYPE = 8; +const FRAGMENT_NODE_TYPE = 11; + +export function createRenderer(opts) { + class Node { + tagName; + data; + parent; + nodeType; + nodeName; + + #element; + #children = []; + + constructor(node_type, element, tag) { + this.tagName = tag; + this.nodeName = tag; + this.#element = element; + this.nodeType = node_type; + } + + get element() { + return this.#element; + } + + get childNodes() { + return this.#children; + } + + get firstChild() { + return this.#children[0]; + } + + get lastChild() { + return this.#children[this.#children.lenght - 1]; + } + + get nextSibling() { + if (this.parent) { + const idx = this.parent.#children.findIndex((el) => el === this); + return this.parent.#children[idx + 1] ?? null; + } + return null; + } + + get className() { + return this.getAttribute('class'); + } + + set className(className) { + this.setAttribute('class', className); + } + + setAttribute(key, value) { + opts.setAttribute(this.#element, key, value); + } + + getAttribute(key) { + return opts.getAttribute(this.#element, key); + } + + hasAttribute(key) { + return !!this.getAttribute(key); + } + + removeAttribute(key) { + this.setAttribute(key, null); + } + + addEventListener(name, handler, options) { + if (this.nodeType === FRAGMENT_NODE_TYPE) return; + opts.addEventListener(this.#element, name, handler, options); + } + + remove() { + if (this.parent) { + this.parent.#children = this.parent.#children.filter((el) => el !== this); + } + } + + appendChild(child) { + this.append(child); + return child; + } + + before(node) { + if (this.parent) { + const idx = this.parent.#children.findIndex((el) => el === this); + const nodes = node.nodeType === FRAGMENT_NODE_TYPE ? node.childNodes : [node]; + this.parent.#children.splice(idx, 0, ...nodes); + for (let node of nodes) { + opts.insert(node.#element, this.parent.#element, this.#element); + node.parent = this.parent; + } + } + } + + append(...nodes) { + for (let node of nodes) { + node.parent = this; + this.#children.push(node); + if (this.nodeType !== FRAGMENT_NODE_TYPE) { + opts.insert(node.#element, this.#element); + } + } + } + + cloneNode() { + const cloned = new Node( + this.nodeType, + this.#element != null ? opts.cloneNode(this.#element) : this.#element, + this.tagName + ); + for (let node of this.#children) { + cloned.append(node.cloneNode()); + } + return cloned; + } + } + + class Document { + createElement(tag) { + const element = opts.createElement(tag); + return new Node(ELEMENT_NODE_TYPE, element, tag); + } + createDocumentFragment() { + return new Node(FRAGMENT_NODE_TYPE); + } + createComment(data) { + const comment = opts.createComment(data); + return new Node(COMMENT_NODE_TYPE, comment, data); + } + createTextNode(data) { + const element = opts.createTextNode(data); + return new Node(TEXT_NODE_TYPE, element, data); + } + } + + return { + Node, + document: new Document() + }; +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 19bb6e9279..f2553db5c4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -850,6 +850,10 @@ declare module 'svelte/compiler' { * @default 'string' */ templatingMode?: 'string' | 'functional'; + /** + * Allow to specify a module that exports a custom renderer for svelte components. + */ + customRenderer?: string; /** * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage. @@ -2566,6 +2570,10 @@ declare module 'svelte/types/compiler/interfaces' { * @default 'string' */ templatingMode?: 'string' | 'functional'; + /** + * Allow to specify a module that exports a custom renderer for svelte components. + */ + customRenderer?: string; /** * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.