diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 216bf08b5c..1f732fb9f7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -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') { // Component name itself could be blocked on async values memoizer.check_blockers(node.metadata.expression); diff --git a/packages/svelte/src/internal/client/custom-renderer/state.js b/packages/svelte/src/internal/client/custom-renderer/state.js index 123e333edd..a46e03f3eb 100644 --- a/packages/svelte/src/internal/client/custom-renderer/state.js +++ b/packages/svelte/src/internal/client/custom-renderer/state.js @@ -28,3 +28,19 @@ export function push_renderer($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; + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 436240ce7a..5fc98fe48b 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -181,4 +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'; +export { push_renderer, without_renderer } from './custom-renderer/state.js'; diff --git a/packages/svelte/tests/custom-renderers/renderer.js b/packages/svelte/tests/custom-renderers/renderer.js index cec57ccc53..a92161d4f6 100644 --- a/packages/svelte/tests/custom-renderers/renderer.js +++ b/packages/svelte/tests/custom-renderers/renderer.js @@ -11,7 +11,7 @@ import { createRenderer } from '../../src/renderer/index.js'; /** * @typedef {{ type: 'element', name: string, attributes: Record, children: ObjNode[], listeners: Record>, parent: ObjNode | null }} ObjElement * @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 {ObjElement | ObjText | ObjComment | ObjFragment} ObjNode */ @@ -61,6 +61,11 @@ function remove_from_parent(node) { children.splice(idx, 1); } +/** + * @type {Array} + */ +export const dom_elements = []; + const renderer = createRenderer({ createFragment() { return /** @type {ObjFragment} */ ({ @@ -93,7 +98,12 @@ const renderer = createRenderer({ return /** @type {ObjComment} */ ({ type: 'comment', 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); + } }); }, diff --git a/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/Child.svelte b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/Child.svelte new file mode 100644 index 0000000000..189c1a8a8c --- /dev/null +++ b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/Child.svelte @@ -0,0 +1,9 @@ + + + + +
+ {message} +
diff --git a/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/_config.js b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/_config.js new file mode 100644 index 0000000000..21ff9d835e --- /dev/null +++ b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/_config.js @@ -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: '', + 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, '
hello from child
'); + } +}); diff --git a/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/main.svelte b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/main.svelte new file mode 100644 index 0000000000..87fac4cc79 --- /dev/null +++ b/packages/svelte/tests/custom-renderers/samples-dom/dom-child-component/main.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/custom-renderers/shared.ts b/packages/svelte/tests/custom-renderers/shared.ts index c8d735c9aa..8213e7624b 100644 --- a/packages/svelte/tests/custom-renderers/shared.ts +++ b/packages/svelte/tests/custom-renderers/shared.ts @@ -4,7 +4,7 @@ import { assert } from 'vitest'; import { compile_directory } from '../helpers.js'; import { suite_with_variants, type BaseTest } from '../suite.js'; 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'; export interface CustomRendererTest extends BaseTest { @@ -26,6 +26,7 @@ export interface CustomRendererTest extends BaseTest { renderer: typeof renderer; serialize: typeof serialize; dispatch_event: typeof dispatch_event; + dom_elements: Array; }) => void | Promise; } @@ -134,8 +135,8 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options let unmount: (() => void) | undefined; let component: Record | undefined; - try { + dom_elements.length = 0; const result = renderer.render(mod.default, { target, props: config.props ?? {}, @@ -184,7 +185,8 @@ async function run_test(cwd: string, config: CustomRendererTest, compile_options warnings, renderer: renderer, serialize, - dispatch_event + dispatch_event, + dom_elements }); } diff --git a/packages/svelte/tests/custom-renderers/test-dom.test.ts b/packages/svelte/tests/custom-renderers/test-dom.test.ts new file mode 100644 index 0000000000..1029614bc4 --- /dev/null +++ b/packages/svelte/tests/custom-renderers/test-dom.test.ts @@ -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');