feat: custom renderers api

pull/18058/head
paoloricciuti 1 month ago
parent 98e8b635fa
commit a03b3f3014

@ -90,6 +90,10 @@
"./reactivity/window": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js"
},
"./renderer": {
"types": "./types/index.d.ts",
"default": "./src/renderer/index.js"
},
"./server": {
"types": "./types/index.d.ts",

@ -0,0 +1 @@
import './types/index.js';

@ -8,7 +8,7 @@ const pkg = JSON.parse(fs.readFileSync(`${dir}/package.json`, 'utf-8'));
// For people not using moduleResolution: 'bundler', we need to generate these files. Think about removing this in Svelte 6 or 7
// It may look weird, but the imports MUST be ending with index.js to be properly resolved in all TS modes
for (const name of ['action', 'animate', 'easing', 'motion', 'store', 'transition', 'legacy']) {
for (const name of ['action', 'animate', 'easing', 'motion', 'store', 'transition', 'legacy', 'renderer']) {
fs.writeFileSync(`${dir}/${name}.d.ts`, "import './types/index.js';\n");
}
@ -44,6 +44,7 @@ await createBundle({
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
[`${pkg.name}/renderer`]: `${dir}/src/internal/client/custom-renderer/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -36,6 +36,10 @@ export default function read_options(node) {
e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary
}
case 'customRenderer': {
component_options.customRenderer = get_static_value(attribute);
break;
}
case 'customElement': {
/** @type {AST.SvelteOptions['customElement']} */
const ce = {};

@ -59,8 +59,11 @@ export function Attribute(node, context) {
context.state.analysis.uses_event_attributes = true;
}
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
// we can't delegate event handlers in a non dom environment
if (!context.state.options.customRenderer) {
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
}
}
}

@ -138,6 +138,19 @@ const visitors = {
VariableDeclaration
};
/**
* @param {string} custom_renderer_module
*/
function custom_renderer_imports(custom_renderer_module) {
const imports = [
b.import_all('$', 'svelte/internal/client'),
]
if(custom_renderer_module){
imports.push(b.imports([['$renderer', '$renderer', true]], custom_renderer_module));
}
return imports;
}
/**
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
@ -151,7 +164,9 @@ export function client_component(analysis, options) {
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
hoisted: [
...custom_renderer_imports(options.customRenderer), ...analysis.instance_body.hoisted
],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
@ -392,6 +407,13 @@ export function client_component(analysis, options) {
);
}
if (options.customRenderer) {
component_block.body.unshift(
b.var('$$pop_renderer', b.call('$.push_renderer', b.id('$renderer')))
);
component_block.body.push(b.stmt(b.call('$$pop_renderer')));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
@ -562,7 +584,9 @@ export function client_component(analysis, options) {
body.unshift(b.imports([], 'svelte/internal/flags/tracing'));
}
if (options.discloseVersion) {
// disclose version attach the svelte version to `window` which is not guaranteed
// to be a thing in custom renderers environments
if (options.discloseVersion && !options.customRenderer) {
body.unshift(b.imports([], 'svelte/internal/disclose-version'));
}

@ -35,7 +35,8 @@ function build_locations(nodes) {
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
const tree = state.options.fragments === 'tree';
// custom renderers needs a tree to work because there's no template element we can use
const tree = state.options.fragments === 'tree' || !!state.options.customRenderer;
const expression = tree ? state.template.as_tree() : state.template.as_html();

@ -47,7 +47,8 @@ export function RegularElement(node, context) {
return;
}
const is_custom_element = is_custom_element_node(node);
// we never treat elements as custom element in custom renderers, since we don't want to apply special handling to them (e.g. class merging)
const is_custom_element = is_custom_element_node(node) && !context.state.options.customRenderer;
// cloneNode is faster, but it does not instantiate the underlying class of the
// custom element until the template is connected to the dom, which would

@ -132,6 +132,10 @@ export interface CompileOptions extends ModuleCompileOptions {
* @since 5.33
*/
fragments?: 'html' | 'tree';
/**
* Path to a module that exports the custom renderer to use. When this is truthy templating mode will also be automatically set to `functional`
*/
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.

@ -85,9 +85,10 @@ export namespace AST {
preserveWhitespace?: boolean;
namespace?: Namespace;
css?: 'injected';
customRenderer?: string;
customElement?: {
tag?: string;
shadow?: 'open' | 'none' | ObjectExpression | undefined;
shadow?: 'open' | 'none';
props?: Record<
string,
{

@ -637,7 +637,7 @@ export function import_all(as, source) {
}
/**
* @param {Array<[string, string]>} parts
* @param {Array<[string, string] | [string, string, boolean]>} parts
* @param {string} source
* @returns {ESTree.ImportDeclaration}
*/
@ -647,7 +647,7 @@ export function imports(parts, source) {
attributes: [],
source: literal(source),
specifiers: parts.map((p) => ({
type: 'ImportSpecifier',
type: p[2] ? 'ImportDefaultSpecifier' : 'ImportSpecifier',
imported: id(p[0]),
local: id(p[1])
}))

@ -83,6 +83,8 @@ const component_options = {
immutable: deprecate(w.options_deprecated_immutable, boolean(false)),
customRenderer: string(undefined),
legacy: removed(
'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead'
),

@ -0,0 +1,28 @@
import { branch, effect_root } from "../reactivity/effects";
import { push_renderer } from "./state";
/**
* @param {*} renderer
* @returns
*/
export function createRenderer(renderer) {
return {
...renderer,
/**
* @param {*} Component
* @param {*} options
*/
render(Component, { target, props }) {
var cleanup = push_renderer(renderer);
const unmount = effect_root(() => {
var anchor = renderer.createComment('');
renderer.insert(target, anchor, null);
branch(() => {
Component(anchor, props);
});
});
cleanup();
return unmount;
}
};
}

@ -0,0 +1,20 @@
/**
* @type {any}
*/
let renderer = null;
export function get_renderer() {
return renderer;
}
/**
*
* @param {any} $renderer
*/
export function push_renderer($renderer) {
let old_renderer = renderer;
renderer = $renderer;
return () => {
renderer = old_renderer;
};
}

@ -181,3 +181,4 @@ export {
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
export { invoke_error_boundary } from './error-handling.js';
export { push_renderer } from "./custom-renderer/state.js"

@ -0,0 +1 @@
export { createRenderer } from '../internal/client/custom-renderer/index.js';

@ -1089,6 +1089,10 @@ declare module 'svelte/compiler' {
* @since 5.33
*/
fragments?: 'html' | 'tree';
/**
* Path to a module that exports the custom renderer to use. When this is truthy templating mode will also be automatically set to `functional`
*/
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.
@ -1240,9 +1244,10 @@ declare module 'svelte/compiler' {
preserveWhitespace?: boolean;
namespace?: Namespace;
css?: 'injected';
customRenderer?: string;
customElement?: {
tag?: string;
shadow?: 'open' | 'none' | ObjectExpression | undefined;
shadow?: 'open' | 'none';
props?: Record<
string,
{
@ -2557,6 +2562,12 @@ declare module 'svelte/reactivity/window' {
export {};
}
declare module 'svelte/renderer' {
export function createRenderer(renderer: any): any;
export {};
}
declare module 'svelte/server' {
import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte';
/**
@ -3065,6 +3076,10 @@ declare module 'svelte/types/compiler/interfaces' {
* @since 5.33
*/
fragments?: 'html' | 'tree';
/**
* Path to a module that exports the custom renderer to use. When this is truthy templating mode will also be automatically set to `functional`
*/
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