Skip to content

feat: add idPrefix to render #15428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wise-grapes-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: Add `idPrefix` option to `render`
1 change: 1 addition & 0 deletions packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ComponentConstructorOptions<
intro?: boolean;
recover?: boolean;
sync?: boolean;
idPrefix?: string;
$$inline?: boolean;
}

Expand Down
14 changes: 6 additions & 8 deletions packages/svelte/src/internal/client/dom/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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++}`;
}
7 changes: 4 additions & 3 deletions packages/svelte/src/internal/disclose-version.js
Original file line number Diff line number Diff line change
@@ -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);
}
13 changes: 9 additions & 4 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,26 @@ 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++}`;
}

/**
* Only available on the server and when compiling with the `server` option.
* 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: '',
Expand Down
12 changes: 10 additions & 2 deletions packages/svelte/src/server/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
11 changes: 7 additions & 4 deletions packages/svelte/tests/hydration/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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>;
/**
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/tests/runtime-browser/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/tests/runtime-browser/driver-ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
2 changes: 1 addition & 1 deletion packages/svelte/tests/runtime-browser/test-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
10 changes: 7 additions & 3 deletions packages/svelte/tests/runtime-legacy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let id = $props.id();
</script>

<p>{id}</p>
Original file line number Diff line number Diff line change
@@ -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>
`
);
}
}
});
Original file line number Diff line number Diff line change
@@ -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}
3 changes: 2 additions & 1 deletion packages/svelte/tests/server-side-rendering/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand All @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare module 'svelte' {
intro?: boolean;
recover?: boolean;
sync?: boolean;
idPrefix?: string;
$$inline?: boolean;
}

Expand Down Expand Up @@ -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 {
Expand Down