feat: custom renderers api

pull/18058/head
paoloricciuti 6 months ago
parent 98e8b635fa
commit a03b3f3014

@ -90,6 +90,10 @@
"./reactivity/window": { "./reactivity/window": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js" "default": "./src/reactivity/window/index.js"
},
"./renderer": {
"types": "./types/index.d.ts",
"default": "./src/renderer/index.js"
}, },
"./server": { "./server": {
"types": "./types/index.d.ts", "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 // 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 // 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"); 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}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`, [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.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}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/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); e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary break; // eslint doesn't know this is unnecessary
} }
case 'customRenderer': {
component_options.customRenderer = get_static_value(attribute);
break;
}
case 'customElement': { case 'customElement': {
/** @type {AST.SvelteOptions['customElement']} */ /** @type {AST.SvelteOptions['customElement']} */
const ce = {}; const ce = {};

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

@ -138,6 +138,19 @@ const visitors = {
VariableDeclaration 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 {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options * @param {ValidatedCompileOptions} options
@ -151,7 +164,9 @@ export function client_component(analysis, options) {
scope: analysis.module.scope, scope: analysis.module.scope,
scopes: analysis.module.scopes, scopes: analysis.module.scopes,
is_instance: false, 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 node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [], legacy_reactive_imports: [],
legacy_reactive_statements: new Map(), 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 = let should_inject_props =
should_inject_context || should_inject_context ||
analysis.needs_props || analysis.needs_props ||
@ -562,7 +584,9 @@ export function client_component(analysis, options) {
body.unshift(b.imports([], 'svelte/internal/flags/tracing')); 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')); body.unshift(b.imports([], 'svelte/internal/disclose-version'));
} }

@ -35,7 +35,8 @@ function build_locations(nodes) {
* @param {number} [flags] * @param {number} [flags]
*/ */
export function transform_template(state, namespace, flags = 0) { 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(); const expression = tree ? state.template.as_tree() : state.template.as_html();

@ -47,7 +47,8 @@ export function RegularElement(node, context) {
return; 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 // 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 // custom element until the template is connected to the dom, which would

@ -132,6 +132,10 @@ export interface CompileOptions extends ModuleCompileOptions {
* @since 5.33 * @since 5.33
*/ */
fragments?: 'html' | 'tree'; 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 `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. * 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; preserveWhitespace?: boolean;
namespace?: Namespace; namespace?: Namespace;
css?: 'injected'; css?: 'injected';
customRenderer?: string;
customElement?: { customElement?: {
tag?: string; tag?: string;
shadow?: 'open' | 'none' | ObjectExpression | undefined; shadow?: 'open' | 'none';
props?: Record< props?: Record<
string, 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 * @param {string} source
* @returns {ESTree.ImportDeclaration} * @returns {ESTree.ImportDeclaration}
*/ */
@ -647,7 +647,7 @@ export function imports(parts, source) {
attributes: [], attributes: [],
source: literal(source), source: literal(source),
specifiers: parts.map((p) => ({ specifiers: parts.map((p) => ({
type: 'ImportSpecifier', type: p[2] ? 'ImportDefaultSpecifier' : 'ImportSpecifier',
imported: id(p[0]), imported: id(p[0]),
local: id(p[1]) local: id(p[1])
})) }))

@ -83,6 +83,8 @@ const component_options = {
immutable: deprecate(w.options_deprecated_immutable, boolean(false)), immutable: deprecate(w.options_deprecated_immutable, boolean(false)),
customRenderer: string(undefined),
legacy: removed( legacy: removed(
'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' '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 { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js'; export { log_if_contains_state } from './dev/console-log.js';
export { invoke_error_boundary } from './error-handling.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 * @since 5.33
*/ */
fragments?: 'html' | 'tree'; 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 `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. * 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; preserveWhitespace?: boolean;
namespace?: Namespace; namespace?: Namespace;
css?: 'injected'; css?: 'injected';
customRenderer?: string;
customElement?: { customElement?: {
tag?: string; tag?: string;
shadow?: 'open' | 'none' | ObjectExpression | undefined; shadow?: 'open' | 'none';
props?: Record< props?: Record<
string, string,
{ {
@ -2557,6 +2562,12 @@ declare module 'svelte/reactivity/window' {
export {}; export {};
} }
declare module 'svelte/renderer' {
export function createRenderer(renderer: any): any;
export {};
}
declare module 'svelte/server' { declare module 'svelte/server' {
import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte';
/** /**
@ -3065,6 +3076,10 @@ declare module 'svelte/types/compiler/interfaces' {
* @since 5.33 * @since 5.33
*/ */
fragments?: 'html' | 'tree'; 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 `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. * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.

Loading…
Cancel
Save