feat: custom renderer api

custom-render-shim-dom
paoloricciuti 5 months ago
parent be398671ca
commit b043b28d45

@ -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, '');

@ -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;
}

@ -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),

@ -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)
);

@ -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.

@ -112,6 +112,8 @@ export const validate_component_options =
templatingMode: list(['string', 'functional']),
customRenderer: boolean(false),
preserveWhitespace: boolean(false),
runes: boolean(undefined),

@ -338,6 +338,10 @@ export type MountOptions<Props extends Record<string, any> = Record<string, any>
* @default true
*/
intro?: boolean;
/**
* The custom renderer to use to mount the component.
*/
customRenderer?: any;
} & ({} extends Props
? {
/**

@ -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) {

@ -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;
}

@ -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,

@ -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;
}

@ -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<any, any>;
* intro?: boolean;
* recover?: boolean;
* customRenderer?: any;
* } : {
* target: Document | Element | ShadowRoot;
* props: Props;
@ -93,10 +95,15 @@ export function mount(component, options) {
* context?: Map<any, any>;
* 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;
}

@ -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<Error>} 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;
}

@ -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()
};
}

@ -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.

Loading…
Cancel
Save