diff --git a/.changeset/nervous-dolphins-allow.md b/.changeset/nervous-dolphins-allow.md new file mode 100644 index 0000000000..c59dc94c06 --- /dev/null +++ b/.changeset/nervous-dolphins-allow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: add warning for invalid render function of createRawSnippet diff --git a/.changeset/pre.json b/.changeset/pre.json index 675c79f28c..5826fb7f66 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -185,6 +185,7 @@ "fluffy-dolls-share", "fluffy-humans-worry", "fluffy-ravens-juggle", + "forty-bikes-buy", "forty-comics-invent", "forty-dogs-divide", "forty-dolls-wave", @@ -193,6 +194,7 @@ "four-balloons-beam", "four-flies-hammer", "four-mice-hammer", + "four-papayas-turn", "four-pugs-listen", "fresh-beds-wash", "fresh-dots-destroy", diff --git a/documentation/docs/02-template-syntax/01-component-fundamentals.md b/documentation/docs/02-template-syntax/01-component-fundamentals.md index 5e4d78e59a..debe4828d2 100644 --- a/documentation/docs/02-template-syntax/01-component-fundamentals.md +++ b/documentation/docs/02-template-syntax/01-component-fundamentals.md @@ -72,13 +72,31 @@ If you're using TypeScript, you can declare the prop types: ```svelte +``` + +If you're using JavaScript, you can declare the prop types using JSDoc: + +```svelte + ``` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3c7523801d..b67d217c81 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.0.0-next.193 + +### Patch Changes + +- fix: improve validation error that occurs when using `{@render ...}` to render default slotted content ([#12521](https://github.com/sveltejs/svelte/pull/12521)) + +- fix: reset hydrate node after `hydrate(...)` ([#12512](https://github.com/sveltejs/svelte/pull/12512)) + ## 5.0.0-next.192 ### Patch Changes diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 9ffef25b01..7a5a495251 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -20,6 +20,10 @@ > Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location% +## invalid_raw_snippet_render + +> The `render` function passed to `createRawSnippet` should return HTML for a single element + ## lifecycle_double_unmount > Tried to unmount a component that was not mounted diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6484b8cb58..5067c4ad51 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.192", + "version": "5.0.0-next.193", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 76329a09bb..79bc9889e1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2352,7 +2352,6 @@ export const template_visitors = { EachBlock(node, context) { const each_node_meta = node.metadata; const collection = /** @type {Expression} */ (context.visit(node.expression)); - let each_item_is_reactive = true; if (!each_node_meta.is_controlled) { context.state.template.push(''); @@ -2362,36 +2361,29 @@ export const template_visitors = { context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection))); } - // The runtime needs to know what kind of each block this is in order to optimize for the - // key === item (we avoid extra allocations). In that case, the item doesn't need to be reactive. - // We can guarantee this by knowing that in order for the item of the each block to change, they - // would need to mutate the key/item directly in the array. Given that in runes mode we use === - // equality, we can apply a fast-path (as long as the index isn't reactive). - let each_type = 0; + let flags = 0; if ( node.key && (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index) ) { - each_type |= EACH_KEYED; - // If there's a destructuring, then we likely need the generated $$index - if (node.index || node.context.type !== 'Identifier') { - each_type |= EACH_INDEX_REACTIVE; + flags |= EACH_KEYED; + + if (node.index) { + flags |= EACH_INDEX_REACTIVE; } - if ( - context.state.analysis.runes && + + // In runes mode, if key === item, we don't need to wrap the item in a source + const key_is_item = node.key.type === 'Identifier' && node.context.type === 'Identifier' && - node.context.name === node.key.name && - (each_type & EACH_INDEX_REACTIVE) === 0 - ) { - // Fast-path for when the key === item - each_item_is_reactive = false; - } else { - each_type |= EACH_ITEM_REACTIVE; + node.context.name === node.key.name; + + if (!context.state.analysis.runes || !key_is_item) { + flags |= EACH_ITEM_REACTIVE; } } else { - each_type |= EACH_ITEM_REACTIVE; + flags |= EACH_ITEM_REACTIVE; } // Since `animate:` can only appear on elements that are the sole child of a keyed each block, @@ -2404,15 +2396,15 @@ export const template_visitors = { return child.attributes.some((attr) => attr.type === 'AnimateDirective'); }) ) { - each_type |= EACH_IS_ANIMATED; + flags |= EACH_IS_ANIMATED; } if (each_node_meta.is_controlled) { - each_type |= EACH_IS_CONTROLLED; + flags |= EACH_IS_CONTROLLED; } if (context.state.analysis.runes) { - each_type |= EACH_IS_STRICT_EQUALS; + flags |= EACH_IS_STRICT_EQUALS; } // If the array is a store expression, we need to invalidate it when the array is changed. @@ -2437,10 +2429,12 @@ export const template_visitors = { ); return [array, ...transitive_dependencies]; }); + if (each_node_meta.array_name) { indirect_dependencies.push(b.call(each_node_meta.array_name)); } else { indirect_dependencies.push(collection); + const transitive_dependencies = serialize_transitive_dependencies( each_node_meta.references, context @@ -2459,6 +2453,7 @@ export const template_visitors = { // into separate expressions, at which point this is called again with an identifier or member expression return serialize_set_binding(assignment, context, () => assignment); } + const left = object(assignment.left); const value = get_assignment_value(assignment, context); const invalidate = b.call( @@ -2499,10 +2494,12 @@ export const template_visitors = { const item_with_loc = with_loc(item, id); return b.call('$.unwrap', item_with_loc); }; + if (node.index) { const index_binding = /** @type {import('#compiler').Binding} */ ( context.state.scope.get(node.index) ); + index_binding.expression = (id) => { const index_with_loc = with_loc(index, id); return b.call('$.unwrap', index_with_loc); @@ -2565,7 +2562,7 @@ export const template_visitors = { declarations.push(b.let(node.index, index)); } - if (context.state.options.dev && (each_type & EACH_KEYED) !== 0) { + if (context.state.options.dev && (flags & EACH_KEYED) !== 0) { context.state.init.push( b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) ); @@ -2574,7 +2571,7 @@ export const template_visitors = { /** @type {Expression[]} */ const args = [ context.state.node, - b.literal(each_type), + b.literal(flags), each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), key_function, b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(block.body))) diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 2df4487536..846fbd671a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -10,6 +10,8 @@ import { import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; +import * as w from '../../warnings.js'; +import { DEV } from 'esm-env'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -88,6 +90,11 @@ export function createRawSnippet(fn) { var html = snippet.render().trim(); var fragment = create_fragment_from_html(html); element = /** @type {Element} */ (fragment.firstChild); + + if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) { + w.invalid_raw_snippet_render(); + } + /** @type {TemplateNode} */ (/** @type {unknown} */ (anchor)).before(element); } diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index e1a6aa4325..31f67a725a 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -60,6 +60,18 @@ export function hydration_mismatch(location) { } } +/** + * The `render` function passed to `createRawSnippet` should return HTML for a single element + */ +export function invalid_raw_snippet_render() { + if (DEV) { + console.warn(`%c[svelte] invalid_raw_snippet_render\n%cThe \`render\` function passed to \`createRawSnippet\` should return HTML for a single element`, bold, normal); + } else { + // TODO print a link to the documentation + console.warn("invalid_raw_snippet_render"); + } +} + /** * Tried to unmount a component that was not mounted */ diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 90457f42f6..b947c0ce3f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.192'; +export const VERSION = '5.0.0-next.193'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/_config.js b/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/_config.js new file mode 100644 index 0000000000..fb05b78e98 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + skip_mode: ['hydrate'], + + warnings: [ + 'The `render` function passed to `createRawSnippet` should return HTML for a single element' + ] +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/main.svelte new file mode 100644 index 0000000000..e35f01800d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-raw-snippet-invalid-render/main.svelte @@ -0,0 +1,11 @@ + + +{@render snippet()} diff --git a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte index 7aa2ee78c6..bb5c166711 100644 --- a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte +++ b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte @@ -18,7 +18,7 @@ - +