Merge branch 'main' into more-precise-validate-dynamic-component

pull/12452/head
paoloricciuti 2 years ago
commit 71fbcbdc9b

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: hydrate multiple `<svelte:head>` elements correctly

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: assign correct scope to attributes of named slot

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add createRawSnippet API

@ -196,6 +196,7 @@
"fresh-walls-bathe", "fresh-walls-bathe",
"fresh-weeks-trade", "fresh-weeks-trade",
"fresh-wombats-learn", "fresh-wombats-learn",
"fresh-zoos-burn",
"friendly-candles-relate", "friendly-candles-relate",
"friendly-clouds-rhyme", "friendly-clouds-rhyme",
"friendly-lies-camp", "friendly-lies-camp",

@ -1,5 +1,11 @@
# svelte # svelte
## 5.0.0-next.189
### Patch Changes
- feat: add createRawSnippet API ([#12425](https://github.com/sveltejs/svelte/pull/12425))
## 5.0.0-next.188 ## 5.0.0-next.188
### Patch Changes ### Patch Changes

@ -250,7 +250,7 @@
## snippet_invalid_rest_parameter ## snippet_invalid_rest_parameter
> snippets do not support rest parameters; use an array instead > Snippets do not support rest parameters; use an array instead
## snippet_shadowing_prop ## snippet_shadowing_prop

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.188", "version": "5.0.0-next.189",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -1108,12 +1108,12 @@ export function snippet_conflict(node) {
} }
/** /**
* snippets do not support rest parameters; use an array instead * Snippets do not support rest parameters; use an array instead
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function snippet_invalid_rest_parameter(node) { export function snippet_invalid_rest_parameter(node) {
e(node, "snippet_invalid_rest_parameter", "snippets do not support rest parameters; use an array instead"); e(node, "snippet_invalid_rest_parameter", "Snippets do not support rest parameters; use an array instead");
} }
/** /**

@ -674,6 +674,12 @@ const validation = {
SnippetBlock(node, context) { SnippetBlock(node, context) {
validate_block_not_empty(node.body, context); validate_block_not_empty(node.body, context);
for (const arg of node.parameters) {
if (arg.type === 'RestElement') {
e.snippet_invalid_rest_parameter(arg);
}
}
context.next({ ...context.state, parent_element: null }); context.next({ ...context.state, parent_element: null });
const { path } = context; const { path } = context;

@ -386,16 +386,20 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
Component(node, { state, visit, path }) { Component(node, { state, visit, path }) {
state.scope.reference(b.id(node.name), path); state.scope.reference(b.id(node.name), path);
for (const attribute of node.attributes) {
visit(attribute);
}
// let:x is super weird: // let:x is super weird:
// - for the default slot, its scope only applies to children that are not slots themselves // - for the default slot, its scope only applies to children that are not slots themselves
// - for named slots, its scope applies to the component itself, too // - for named slots, its scope applies to the component itself, too
const [scope, is_default_slot] = analyze_let_directives(node, state.scope); const [scope, is_default_slot] = analyze_let_directives(node, state.scope);
if (!is_default_slot) { if (is_default_slot) {
for (const attribute of node.attributes) {
visit(attribute);
}
} else {
scopes.set(node, scope); scopes.set(node, scope);
for (const attribute of node.attributes) {
visit(attribute, { ...state, scope });
}
} }
for (const child of node.fragment.nodes) { for (const child of node.fragment.nodes) {

@ -190,3 +190,5 @@ export {
tick, tick,
untrack untrack
} from './internal/client/runtime.js'; } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -35,3 +35,5 @@ export function unmount() {
export async function tick() {} export async function tick() {}
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

@ -1,12 +1,16 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../../shared/validate.js'; import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect } from '../../reactivity/effects.js'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import { import {
dev_current_component_function, dev_current_component_function,
set_dev_current_component_function set_dev_current_component_function
} from '../../runtime.js'; } from '../../runtime.js';
import { hydrate_node, hydrating } from '../hydration.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
/** /**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -60,3 +64,40 @@ export function wrap_snippet(component, fn) {
} }
}); });
} }
/**
* Create a snippet programmatically
* @template {unknown[]} Params
* @param {(...params: Getters<Params>) => {
* render: () => string
* setup?: (element: Element) => void
* }} fn
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol(
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
anchor.before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
}
);
}

@ -51,6 +51,7 @@ export function head(render_fn) {
block(() => render_fn(anchor), HEAD_EFFECT); block(() => render_fn(anchor), HEAD_EFFECT);
} finally { } finally {
if (was_hydrating) { if (was_hydrating) {
head_anchor = hydrate_node; // so that next head block starts from the correct node
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
} }
} }

@ -0,0 +1,22 @@
/** @import { Snippet } from 'svelte' */
/** @import { Payload } from '#server' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../shared/validate.js';
/**
* Create a snippet programmatically
* @template {unknown[]} Params
* @param {(...params: Getters<Params>) => {
* render: () => string
* setup?: (element: Element) => void
* }} fn
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out += fn(...getters)
.render()
.trim();
});
}

@ -7,4 +7,8 @@ export type SourceLocation =
| [line: number, column: number] | [line: number, column: number]
| [line: number, column: number, SourceLocation[]]; | [line: number, column: number, SourceLocation[]];
export type Getters<T> = {
[K in keyof T]: () => T[K];
};
export type Snapshot<T> = ReturnType<typeof $state.snapshot<T>>; export type Snapshot<T> = ReturnType<typeof $state.snapshot<T>>;

@ -1,3 +1,5 @@
/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { is_void, IS_COMPONENT } from '../../constants.js'; import { is_void, IS_COMPONENT } from '../../constants.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -6,6 +8,7 @@ const snippet_symbol = Symbol.for('svelte.snippet');
/** /**
* @param {any} fn * @param {any} fn
* @returns {import('svelte').Snippet}
*/ */
export function add_snippet_symbol(fn) { export function add_snippet_symbol(fn) {
fn[snippet_symbol] = true; fn[snippet_symbol] = true;

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.188'; export const VERSION = '5.0.0-next.189';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'snippet_invalid_rest_parameter',
message: 'Snippets do not support rest parameters; use an array instead',
position: [19, 26]
}
});

@ -0,0 +1,3 @@
{#snippet children(...args)}
{args}
{/snippet}

@ -1,2 +1,2 @@
{@html '<meta name="head_nested_html" content="head_nested_html">'} {@html '<meta name="head_nested_html" content="head_nested_html">'}
<meta name="head_nested" content="head_nested"> <meta name="head_nested" content="head_nested" />

@ -1,5 +1,9 @@
<script>
let text = $state('foo');
</script>
<svelte:head> <svelte:head>
{@html '<meta name="nested_html" content="nested_html">'} {@html '<meta name="nested_html" content="nested_html">'}
<meta name="nested" content="nested"> <meta name="nested" content="nested" />
<meta name="foo" content={text} />
</svelte:head> </svelte:head>

@ -4,9 +4,12 @@
</script> </script>
<svelte:head> <svelte:head>
{@html '<meta name="main_html" content="main_html">'} <!-- the if block forces a comment node; tests that the nested head starts at the correct node -->
<meta name="main" content="main"> {#if true}
<HeadNested /> {@html '<meta name="main_html" content="main_html">'}
<meta name="main" content="main" />
<HeadNested />
{/if}
</svelte:head> </svelte:head>
<Nested/> <Nested />

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
snapshot(target) {
return {
p: target.querySelector('p')
};
}
});

@ -0,0 +1,14 @@
<script>
import { createRawSnippet } from 'svelte';
const snippet = createRawSnippet(() => ({
render: () => `
<p>rendered</p>
`,
setup(p) {
p.textContent = 'hydrated';
}
}));
</script>
{@render snippet()}

@ -0,0 +1,7 @@
<script lang="ts">
export let onclick;
</script>
<button {onclick}>
<slot />
</button>

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, logs, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.deepEqual(logs, [1]);
}
});

@ -0,0 +1,8 @@
<script lang="ts">
import Parent from './Parent.svelte';
import Child from './Child.svelte';
</script>
<Parent>
<Child slot="item" let:item onclick={() => console.log(item)}>asd</Child>
</Parent>

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ target, assert, logs }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.deepEqual(logs, ['tearing down']);
}
});

@ -0,0 +1,18 @@
<script>
import { createRawSnippet } from 'svelte';
let show = $state(true);
const snippet = createRawSnippet(() => ({
render: () => `<hr>`,
setup(p) {
return () => console.log('tearing down')
}
}));
</script>
<button onclick={() => show = !show}>click</button>
{#if show}
{@render snippet()}
{/if}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown
},
html: `<button>click</button><p>clicks: 0</p>`,
test({ target, assert }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>click</button><p>clicks: 1</p>`);
}
});

@ -0,0 +1,20 @@
<script>
import { createRawSnippet } from 'svelte';
let count = $state(0);
const hello = createRawSnippet((count) => ({
render: () => `
<p>clicks: ${count()}</p>
`,
setup(p) {
$effect(() => {
p.textContent = `clicks: ${count()}`
});
}
}));
</script>
<button onclick={() => count += 1}>click</button>
{@render hello(count)}

@ -365,6 +365,13 @@ declare module 'svelte' {
export function flushSync(fn?: (() => void) | undefined): void; export function flushSync(fn?: (() => void) | undefined): void;
/** Anything except a function */ /** Anything except a function */
type NotFunction<T> = T extends Function ? never : T; type NotFunction<T> = T extends Function ? never : T;
/**
* Create a snippet programmatically
* */
export function createRawSnippet<Params extends unknown[]>(fn: (...params: Getters<Params>) => {
render: () => string;
setup?: (element: Element) => void;
}): Snippet<Params>;
/** /**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`. * Transitions will play during the initial render unless the `intro` option is set to `false`.
@ -450,6 +457,9 @@ declare module 'svelte' {
* https://svelte.dev/docs/svelte#getallcontexts * https://svelte.dev/docs/svelte#getallcontexts
* */ * */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T; export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
export {}; export {};
} }

@ -256,6 +256,10 @@ We can tighten things up further by declaring a generic, so that `data` and `row
</script> </script>
``` ```
## Creating snippets programmatically
In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet)
## Snippets and slots ## Snippets and slots
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.

@ -93,6 +93,37 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
</script> </script>
``` ```
### `createRawSnippet`
An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags:
```js
import { createRawSnippet } from 'svelte';
const greet = createRawSnippet((name) => {
return {
render: () => `
<h1>Hello ${name()}!</h1>
`,
setup: (node) => {
$effect(() => {
node.textContent = `Hello ${name()}!`;
});
}
};
});
```
The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element.
The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes:
```svelte
{@render greet(name)}
```
If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether.
## `svelte/reactivity` ## `svelte/reactivity`
Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA) Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA)

Loading…
Cancel
Save