fix: allow DOM components to be mounted into custom renderers (require manual handling)

pull/18058/head
paoloricciuti 5 months ago
parent 7f9021a056
commit ce4442d663

@ -456,6 +456,13 @@ export function build_component(node, component_name, loc, context) {
}; };
} }
if (context.state.analysis.custom_renderer) {
const prev = fn;
fn = (node_id) => {
return b.call('$.without_renderer', b.arrow([], prev(node_id)));
};
}
if (node.type !== 'SvelteSelf') { if (node.type !== 'SvelteSelf') {
// Component name itself could be blocked on async values // Component name itself could be blocked on async values
memoizer.check_blockers(node.metadata.expression); memoizer.check_blockers(node.metadata.expression);

@ -28,3 +28,19 @@ export function push_renderer($renderer) {
renderer = old_renderer; renderer = old_renderer;
}; };
} }
/**
* @template T
* @param {() => T} fn
* @returns {T}
*/
export function without_renderer(fn) {
if (renderer === null) return fn();
let old_renderer = renderer;
renderer = null;
try {
return fn();
} finally {
renderer = old_renderer;
}
}

@ -181,4 +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'; export { push_renderer, without_renderer } from './custom-renderer/state.js';

@ -11,7 +11,7 @@ import { createRenderer } from '../../src/renderer/index.js';
/** /**
* @typedef {{ type: 'element', name: string, attributes: Record<string, string>, children: ObjNode[], listeners: Record<string, Array<{handler: any, options?: any}>>, parent: ObjNode | null }} ObjElement * @typedef {{ type: 'element', name: string, attributes: Record<string, string>, children: ObjNode[], listeners: Record<string, Array<{handler: any, options?: any}>>, parent: ObjNode | null }} ObjElement
* @typedef {{ type: 'text', value: string, parent: ObjNode | null }} ObjText * @typedef {{ type: 'text', value: string, parent: ObjNode | null }} ObjText
* @typedef {{ type: 'comment', value: string, parent: ObjNode | null }} ObjComment * @typedef {{ type: 'comment', value: string, parent: ObjNode | null, before: (node: any)=> void }} ObjComment
* @typedef {{ type: 'fragment', children: ObjNode[], parent: ObjNode | null }} ObjFragment * @typedef {{ type: 'fragment', children: ObjNode[], parent: ObjNode | null }} ObjFragment
* @typedef {ObjElement | ObjText | ObjComment | ObjFragment} ObjNode * @typedef {ObjElement | ObjText | ObjComment | ObjFragment} ObjNode
*/ */
@ -61,6 +61,11 @@ function remove_from_parent(node) {
children.splice(idx, 1); children.splice(idx, 1);
} }
/**
* @type {Array<DocumentFragment | Node>}
*/
export const dom_elements = [];
const renderer = createRenderer({ const renderer = createRenderer({
createFragment() { createFragment() {
return /** @type {ObjFragment} */ ({ return /** @type {ObjFragment} */ ({
@ -93,7 +98,12 @@ const renderer = createRenderer({
return /** @type {ObjComment} */ ({ return /** @type {ObjComment} */ ({
type: 'comment', type: 'comment',
value: data, value: data,
parent: null parent: null,
// adding this allows for this renderer to interleave with a DOM-based renderer
// the argument will be the DOM node that represent a DOM Component being mounted
before(node) {
dom_elements.push(node);
}
}); });
}, },

@ -0,0 +1,9 @@
<svelte:options customRenderer={null} />
<script>
let { message } = $props();
</script>
<div>
<span>{message}</span>
</div>

@ -0,0 +1,14 @@
import { test } from '../../test-dom.test';
export default test({
// this is the custom rendered component...it doesn't have anything inside because the part of the renderer
// responsible for the interleaving is the `before` function on the comment node which only push into `dom_elements` in this case
html: '<custom></custom>',
test({ assert, dom_elements }) {
// we then get the element out of dom_elements
const [div] = dom_elements;
// check that is an actual DOM element and that it has the expected content
assert.instanceOf(div, HTMLDivElement);
assert.equal(div.outerHTML, '<div><span>hello from child</span></div>');
}
});

@ -0,0 +1,7 @@
<script>
import Child from './Child.svelte';
</script>
<custom>
<Child message="hello from child"></Child>
</custom>

@ -4,7 +4,7 @@ import { assert } from 'vitest';
import { compile_directory } from '../helpers.js'; import { compile_directory } from '../helpers.js';
import { suite_with_variants, type BaseTest } from '../suite.js'; import { suite_with_variants, type BaseTest } from '../suite.js';
import type { CompileOptions } from '#compiler'; import type { CompileOptions } from '#compiler';
import renderer, { create_root, serialize, dispatch_event } from './renderer.js'; import renderer, { create_root, serialize, dispatch_event, dom_elements } from './renderer.js';
import { writeFile } from 'node:fs/promises'; import { writeFile } from 'node:fs/promises';
export interface CustomRendererTest extends BaseTest { export interface CustomRendererTest extends BaseTest {
@ -26,6 +26,7 @@ export interface CustomRendererTest extends BaseTest {
renderer: typeof renderer; renderer: typeof renderer;
serialize: typeof serialize; serialize: typeof serialize;
dispatch_event: typeof dispatch_event; dispatch_event: typeof dispatch_event;
dom_elements: Array<DocumentFragment | Node>;
}) => void | Promise<void>; }) => void | Promise<void>;
} }
@ -134,8 +135,8 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options
let unmount: (() => void) | undefined; let unmount: (() => void) | undefined;
let component: Record<string, any> | undefined; let component: Record<string, any> | undefined;
try { try {
dom_elements.length = 0;
const result = renderer.render(mod.default, { const result = renderer.render(mod.default, {
target, target,
props: config.props ?? {}, props: config.props ?? {},
@ -184,7 +185,8 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options
warnings, warnings,
renderer: renderer, renderer: renderer,
serialize, serialize,
dispatch_event dispatch_event,
dom_elements
}); });
} }

@ -0,0 +1,8 @@
// @vitest-environment jsdom
import { custom_renderer_suite, ok } from './shared';
const { test, run } = custom_renderer_suite();
export { test, ok };
await run(__dirname, 'samples-dom');
Loading…
Cancel
Save