Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 in `render`/`mount`/`hydrate` functions
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function CallExpression(node, context) {
}

context.state.analysis.props_id = parent.id;
context.state.analysis.needs_context = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also revert this if we don't use context anymore


break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,13 @@ export function client_component(analysis, options) {

// we want the cleanup function for the stores to run as the very last thing
// so that it can effectively clean up the store subscription even after the user effects runs
// if we have $props.id `should_inject_context` will always be true
if (should_inject_context) {
// we need to put the `$props.id` after the `$.push` because the `component_context` will be properly initialized
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
}
Comment on lines +403 to +407
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we don't use context we can revert this change too

component_block.body.unshift(b.stmt(b.call('$.push', ...push_args)));

let to_push;
Expand Down Expand Up @@ -562,11 +568,6 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}

if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
}

if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))
Expand Down
5 changes: 5 additions & 0 deletions packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ComponentConstructorOptions<
props?: Props;
context?: Map<any, any>;
hydrate?: boolean;
idPrefix?: string;
intro?: boolean;
recover?: boolean;
sync?: boolean;
Expand Down Expand Up @@ -337,6 +338,10 @@ export type MountOptions<Props extends Record<string, any> = Record<string, any>
* @default true
*/
intro?: boolean;
/**
* Provide a prefix for the generated ID from `$props.id`
*/
idPrefix?: string;
} & ({} extends Props
? {
/**
Expand Down
7 changes: 7 additions & 0 deletions packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export function set_hydrating(value) {
hydrating = value;
}

export let id_prefix = '';

/** @param {string} v */
export function set_id_prefix(v) {
id_prefix = v;
}

/**
* The node that is currently being hydrated. This starts out as the first node inside the opening
* <!--[--> comment, and updates each time a component calls `$.child(...)` or `$.sibling(...)`.
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte/src/internal/client/dom/template.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { hydrate_next, hydrate_node, hydrating, id_prefix, set_hydrate_node } from './hydration.js';
import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
Expand Down Expand Up @@ -264,12 +264,12 @@ export function props_id() {
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s')
hydrate_node.textContent?.startsWith(`#${id_prefix}s`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}

return 'c' + uid++;
return `${id_prefix}c${uid++}`;
}
18 changes: 14 additions & 4 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating
set_hydrating,
set_id_prefix
} from './dom/hydration.js';
import { array_from } from '../shared/utils.js';
import {
Expand Down Expand Up @@ -86,13 +87,15 @@ export function mount(component, options) {
* context?: Map<any, any>;
* intro?: boolean;
* recover?: boolean;
* idPrefix?: string;
* } : {
* target: Document | Element | ShadowRoot;
* props: Props;
* events?: Record<string, (e: any) => any>;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: boolean;
* idPrefix?: string;
* }} options
* @returns {Exports}
*/
Expand Down Expand Up @@ -165,7 +168,10 @@ const document_listeners = new Map();
* @param {MountOptions} options
* @returns {Exports}
*/
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
function _mount(
Component,
{ target, anchor, props = {}, events, context, intro = true, idPrefix }
) {
init_operations();

var registered_events = new Set();
Expand Down Expand Up @@ -209,10 +215,14 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
var anchor_node = anchor ?? target.appendChild(create_text());

branch(() => {
if (context) {
if (context || idPrefix != null) {
push({});
var ctx = /** @type {ComponentContext} */ (component_context);
ctx.c = context;
if (context) {
ctx.c = context;
}

set_id_prefix(idPrefix ? idPrefix + '-' : '');
}

if (events) {
Expand Down
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
3 changes: 2 additions & 1 deletion packages/svelte/src/legacy/legacy-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ class Svelte4Component {
props,
context: options.context,
intro: options.intro ?? false,
recover: options.recover
recover: options.recover,
idPrefix: options.idPrefix
});

// We don't flushSync for custom element wrappers or if the user doesn't want it
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;
21 changes: 12 additions & 9 deletions packages/svelte/tests/hydration/test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// @vitest-environment jsdom

import type { CompileOptions } from '#compiler';
import * as fs from 'node:fs';
import { assert } from 'vitest';
import { compile_directory, should_update_expected } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { suite, assert_ok, type BaseTest } from '../suite.js';
import { flushSync } from 'svelte';
import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler';
import { flushSync } from 'svelte';
import { assert } from 'vitest';
import { compile_directory } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { assert_ok, suite, type BaseTest } from '../suite.js';

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 Expand Up @@ -164,6 +167,6 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
config.after_test?.();
}
});
export { test, assert_ok };
export { assert_ok, test };

await run(__dirname);
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
7 changes: 5 additions & 2 deletions packages/svelte/tests/runtime-legacy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 +286,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 @@ -362,7 +364,8 @@ async function run_test_variant(
target,
props,
intro: config.intro,
recover: config.recover ?? false
recover: config.recover ?? false,
idPrefix: config.id_prefix
});
}
} else {
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>myPrefix-c1</h1>
<p>myPrefix-c2</p>
<p>myPrefix-c3</p>
<p>myPrefix-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>myPrefix-c1</h1>
<p>myPrefix-c2</p>
<p>myPrefix-c3</p>
<p>myPrefix-c4</p>
<p>myPrefix-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>myPrefix-c1</p>
`
);
}
}
});
Loading
Loading