feat: add `idPrefix` to `render` (#15428)

This ensures someone like Astro can render multiple islands and the unique ids produced in them are able to be deduplicated

---------

Co-authored-by: Hugos68 <hugokorteapple@gmail.com>
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/15431/head
Rich Harris 6 months ago committed by GitHub
parent 7ce2dfc622
commit b82692af2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: Add `idPrefix` option to `render`

@ -19,6 +19,7 @@ export interface ComponentConstructorOptions<
intro?: boolean;
recover?: boolean;
sync?: boolean;
idPrefix?: string;
$$inline?: boolean;
}

@ -250,12 +250,6 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
let uid = 1;
export function reset_props_id() {
uid = 1;
}
/**
* Create (or hydrate) an unique UID for the component instance.
*/
@ -264,12 +258,16 @@ export function props_id() {
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s')
hydrate_node.textContent?.startsWith(`#`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}
return 'c' + uid++;
// @ts-expect-error This way we ensure the id is unique even across Svelte runtimes
(window.__svelte ??= {}).uid ??= 1;
// @ts-expect-error
return `c${window.__svelte.uid++}`;
}

@ -1,5 +1,6 @@
import { PUBLIC_VERSION } from '../version.js';
if (typeof window !== 'undefined')
// @ts-ignore
(window.__svelte ||= { v: new Set() }).v.add(PUBLIC_VERSION);
if (typeof window !== 'undefined') {
// @ts-expect-error
((window.__svelte ??= {}).v ??= new Set()).add(PUBLIC_VERSION);
}

@ -86,9 +86,14 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
function props_id_generator() {
/**
* Creates an ID generator
* @param {string} prefix
* @returns {() => string}
*/
function props_id_generator(prefix) {
let uid = 1;
return () => 's' + uid++;
return () => `${prefix}s${uid++}`;
}
/**
@ -96,11 +101,11 @@ function props_id_generator() {
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
const uid = props_id_generator();
const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : '');
/** @type {Payload} */
const payload = {
out: '',

@ -12,10 +12,18 @@ export function render<
...args: {} extends Props
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: { props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: { props: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }
options: {
props: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
): RenderOutput;

@ -2,9 +2,9 @@
import * as fs from 'node:fs';
import { assert } from 'vitest';
import { compile_directory, should_update_expected } from '../helpers.js';
import { compile_directory } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { suite, assert_ok, type BaseTest } from '../suite.js';
import { assert_ok, suite, type BaseTest } from '../suite.js';
import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler';
@ -13,6 +13,7 @@ import { flushSync } from 'svelte';
interface HydrationTest extends BaseTest {
load_compiled?: boolean;
server_props?: Record<string, any>;
id_prefix?: string;
props?: Record<string, any>;
compileOptions?: Partial<CompileOptions>;
/**
@ -50,7 +51,8 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
const head = window.document.head;
const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, {
props: config.server_props ?? config.props ?? {}
props: config.server_props ?? config.props ?? {},
idPrefix: config?.id_prefix
});
const override = read(`${cwd}/_override.html`);
@ -103,7 +105,8 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
component: (await import(`${cwd}/_output/client/main.svelte.js`)).default,
target,
hydrate: true,
props: config.props
props: config.props,
idPrefix: config?.id_prefix
});
console.warn = warn;

@ -119,6 +119,7 @@ function normalize_children(node) {
* skip_mode?: Array<'server' | 'client' | 'hydrate'>;
* html?: string;
* ssrHtml?: string;
* id_prefix?: string;
* props?: Props;
* compileOptions?: Partial<CompileOptions>;
* test?: (args: {

@ -6,5 +6,5 @@ import config from '__CONFIG__';
import { render } from 'svelte/server';
export default function () {
return render(SvelteComponent, { props: config.props || {} });
return render(SvelteComponent, { props: config.props || {}, idPrefix: config?.id_prefix });
}

@ -20,7 +20,7 @@ export async function run_ssr_test(
await compile_directory(test_dir, 'server', config.compileOptions);
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
const { body } = render(Component, { props: config.props || {} });
const { body } = render(Component, { props: config.props || {}, idPrefix: config.id_prefix });
fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);

@ -11,7 +11,6 @@ import { setup_html_equal } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { reset_props_id } from '../../src/internal/client/dom/template.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -37,6 +36,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
compileOptions?: Partial<CompileOptions>;
props?: Props;
server_props?: Props;
id_prefix?: string;
before_test?: () => void;
after_test?: () => void;
test?: (args: {
@ -285,7 +285,8 @@ async function run_test_variant(
// ssr into target
const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default;
const { html, head } = render(SsrSvelteComponent, {
props: config.server_props ?? config.props ?? {}
props: config.server_props ?? config.props ?? {},
idPrefix: config.id_prefix
});
fs.writeFileSync(`${cwd}/_output/rendered.html`, html);
@ -346,7 +347,10 @@ async function run_test_variant(
if (runes) {
props = proxy({ ...(config.props || {}) });
reset_props_id();
// @ts-expect-error
globalThis.__svelte.uid = 1;
if (manual_hydrate) {
hydrate_fn = () => {
instance = hydrate(mod.default, {

@ -0,0 +1,5 @@
<script>
let id = $props.id();
</script>
<p>{id}</p>

@ -0,0 +1,60 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
id_prefix: 'myPrefix',
test({ assert, target, variant }) {
if (variant === 'dom') {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>c1</h1>
<p>c2</p>
<p>c3</p>
<p>c4</p>
`
);
} else {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>myPrefix-s1</h1>
<p>myPrefix-s2</p>
<p>myPrefix-s3</p>
<p>myPrefix-s4</p>
`
);
}
let button = target.querySelector('button');
flushSync(() => button?.click());
if (variant === 'dom') {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>c1</h1>
<p>c2</p>
<p>c3</p>
<p>c4</p>
<p>c5</p>
`
);
} else {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>myPrefix-s1</h1>
<p>myPrefix-s2</p>
<p>myPrefix-s3</p>
<p>myPrefix-s4</p>
<p>c1</p>
`
);
}
}
});

@ -0,0 +1,19 @@
<script>
import Child from './Child.svelte';
let id = $props.id();
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<h1>{id}</h1>
<Child />
<Child />
<Child />
{#if show}
<Child />
{/if}

@ -15,6 +15,7 @@ import type { CompileOptions } from '#compiler';
interface SSRTest extends BaseTest {
compileOptions?: Partial<CompileOptions>;
props?: Record<string, any>;
id_prefix?: string;
withoutNormalizeHtml?: boolean;
errors?: string[];
}
@ -33,7 +34,7 @@ const { test, run } = suite<SSRTest>(async (config, test_dir) => {
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
const expected_html = try_read_file(`${test_dir}/_expected.html`);
const rendered = render(Component, { props: config.props || {} });
const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix });
const { body, head } = rendered;
fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);

@ -16,6 +16,7 @@ declare module 'svelte' {
intro?: boolean;
recover?: boolean;
sync?: boolean;
idPrefix?: string;
$$inline?: boolean;
}
@ -2080,11 +2081,19 @@ declare module 'svelte/server' {
...args: {} extends Props
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: { props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: { props: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }
options: {
props: Omit<Props, '$$slots' | '$$events'>;
context?: Map<any, any>;
idPrefix?: string;
}
]
): RenderOutput;
interface RenderOutput {

Loading…
Cancel
Save