Skip to content
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