diff --git a/.changeset/create-element-fast-path.md b/.changeset/create-element-fast-path.md deleted file mode 100644 index 8b88a14163..0000000000 --- a/.changeset/create-element-fast-path.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: use `createElement` instead of `createElementNS` for HTML elements diff --git a/.changeset/current-sources-set.md b/.changeset/current-sources-set.md deleted file mode 100644 index e1a4dc43b2..0000000000 --- a/.changeset/current-sources-set.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: store `current_sources` as a `Set` for O(1) membership checks diff --git a/.changeset/declaration-tag-multiple-declarators.md b/.changeset/declaration-tag-multiple-declarators.md new file mode 100644 index 0000000000..3f17162b54 --- /dev/null +++ b/.changeset/declaration-tag-multiple-declarators.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`) diff --git a/.changeset/declaration-tag-state-referenced-locally.md b/.changeset/declaration-tag-state-referenced-locally.md new file mode 100644 index 0000000000..00803c3e1c --- /dev/null +++ b/.changeset/declaration-tag-state-referenced-locally.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags diff --git a/.changeset/declaration-tag-whitespace.md b/.changeset/declaration-tag-whitespace.md new file mode 100644 index 0000000000..b64d1dd5a1 --- /dev/null +++ b/.changeset/declaration-tag-whitespace.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: tolerate whitespace before `let`/`const` in declaration tags diff --git a/.changeset/fix-find-matching-bracket-infinite-loop.md b/.changeset/fix-find-matching-bracket-infinite-loop.md new file mode 100644 index 0000000000..c0656ea794 --- /dev/null +++ b/.changeset/fix-find-matching-bracket-infinite-loop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite loop when a tag's expression ends with a trailing `/` at the end of the input diff --git a/.changeset/fix-type-identifier-in-tag.md b/.changeset/fix-type-identifier-in-tag.md new file mode 100644 index 0000000000..90e03da80f --- /dev/null +++ b/.changeset/fix-type-identifier-in-tag.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: more robust parsing of declaration tags with regards to `type` diff --git a/.changeset/input-type-before-value.md b/.changeset/input-type-before-value.md new file mode 100644 index 0000000000..68d02f920f --- /dev/null +++ b/.changeset/input-type-before-value.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve newlines in spread input values when the `type` attribute is applied after `value` diff --git a/.changeset/search-params-duplicate-set.md b/.changeset/search-params-duplicate-set.md new file mode 100644 index 0000000000..9fcaa8b289 --- /dev/null +++ b/.changeset/search-params-duplicate-set.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update `SvelteURLSearchParams` when setting duplicate keys to the same joined value diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f72b6c23e..7eff1b9fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: Lint: permissions: {} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 @@ -99,7 +99,7 @@ jobs: if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } - name: check browser-support docs page is up to date - run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/05-browser-support.md`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/05-browser-support.md; exit 1); }' + run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/.generated/`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/.generated/; exit 1); }' Benchmarks: permissions: {} runs-on: ubuntu-latest diff --git a/.prettierignore b/.prettierignore index 92d9bc797b..28f447f359 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ packages/**/config/*.js # packages/svelte packages/svelte/messages/**/*.md packages/svelte/scripts/_bundle.js +packages/svelte/scripts/_baseline/*.ts packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/warnings.js packages/svelte/src/internal/client/errors.js diff --git a/AGENTS.md b/AGENTS.md index c6cd3ea310..7f143248aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ This guide is for AI coding agents working in the Svelte monorepo. **Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here. +When submitting a PR, you **MUST** read [`PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and fill it out correctly. **DO NOT** submit a PR without running the full test suite. + ## Quick Reference If asked to do a performance investigation, use the `performance-investigation` skill. diff --git a/documentation/docs/03-template-syntax/10-@const.md b/documentation/docs/03-template-syntax/10-@const.md index 2a587b7a3d..6f2edc1a37 100644 --- a/documentation/docs/03-template-syntax/10-@const.md +++ b/documentation/docs/03-template-syntax/10-@const.md @@ -2,6 +2,8 @@ title: {@const ...} --- +> [!NOTE] `{@const x = y}` is legacy syntax — use [`{const x = $derived(y)}`](declaration-tags) instead + The `{@const ...}` tag defines a local constant. ```svelte diff --git a/documentation/docs/03-template-syntax/11-declaration-tags.md b/documentation/docs/03-template-syntax/11-declaration-tags.md new file mode 100644 index 0000000000..e0edaf6a38 --- /dev/null +++ b/documentation/docs/03-template-syntax/11-declaration-tags.md @@ -0,0 +1,72 @@ +--- +title: {let/const ...} +--- + +Declaration tags define local variables inside markup with `const` or `let`: + + +```svelte + + + +{#each boxes as box} + {const area = box.width * box.height} + {const label = `${box.width} ⨉ ${box.height} = ${area}`} + +

{label}

+{/each} +``` + + +> [!NOTE] Declaration tags are available since Svelte 5.56. + +> [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead. + +When values should be reactive, you can use `$state` and `$derived`: + + +```svelte + + + +

Hello {user.name}

+ + +{#if editing} + {let name = $state(user.name)} + {const greeting = $derived(`Hello ${name}`)} + +
+ +

{greeting}

+ + +{/if} +``` + + +Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the ``; } if (tag === 'input') { - // `bind:checked` requires type="checkbox"|"radio"; `bind:group` too. + // `bind:checked` and `bind:group` require type="checkbox" | "radio" const type = name === 'checked' || name === 'indeterminate' ? ' type="checkbox"' @@ -567,7 +563,6 @@ function binding_fixture(name: string, props: BindingProperty): string { * Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then * bundle the result through the shared `bundle` helper. Fixtures are tiny * so circular-dep warnings from the Svelte runtime are silenced. - * */ async function bundle_fixture(feature: Feature): Promise { const entry_code = @@ -612,6 +607,8 @@ async function find_all_conditional_features( const features = enumerate_features(subpackage_exports); const rows: ConditionalRow[] = []; + const missing_doc_links: string[] = []; + for (let i = 0; i < features.length; i++) { const feature = features[i]; process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80)); @@ -645,25 +642,35 @@ async function find_all_conditional_features( } } + let doc_link = doc_links[feature.name]; + if (doc_link === undefined) { + doc_link = null; + missing_doc_links.push(feature.name); + } + rows.push({ name: feature.name, + doc_link, versions, baseline_year: final_year }); } process.stdout.write('\n'); + if (missing_doc_links.length) { + throw new Error(`Missing documentation url for some features. +Add them to the \`doc_links\` map in \`scripts/generate-browser-support.ts\`, or add an explicit \`null\` if they don't have a documentation url. +${missing_doc_links.map((name) => ` - "${name}"`).join('\n')}`); + } + return rows; } -/** - * Render the per-feature browser-requirements table from the auto-detected - * rows. Sorted by Safari floor descending, then alphabetically. - */ -function render_conditional_table(rows: ConditionalRow[], runtime_floor: RuntimeFloor): string { - if (rows.length === 0) { +function render_conditional_table(features: ConditionalRow[], runtime_floor: RuntimeFloor): string { + if (features.length === 0) { return '_No features currently require browser versions newer than the runtime floor._'; } + features.sort((a, b) => a.name.localeCompare(b.name)); const browsers = [ ['chrome', 'Chrome/Edge'], @@ -671,32 +678,25 @@ function render_conditional_table(rows: ConditionalRow[], runtime_floor: Runtime ['safari', 'Safari'] ] as const; - const runtime_versions = browser_versions_for(runtime_floor); - - const sorted = [...rows].sort((a, b) => { - const sa = Number(a.versions.safari ?? '0'); - const sb = Number(b.versions.safari ?? '0'); - if (sb !== sa) return sb - sa; - return a.name.localeCompare(b.name); - }); - - const header = '| Feature | ' + browsers.map(([, label]) => `${label}`).join(' | ') + ' |'; - const sep = '| --- |' + browsers.map(() => ' ---: |').join(''); + const floor_versions = browser_versions_for(runtime_floor); - const body = sorted.map((entry) => { - const cells = browsers.map(([key]) => { - const v = entry.versions[key]; + const rows: string[][] = []; + for (const row of features) { + const name_cell = row.doc_link ? `[${row.name}](${row.doc_link})` : row.name; + const versions = browsers.map(([key]) => { + const v = row.versions[key]; if (v === null) return 'not supported'; if (v === undefined) return ''; - const floor_v = runtime_versions[key]; - return floor_v && Number(v) <= Number(floor_v) - ? '' - : v; + const floor_v = floor_versions[key]; + if (floor_v && Number(v) <= Number(floor_v)) + return ''; + + return v; }); - return `| ${entry.name} | ${cells.join(' | ')} |`; - }); + rows.push([name_cell, ...versions]); + } - return [header, sep, ...body].join('\n'); + return render_markdown_table(['Feature', ...browsers.map(([, label]) => `${label}`)], rows); } function browser_versions_for(target: RuntimeFloor): Record { @@ -750,53 +750,59 @@ function browser_versions_for(target: RuntimeFloor): Record { return lookup; } -function render_table(versions: Record, target: RuntimeFloor): string { - // Chrome and Edge ship from the same engine and historically resolve to the - // same Baseline version. Collapse them into one row when they match, but - // fall back to listing them separately if they ever drift. - const chrome_edge: [string, string] | null = - versions.chrome && versions.chrome === versions.edge ? ['Chrome/Edge', versions.chrome] : null; - - const base_rows: Array<[string, string]> = chrome_edge - ? [chrome_edge, ['Chrome (Android)', versions.chrome_android]] - : [ - ['Chrome', versions.chrome], - ['Chrome (Android)', versions.chrome_android], - ['Edge', versions.edge] - ]; - - const rows = [ - ...base_rows, - ['Firefox', versions.firefox], - ['Firefox (Android)', versions.firefox_android], - ['Safari', versions.safari], - ['Safari (iOS)', versions.safari_ios], - ['Opera', versions.opera], - ['Opera (Android)', versions.opera_android], - ['Samsung Internet', versions.samsunginternet_android], - ['Android WebView', versions.webview_android] - ].filter(([label, version]) => version !== undefined) as Array<[string, string]>; - - const headings = ['Browser', 'Minimum version']; - - const widths = headings.map((heading, i) => - Math.max(heading.length, ...rows.map((r) => String(r[i]).length)) - ); +const BROWSER = { + chrome: 'Chrome', + edge: 'Edge', + firefox: 'Firefox', + safari: 'Safari', + opera: 'Opera', + samsung_internet: 'Samsung Internet', + webview_android: 'Android WebView', + internet_explorer: 'Internet Explorer' +}; - const pad = (s: string, n: number) => s + ' '.repeat(Math.max(0, n - s.length)); +function render_browser_table(versions: Record, target: RuntimeFloor): string { + const rows: Array<[string, string]> = [ + [BROWSER.chrome, versions.chrome], + [`${BROWSER.chrome} (Android)`, versions.chrome_android] + ]; - const header = `| ${headings.map((heading, i) => pad(heading, widths[i])).join(' | ')} |`; - const sep = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`; - const body = rows - .map(([a, b]) => `| ${pad(a, widths[0])} | ${pad(String(b), widths[1])} |`) - .join('\n'); + if (versions.chrome === versions.edge) { + rows[0][0] += `/${BROWSER.edge}`; + } else { + rows.push([BROWSER.edge, versions.edge]); + } + + rows.push( + [BROWSER.firefox, versions.firefox], + [`${BROWSER.firefox} (Android)`, versions.firefox_android], + [BROWSER.safari, versions.safari], + [`${BROWSER.safari} (iOS)`, versions.safari_ios], + [BROWSER.opera, versions.opera], + [`${BROWSER.opera} (Android)`, versions.opera_android], + [BROWSER.samsung_internet, versions.samsunginternet_android], + [BROWSER.webview_android, versions.webview_android], + [BROWSER.internet_explorer, 'not supported'] + ); const target_label = target === 'newly' ? '"newly available"' : target; - return `${header}\n${sep}\n${body}\n\n> [!NOTE] This equates to a Baseline target of ${target_label}.`; + return ( + render_markdown_table( + ['Browser', 'Minimum version'], + rows.filter(([, version]) => version !== undefined) + ) + + `\n\n> [!NOTE] This equates to a Baseline target of ${target_label}.` + ); +} + +function render_markdown_table(headers: string[], rows: string[][]): string { + return `| ${headers.join(' | ')} | +| ${headers.map(() => '-').join(' | ')} | +${rows.map((row) => `| ${row.join(' | ')} |`).join('\n')} +`; } -/* eslint-disable no-console */ async function main() { console.log('Preparing scratch directory…'); // Wipe and recreate so stale bundles can't leak into the next scan. @@ -844,14 +850,11 @@ async function main() { const versions = browser_versions_for(target); console.log('Rewriting docs page…'); - generate('browser-support.md', render_table(versions, target)); + generate('browser-support.md', render_browser_table(versions, target)); generate('browser-support-features.md', render_conditional_table(conditional_rows, target)); console.log('Done.'); } finally { - // Ensure cleanup happens even on failure — otherwise leftover bundle - // files in `scripts/_baseline/` get picked up by `pnpm lint` on the - // next CI step and produce spurious naming/no-console errors. fs.rmSync(tmp_dir, { recursive: true, force: true }); } } diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index bafb450ab4..3b28d5e2a1 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1004,6 +1004,24 @@ export function debug_tag_invalid_arguments(node) { e(node, 'debug_tag_invalid_arguments', `{@debug ...} arguments must be identifiers, not arbitrary expressions\nhttps://svelte.dev/e/debug_tag_invalid_arguments`); } +/** + * Declaration tags must be `let` or `const` declarations + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function declaration_tag_invalid_type(node) { + e(node, 'declaration_tag_invalid_type', `Declaration tags must be \`let\` or \`const\` declarations\nhttps://svelte.dev/e/declaration_tag_invalid_type`); +} + +/** + * Declaration tags cannot be used in legacy mode + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function declaration_tag_no_legacy_mode(node) { + e(node, 'declaration_tag_no_legacy_mode', `Declaration tags cannot be used in legacy mode\nhttps://svelte.dev/e/declaration_tag_no_legacy_mode`); +} + /** * Directive value must be a JavaScript expression enclosed in curly braces * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 459858fe13..c51cd8434b 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -262,6 +262,10 @@ export function convert(source, ast) { }; }, // @ts-ignore + DeclarationTag(node) { + return node; + }, + // @ts-ignore KeyBlock(node, { visit }) { remove_surrounding_whitespace_nodes(node.fragment.nodes); return { diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 45a7c2a58c..d338f9ffb8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -1,10 +1,11 @@ -/** @import { Comment, Program } from 'estree' */ +/** @import { Comment, Program, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Parser } from './index.js' */ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as e from '../../errors.js'; +import { find_matching_bracket } from './utils/bracket.js'; const JSParser = acorn.Parser; const TSParser = JSParser.extend(tsPlugin()); @@ -98,6 +99,48 @@ export function parse_expression_at(parser, source, index) { } } +/** + * @param {Parser} parser + * @param {string} source + * @param {number} index + * @returns {Statement} + */ +export function parse_statement_at(parser, source, index) { + const acorn = parser.ts ? TSParser : JSParser; + let end = find_matching_bracket(source, index, '{'); + if (end === undefined) e.unexpected_eof(source.length); + + while (source[end - 1] === ';') { + end -= 1; + } + + const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`; + const { onComment, add_comments } = get_comment_handlers( + padded_source, + parser.root.comments, + index + ); + + try { + const ast = acorn.parse(padded_source, { + onComment, + sourceType: 'module', + ecmaVersion: 16, + locations: true + }); + + add_comments(ast); + + const statement = /** @type {Statement} */ ( + /** @type {unknown} */ (/** @type {Program} */ (ast).body[0]) + ); + statement.end = Math.min(/** @type {number} */ (statement.end), end); + return statement; + } catch (e) { + handle_parse_error(e); + } +} + const regex_position_indicator = / \(\d+:\d+\)$/; /** diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 5242cba31f..02d1629c99 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -302,7 +302,10 @@ export class Parser { } pop() { - this.fragments.pop(); + const fragment = this.fragments.pop(); + if (fragment?.metadata.transparent && fragment.nodes.some((n) => n.type === 'DeclarationTag')) { + fragment.metadata.transparent = false; + } return this.stack.pop(); } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index ff153128a5..15c79e0353 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -1,16 +1,20 @@ -/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */ +/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; import { ExpressionMetadata } from '../../nodes.js'; -import { parse_expression_at } from '../acorn.js'; +import { parse_expression_at, parse_statement_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; import { create_fragment } from '../utils/create.js'; -import { match_bracket } from '../utils/bracket.js'; +import { find_matching_bracket, match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /\s*}/y; +const regex_supported_declaration = /(?:let|const)\b/y; +const regex_unsupported_declaration = /(?:var|interface|enum)\b/y; +// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing. +const regex_maybe_type_declaration = /type\b/y; const pointy_bois = { '<': '>' }; @@ -31,6 +35,20 @@ export default function tag(parser) { } } + const declaration = read_declaration(parser); + if (declaration) { + parser.append({ + type: 'DeclarationTag', + start, + end: parser.index, + declaration: /** @type {VariableDeclaration} */ (declaration), + metadata: { + expression: new ExpressionMetadata() + } + }); + return; + } + const expression = read_expression(parser); parser.allow_whitespace(); @@ -47,6 +65,89 @@ export default function tag(parser) { }); } +/** + * @param {Parser} parser + * @returns {null | import('estree').VariableDeclaration} + */ +function read_declaration(parser) { + const start = parser.index; + + const unsupported = parser.match_regex(regex_unsupported_declaration); + if (unsupported) { + e.declaration_tag_invalid_type({ start, end: start + unsupported.length }); + } + + if ( + !parser.match_regex(regex_supported_declaration) && + // `type` is special, since it is not a reserved keyword and can be used + // as part of a valid expression. We gotta parse first and then see what it is. + !parser.match_regex(regex_maybe_type_declaration) + ) { + return null; + } + + const initial_comment_count = parser.root.comments.length; + + /** @type {import('estree').Statement | import('estree').VariableDeclaration} */ + let declaration; + try { + declaration = parse_statement_at(parser, parser.template, start); + } catch (error) { + if (!parser.loose) throw error; + + const end = find_matching_bracket(parser.template, start, '{'); + if (end === undefined) throw error; + + parser.index = end; + const kind = parser.template.startsWith('const', start) ? 'const' : 'let'; + + declaration = { + type: 'VariableDeclaration', + kind, + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: '', + start: parser.index, + end: parser.index + }, + init: null, + start: parser.index, + end: parser.index + } + ], + start, + end + }; + } + + if (declaration.type !== 'VariableDeclaration') { + if (declaration.type === 'ExpressionStatement') { + parser.root.comments.length = initial_comment_count; // Else they show up duplicated + return null; + } else { + // This is a TSTypeAliasDeclaration + e.declaration_tag_invalid_type({ + start: declaration.start ?? start, + end: declaration.end ?? parser.index + }); + } + } + + // TODO support using + if (declaration.kind !== 'let' && declaration.kind !== 'const') { + e.declaration_tag_invalid_type(declaration); + } + + parser.index = /** @type {number} */ (declaration.end); + parser.allow_whitespace(); + parser.eat('}', true); + + return declaration; +} + /** @param {Parser} parser */ function open(parser) { let start = parser.index - 2; diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index 63fbc68fb3..47299c9de5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -39,7 +39,9 @@ function find_string_end(string, search_start_index, string_start_char) { * @returns {number} The index of the end of this regex expression, or `Infinity` if not found. */ function find_regex_end(string, search_start_index) { - return find_unescaped_char(string, search_start_index, '/'); + const slash = find_unescaped_char(string, search_start_index, '/'); + const eol = find_unescaped_char(string, search_start_index, '\n'); + return slash < eol ? slash : Infinity; } /** @@ -105,7 +107,11 @@ export function find_matching_bracket(template, index, open) { continue; case '/': { const next_char = template[i + 1]; - if (!next_char) continue; + if (!next_char) { + // `/` is the last character; advance past it so we don't loop forever + i++; + continue; + } if (next_char === '/') { i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length; continue; @@ -114,7 +120,12 @@ export function find_matching_bracket(template, index, open) { i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length; continue; } - i = find_regex_end(template, i + 1) + '/'.length; + const end = find_regex_end(template, i + 1) + '/'.length; + if (end === Infinity) { + i++; + } else { + i = end; + } continue; } default: { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f4719251bd..d6eef085d1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -36,6 +36,7 @@ import { ClassDeclaration } from './visitors/ClassDeclaration.js'; import { ClassDirective } from './visitors/ClassDirective.js'; import { Component } from './visitors/Component.js'; import { ConstTag } from './visitors/ConstTag.js'; +import { DeclarationTag } from './visitors/DeclarationTag.js'; import { DebugTag } from './visitors/DebugTag.js'; import { EachBlock } from './visitors/EachBlock.js'; import { ExportDefaultDeclaration } from './visitors/ExportDefaultDeclaration.js'; @@ -157,6 +158,7 @@ const visitors = { ClassDirective, Component, ConstTag, + DeclarationTag, DebugTag, EachBlock, ExportDefaultDeclaration, @@ -312,6 +314,7 @@ export function analyze_module(source, options) { options: /** @type {ValidatedCompileOptions} */ (options), fragment: null, parent_element: null, + in_declaration_tag: false, reactive_statement: null, derived_function_depth: -1 }, @@ -725,6 +728,7 @@ export function analyze_component(root, source, options) { ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', fragment: ast === template.ast ? ast : null, parent_element: null, + in_declaration_tag: false, has_props_rune: false, component_slots: new Set(), expression: null, @@ -792,6 +796,7 @@ export function analyze_component(root, source, options) { options, fragment: ast === template.ast ? ast : null, parent_element: null, + in_declaration_tag: false, has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', reactive_statement: null, diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 354f1c0856..bd486a12ca 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -16,6 +16,8 @@ export interface AnalysisState { * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between. */ parent_element: string | null; + /** True if inside DeclarationTag */ + in_declaration_tag: boolean; has_props_rune: boolean; /** Which slots the current parent component has */ component_slots: Set; @@ -35,7 +37,7 @@ export interface AnalysisState { */ derived_function_depth: number; - /** Collected info about async `{@const }` declarations */ + /** Collected info about async `{@const }`/`{let/const ...}` declarations */ async_consts?: { id: Identifier; /** How many `$.run(...)` entries are already allocated in this scope */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 52eba8c735..d3524af4db 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -255,6 +255,9 @@ export function CallExpression(node, context) { if (expression.has_await) { context.state.analysis.async_deriveds.add(node); } + + // Tell surrounding declaration tag about metadata for correct calculation of blockers etc + if (context.state.in_declaration_tag) context.state.expression?.merge(expression); } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); } else { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index 4f07249a39..64e93d3efe 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -1,8 +1,8 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -import * as b from '#compiler/builders'; import { validate_opening_tag } from './shared/utils.js'; +import { mark_async_declaration } from './DeclarationTag.js'; /** * @param {AST.ConstTag} node @@ -44,28 +44,5 @@ export function ConstTag(node, context) { derived_function_depth: context.state.function_depth + 1 }); - const has_await = node.metadata.expression.has_await; - const blockers = [...node.metadata.expression.dependencies] - .map((dep) => dep.blocker) - .filter((b) => b !== null && b.object !== context.state.async_consts?.id); - - if (has_await || context.state.async_consts || blockers.length > 0) { - const run = (context.state.async_consts ??= { - id: context.state.analysis.root.unique('promises'), - declaration_count: 0 - }); - node.metadata.promises_id = run.id; - - const bindings = context.state.scope.get_bindings(declaration); - - // keep the counter in sync with the number of thunks pushed in ConstTag in transform - // TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust - // via something like the approach in https://github.com/sveltejs/svelte/pull/18032 - const length = run.declaration_count + (blockers.length > 0 ? 1 : 0); - run.declaration_count += blockers.length > 0 ? 2 : 1; - const blocker = b.member(run.id, b.literal(length), true); - for (const binding of bindings) { - binding.blocker = blocker; - } - } + mark_async_declaration(context, node.metadata, [declaration]); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js new file mode 100644 index 0000000000..b5e8c931f1 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js @@ -0,0 +1,60 @@ +/** @import { AST } from '#compiler' */ +/** @import { Context } from '../types' */ +import * as b from '#compiler/builders'; +import * as e from '../../../errors.js'; + +/** + * @param {AST.DeclarationTag} node + * @param {Context} context + */ +export function DeclarationTag(node, context) { + if (!context.state.analysis.runes && !context.state.analysis.maybe_runes) { + e.declaration_tag_no_legacy_mode(node); + } + + context.visit(node.declaration, { + ...context.state, + in_declaration_tag: true, + // the declaration lives in the fragment scope, which is one level deeper than the + // `function_depth` we're tracking here (`set_scope` doesn't update `function_depth`). + // align them so that `state_referenced_locally` warnings are calculated correctly + function_depth: context.state.scope.function_depth, + expression: node.metadata.expression + }); + + mark_async_declaration(context, node.metadata, node.declaration.declarations); +} + +/** + * @param {Context} context + * @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata + * @param {import('estree').VariableDeclarator[]} declarations + */ +export function mark_async_declaration(context, metadata, declarations) { + const has_await = metadata.expression.has_await; + const blockers = [...metadata.expression.dependencies] + .map((dep) => dep.blocker) + .filter((b) => b !== null && b.object !== context.state.async_consts?.id); + + if (has_await || context.state.async_consts || blockers.length > 0) { + const run = (context.state.async_consts ??= { + id: context.state.analysis.root.unique('promises'), + declaration_count: 0 + }); + metadata.promises_id = run.id; + + const bindings = declarations.flatMap((declaration) => + context.state.scope.get_bindings(declaration) + ); + + // keep the counter in sync with the number of thunks pushed in transform + // TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust + // via something like the approach in https://github.com/sveltejs/svelte/pull/18032 + const length = run.declaration_count + (blockers.length > 0 ? 1 : 0); + run.declaration_count += blockers.length > 0 ? 2 : 1; + const blocker = b.member(run.id, b.literal(length), true); + for (const binding of bindings) { + binding.blocker = blocker; + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 5c1e8031b8..ebb2fc2b67 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -162,7 +162,7 @@ export function Identifier(node, context) { if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) { let snippet_name; - // Find out if this references a {@const ...} declaration of an implicit children snippet + // Find out if this references a {@const ...}/{let/const ...} declaration of an implicit children snippet // when it is itself inside a snippet block at the same level. If so, error. for (let i = context.path.length - 1; i >= 0; i--) { const parent = context.path[i]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index d92d07b038..c749a4e38f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -4,7 +4,7 @@ /** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */ import { walk } from 'zimmerframe'; import * as b from '#compiler/builders'; -import { build_getter, is_state_source } from './utils.js'; +import { build_getter, get_transform } from './utils.js'; import { render_stylesheet } from '../css/index.js'; import { dev, filename, custom_renderer } from '../../../state.js'; import { AnimateDirective } from './visitors/AnimateDirective.js'; @@ -22,6 +22,7 @@ import { ClassBody } from './visitors/ClassBody.js'; import { Comment } from './visitors/Comment.js'; import { Component } from './visitors/Component.js'; import { ConstTag } from './visitors/ConstTag.js'; +import { DeclarationTag } from './visitors/DeclarationTag.js'; import { DebugTag } from './visitors/DebugTag.js'; import { EachBlock } from './visitors/EachBlock.js'; import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; @@ -66,20 +67,7 @@ const visitors = { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { - const transform = { ...state.transform }; - - for (const [name, binding] of scope.declarations) { - if ( - binding.kind === 'normal' || - // Reads of `$state(...)` declarations are not - // transformed if they are never reassigned - (binding.kind === 'state' && !is_state_source(binding, state.analysis)) - ) { - delete transform[name]; - } - } - - next({ ...state, transform, scope }); + next({ ...state, transform: get_transform(scope, state), scope }); } else { next(); } @@ -99,6 +87,7 @@ const visitors = { Comment, Component, ConstTag, + DeclarationTag, DebugTag, EachBlock, ExportNamedDeclaration, @@ -152,6 +141,7 @@ export function client_component(analysis, options) { scopes: analysis.module.scopes, is_instance: false, hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted], + templates: new Map(), node: /** @type {any} */ (null), // populated by the root node legacy_reactive_imports: [], legacy_reactive_statements: new Map(), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js index 246824a447..ba20f31912 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -1,3 +1,4 @@ +/** @import { TemplateLiteral } from 'estree' */ /** @import { Namespace } from '#compiler' */ /** @import { ComponentClientTransformState } from '../types.js' */ /** @import { Node } from './types.js' */ @@ -31,15 +32,30 @@ function build_locations(nodes) { /** * @param {ComponentClientTransformState} state - * @param {Namespace} namespace + * @param {string} name * @param {number} [flags] */ -export function transform_template(state, namespace, flags = 0) { +export function transform_template(state, name, flags = 0) { + const namespace = state.metadata.namespace; // custom renderers needs a tree to work because there's no template element we can use const tree = state.options.fragments === 'tree' || custom_renderer; const expression = tree ? state.template.as_tree() : state.template.as_html(); + const key = + tree || dev + ? null + : get_template_key( + /** @type {TemplateLiteral} */ (expression), + state.metadata.namespace, + flags + ); + + if (key !== null) { + const existing = state.templates.get(key); + if (existing !== undefined) return existing; + } + if (tree) { if (namespace === 'svg') flags |= TEMPLATE_USE_SVG; if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML; @@ -64,5 +80,26 @@ export function transform_template(state, namespace, flags = 0) { ); } - return call; + const id = state.scope.root.unique(name); + state.hoisted.push(b.var(id, call)); + + if (key !== null) { + state.templates.set(key, id); + } + + return id; +} + +/** + * Returns a stable key for templates that are safe to deduplicate - plain + * `$.from_html`/`from_svg`/`from_mathml` factories with literal arguments - or `null` + * for anything else. Dev-mode templates are wrapped in `$.add_locations(...)`, which + * embeds per-call-site locations, so they never produce a key and are never shared. + * @param {TemplateLiteral} template + * @param {Namespace} namespace + * @param {number} flags + * @returns {string | null} + */ +function get_template_key(template, namespace, flags) { + return `${namespace} ${flags} ${template.quasis[0].value.raw}`; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 287bf24ac6..7a95a2d43c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -40,6 +40,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly analysis: ComponentAnalysis; readonly options: ValidatedCompileOptions; readonly hoisted: Array; + /** Deduplicates hoisted templates by content, mapping a template key to its hoisted identifier */ + readonly templates: Map; readonly events: Set; readonly store_to_invalidate?: string; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index f21fb43fc1..d52b438d92 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -179,3 +179,24 @@ export function create_derived(state, expression, async = false) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); } } + +/** + * @param {Scope} scope + * @param {ClientTransformState} state + */ +export function get_transform(scope, state) { + const transform = { ...state.transform }; + + for (const [name, binding] of scope.declarations) { + if ( + binding.kind === 'normal' || + // Reads of `$state(...)` declarations are not + // transformed if they are never reassigned + (binding.kind === 'state' && !is_state_source(binding, state.analysis)) + ) { + delete transform[name]; + } + } + + return transform; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index bf559cd24d..d05d7a8ed9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -7,6 +7,7 @@ import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; import { build_expression } from './shared/utils.js'; +import { add_async_declaration } from './DeclarationTag.js'; /** * @param {AST.ConstTag} node @@ -26,7 +27,7 @@ export function ConstTag(node, context) { context.state.transform[declaration.id.name] = { read: get_value }; - add_const_declaration(context.state, declaration.id, expression, node.metadata); + add_const_declaration(context, declaration.id, expression, node.metadata); } else { const identifiers = extract_identifiers(declaration.id); const tmp = b.id(context.state.scope.generate('computed_const')); @@ -63,7 +64,7 @@ export function ConstTag(node, context) { expression = b.call('$.tag', expression, b.literal('[@const]')); } - add_const_declaration(context.state, tmp, expression, node.metadata); + add_const_declaration(context, tmp, expression, node.metadata); for (const node of identifiers) { context.state.transform[node.name] = { @@ -74,38 +75,26 @@ export function ConstTag(node, context) { } /** - * @param {ComponentContext['state']} state + * @param {ComponentContext} context * @param {Identifier} id * @param {Expression} expression * @param {AST.ConstTag['metadata']} metadata */ -function add_const_declaration(state, id, expression, metadata) { +function add_const_declaration(context, id, expression, metadata) { // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors const after = dev ? [b.stmt(b.call('$.get', id))] : []; - const blockers = [...metadata.expression.dependencies] - .map((dep) => dep.blocker) - .filter((b) => b !== null && b.object !== state.async_consts?.id); - if (metadata.promises_id) { - const run = (state.async_consts ??= { - id: metadata.promises_id, - thunks: [] - }); - - state.consts.push(b.let(id)); - - if (blockers.length === 1) { - run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise'))); - } else if (blockers.length > 0) { - run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers)))); - } - - // keep the number of thunks pushed in sync with ConstTag in analysis phase - const assignment = b.assignment('=', id, expression); - run.thunks.push(b.thunk(assignment, metadata.expression.has_await)); + add_async_declaration( + context, + metadata, + [id], + [b.stmt(b.assignment('=', id, expression))], + 'let' + ); } else { + const { state } = context; state.consts.push(b.const(id, expression)); state.consts.push(...after); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js new file mode 100644 index 0000000000..abec5e828f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js @@ -0,0 +1,89 @@ +/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js'; +import * as b from '#compiler/builders'; +import { add_state_transformers } from './shared/declarations.js'; + +/** + * @param {AST.DeclarationTag} node + * @param {ComponentContext} context + */ +export function DeclarationTag(node, context) { + // register the transformers _before_ visiting the declaration, so that + // later declarators can reference earlier ones (e.g. `{let a = $state(0), b = $derived(a * 2)}`) + add_state_transformers(context); + const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration)); + + if ( + node.metadata.promises_id && + node.declaration.type === 'VariableDeclaration' && + declaration?.type === 'VariableDeclaration' + ) { + const { ids, assignments } = build_async_declaration_parts(declaration); + add_async_declaration(context, node.metadata, ids, assignments, declaration.kind); + } else { + context.state.consts.push(declaration ?? node.declaration); + } +} + +/** + * @param {VariableDeclaration} declaration + */ +export function build_async_declaration_parts(declaration) { + const ids = new Map(); + for (const declarator of declaration.declarations) { + for (const id of extract_identifiers(declarator.id)) { + ids.set(id.name, id); + } + } + + const assignments = declaration.declarations + .filter((declarator) => declarator.init !== null) + .map((declarator) => + b.stmt( + b.assignment( + '=', + /** @type {Pattern} */ (declarator.id), + /** @type {Expression} */ (declarator.init) + ) + ) + ); + + return { ids: [...ids.values()], assignments }; +} + +/** + * @param {ComponentContext} context + * @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata + * @param {Identifier[]} ids + * @param {ExpressionStatement[]} assignments + * @param {VariableDeclaration['kind']} [kind] + */ +export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') { + const run = (context.state.async_consts ??= { + id: /** @type {Identifier} */ (metadata.promises_id), + thunks: [] + }); + + for (const id of ids) { + context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); + } + + const blockers = [...metadata.expression.dependencies] + .map((dep) => dep.blocker) + .filter((b) => b !== null && b.object !== context.state.async_consts?.id); + + if (blockers.length === 1) { + run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise'))); + } else if (blockers.length > 0) { + run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers)))); + } + + // keep the number of thunks pushed in sync with analysis phase + const has_await = + metadata.expression.has_await || + assignments.some((assignment) => has_await_expression(assignment)); + const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments); + run.thunks.push(b.thunk(body, has_await)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index ad0c487fb4..893b1db568 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -52,7 +52,6 @@ export function Fragment(node, context) { (trimmed[0].type === 'IfBlock' && trimmed[0].elseif && /** @type {AST.IfBlock} */ (parent).metadata.flattened?.includes(trimmed[0]))); - const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent /** @type {Statement[]} */ const body = []; @@ -96,8 +95,7 @@ export function Fragment(node, context) { let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined; - const template = transform_template(state, namespace, flags); - state.hoisted.push(b.var(template_name, template)); + const template_name = transform_template(state, 'root', flags); state.init.unshift(b.var(id, b.call(template_name))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); @@ -147,8 +145,7 @@ export function Fragment(node, context) { // special case — we can use `$.comment` instead of creating a unique template state.init.unshift(b.var(id, b.call('$.comment'))); } else { - const template = transform_template(state, namespace, flags); - state.hoisted.push(b.var(template_name, template)); + const template_name = transform_template(state, 'root', flags); state.init.unshift(b.var(id, b.call(template_name))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 040bf4385d..abff9b2394 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -18,7 +18,7 @@ import { is_customizable_select_element } from '../../../nodes.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; -import { build_getter } from '../utils.js'; +import { build_getter, get_transform } from '../utils.js'; import { get_attribute_name, build_attribute_value, @@ -303,11 +303,14 @@ export function RegularElement(node, context) { } } + const scope = /** @type {Scope} */ (context.state.scopes.get(node.fragment)); + /** @type {ComponentClientTransformState} */ const state = { ...context.state, metadata, - scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)), + scope, + transform: get_transform(scope, context.state), preserve_whitespace: context.state.preserve_whitespace || name === 'pre' || name === 'textarea' }; @@ -321,8 +324,19 @@ export function RegularElement(node, context) { state.options.preserveComments ); + const has_declarations = !node.fragment.metadata.transparent; + /** @type {typeof state} */ - const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] }; + const child_state = { + ...state, + init: [], + update: [], + after_update: [], + snippets: [], + consts: has_declarations ? [] : state.consts, + async_consts: has_declarations ? undefined : state.async_consts, + memoizer: has_declarations ? new Memoizer() : state.memoizer + }; for (const node of hoisted) { context.visit(node, child_state); @@ -379,7 +393,6 @@ export function RegularElement(node, context) { context.state.template.push_comment(); // Create a separate template for the rich content - const template_name = context.state.scope.root.unique(`${name}_content`); const fragment_id = b.id(context.state.scope.generate('fragment')); const anchor_id = b.id(context.state.scope.generate('anchor')); @@ -403,9 +416,8 @@ export function RegularElement(node, context) { } ); - // Transform the template to $.from_html(...) and hoist it - const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT); - context.state.hoisted.push(b.var(template_name, template)); + // Transform the template to $.from_html(...) and hoist it (deduplicating identical templates) + const template_name = transform_template(select_state, `${name}_content`, TEMPLATE_FRAGMENT); // Build the rich content function body // The anchor is the child of the element (a hydration marker during hydration) @@ -446,11 +458,21 @@ export function RegularElement(node, context) { } } - if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock')) { + if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock') || has_declarations) { + if (child_state.async_consts && child_state.async_consts.thunks.length > 0) { + child_state.consts.push( + b.var( + child_state.async_consts.id, + b.call('$.run', b.array(child_state.async_consts.thunks)) + ) + ); + } + // Wrap children in `{...}` to avoid declaration conflicts context.state.init.push( b.block([ ...child_state.snippets, + ...child_state.consts, ...child_state.init, ...element_state.init, child_state.update.length > 0 ? build_render_statement(child_state) : b.empty, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index f929a3bc47..eaffd15084 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -40,6 +40,7 @@ export function SvelteBoundary(node, context) { const hoisted = []; let has_const = false; + let has_declaration = false; // const tags need to live inside the boundary, but might also be referenced in hoisted snippets. // to resolve this we cheat: we duplicate const tags inside snippets @@ -55,6 +56,10 @@ export function SvelteBoundary(node, context) { }); } } + + if (child.type === 'DeclarationTag') { + has_declaration = true; + } } for (const child of node.fragment.nodes) { @@ -68,10 +73,10 @@ export function SvelteBoundary(node, context) { if (child.type === 'SnippetBlock') { if ( context.state.options.experimental.async && - has_const && + (has_const || has_declaration) && !['failed', 'pending'].includes(child.expression.name) ) { - // we can't hoist snippets as they may reference const tags, so we just keep them in the fragment + // we can't hoist snippets as they may reference const/declaration tags, so we just keep them in the fragment nodes.push(child); } else { /** @type {Statement[]} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 72685a8e83..b9f4690179 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -49,8 +49,13 @@ export function VariableDeclaration(node, context) { } if (declarator.id.type === 'Identifier') { + const exclude_id = context.state.scope.root.unique('rest_excludes'); + context.state.hoisted.push( + b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name))))) + ); + /** @type {Expression[]} */ - const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + const args = [b.id('$$props'), exclude_id]; if (dev) { // include rest name, so we can provide informative error messages @@ -95,8 +100,13 @@ export function VariableDeclaration(node, context) { } } else { // RestElement + const exclude_id = context.state.scope.root.unique('rest_excludes'); + context.state.hoisted.push( + b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name))))) + ); + /** @type {Expression[]} */ - const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + const args = [b.id('$$props'), exclude_id]; if (dev) { // include rest name, so we can provide informative error messages diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 6d1de18708..2101ea04e8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -15,6 +15,7 @@ import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; import { Component } from './visitors/Component.js'; import { ConstTag } from './visitors/ConstTag.js'; +import { DeclarationTag } from './visitors/DeclarationTag.js'; import { DebugTag } from './visitors/DebugTag.js'; import { EachBlock } from './visitors/EachBlock.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js'; @@ -64,6 +65,7 @@ const template_visitors = { AwaitBlock, Component, ConstTag, + DeclarationTag, DebugTag, EachBlock, Fragment, diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index 4912728a1e..2863955786 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -28,7 +28,7 @@ export interface ComponentServerTransformState extends ServerTransformState { readonly preserve_whitespace: boolean; /** True if the current node is a) a component or render tag and b) the sole child of a block */ readonly is_standalone: boolean; - /** Transformed async `{@const }` declarations (if any) and those coming after them */ + /** Transformed async `{@const }`/`{let/const ...}` declarations (if any) and those coming after them */ async_consts?: { id: Identifier; thunks: Expression[]; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js index 9420bdd6d2..cf56ef6910 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js @@ -1,8 +1,9 @@ -/** @import { Expression, Pattern, Statement } from 'estree' */ +/** @import { Expression, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; import { extract_identifiers } from '../../../../utils/ast.js'; +import { add_async_declaration } from './DeclarationTag.js'; /** * @param {AST.ConstTag} node @@ -12,31 +13,15 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; const id = /** @type {Pattern} */ (context.visit(declaration.id)); const init = /** @type {Expression} */ (context.visit(declaration.init)); - const blockers = [...node.metadata.expression.dependencies] - .map((dep) => dep.blocker) - .filter((b) => b !== null && b.object !== context.state.async_consts?.id); if (node.metadata.promises_id) { - const run = (context.state.async_consts ??= { - id: node.metadata.promises_id, - thunks: [] - }); - - const identifiers = extract_identifiers(declaration.id); - - for (const identifier of identifiers) { - context.state.init.push(b.let(identifier.name)); - } - - if (blockers.length === 1) { - run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0]))); - } else if (blockers.length > 0) { - run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers)))); - } - - // keep the number of thunks pushed in sync with ConstTag in analysis phase - const assignment = b.assignment('=', id, init); - run.thunks.push(b.thunk(assignment, node.metadata.expression.has_await)); + add_async_declaration( + context, + node.metadata, + extract_identifiers(id), + [b.stmt(b.assignment('=', id, init))], + 'let' + ); } else { context.state.init.push(b.const(id, init)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js new file mode 100644 index 0000000000..8338700c0b --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js @@ -0,0 +1,85 @@ +/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types.js' */ +import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js'; +import * as b from '#compiler/builders'; + +/** + * @param {AST.DeclarationTag} node + * @param {ComponentContext} context + */ +export function DeclarationTag(node, context) { + const declaration = /** @type {Statement} */ (context.visit(node.declaration)); + + if ( + node.metadata.promises_id && + node.declaration.type === 'VariableDeclaration' && + declaration.type === 'VariableDeclaration' + ) { + const { ids, assignments } = build_async_declaration_parts(declaration); + add_async_declaration(context, node.metadata, ids, assignments, declaration.kind); + } else { + context.state.init.push(declaration); + } +} + +/** + * @param {VariableDeclaration} declaration + */ +export function build_async_declaration_parts(declaration) { + const ids = new Map(); + for (const declarator of declaration.declarations) { + for (const id of extract_identifiers(declarator.id)) { + ids.set(id.name, id); + } + } + + const assignments = declaration.declarations + .filter((declarator) => declarator.init !== null) + .map((declarator) => + b.stmt( + b.assignment( + '=', + /** @type {Pattern} */ (declarator.id), + /** @type {Expression} */ (declarator.init) + ) + ) + ); + + return { ids: [...ids.values()], assignments }; +} + +/** + * @param {ComponentContext} context + * @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata + * @param {Identifier[]} ids + * @param {ExpressionStatement[]} assignments + * @param {VariableDeclaration['kind']} [kind] + */ +export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') { + const run = (context.state.async_consts ??= { + id: /** @type {Identifier} */ (metadata.promises_id), + thunks: [] + }); + + for (const id of ids) { + context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); + } + + const blockers = [...metadata.expression.dependencies] + .map((dep) => dep.blocker) + .filter((b) => b !== null && b.object !== context.state.async_consts?.id); + + if (blockers.length === 1) { + run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0]))); + } else if (blockers.length > 0) { + run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers)))); + } + + // keep the number of thunks pushed in sync with analysis phase + const has_await = + metadata.expression.has_await || + assignments.some((assignment) => has_await_expression(assignment)); + const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments); + run.thunks.push(b.thunk(body, has_await)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 6b4db1e819..fdf0d22b1c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -17,17 +17,23 @@ import { is_customizable_select_element } from '../../../nodes.js'; export function RegularElement(node, context) { const name = context.state.namespace === 'html' ? node.name.toLowerCase() : node.name; const namespace = determine_namespace_for_children(node, context.state.namespace); + const has_child_declarations = !node.fragment.metadata.transparent; /** @type {ComponentServerTransformState} */ const state = { ...context.state, namespace, + scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)), preserve_whitespace: context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea', init: [], - template: [] + template: [], + async_consts: undefined }; + /** @type {ComponentServerTransformState} */ + const attribute_state = { ...state, scope: context.state.scope }; + const node_is_void = is_void(name); const optimiser = new PromiseOptimiser(); @@ -50,7 +56,11 @@ export function RegularElement(node, context) { if (!is_special) { // only open the tag in the non-special path state.template.push(b.literal(`<${name}`)); - body = build_element_attributes(node, { ...context, state }, optimiser.transform); + body = build_element_attributes( + node, + { ...context, state: attribute_state }, + optimiser.transform + ); state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance } @@ -72,10 +82,7 @@ export function RegularElement(node, context) { node.fragment.nodes, context.path, namespace, - { - ...state, - scope: /** @type {Scope} */ (state.scopes.get(node.fragment)) - }, + state, state.preserve_whitespace, state.options.preserveComments ); @@ -209,7 +216,17 @@ export function RegularElement(node, context) { state.template.push(b.stmt(b.call('$.pop_element'))); } - if (optimiser.is_async()) { + if (has_child_declarations && state.async_consts && state.async_consts.thunks.length > 0) { + state.init.push( + b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks))) + ); + } + + if (has_child_declarations) { + context.state.template.push( + ...optimiser.render([b.block([...state.init, ...build_template(state.template)])]) + ); + } else if (optimiser.is_async()) { context.state.template.push( ...optimiser.render([...state.init, ...build_template(state.template)]) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index f61b59f3bd..c7fce61ff9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -152,6 +152,7 @@ export function clean_nodes( if ( node.type === 'ConstTag' || + node.type === 'DeclarationTag' || node.type === 'DebugTag' || node.type === 'SvelteBody' || node.type === 'SvelteWindow' || diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 48ebacb50b..da7e73015b 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -217,6 +217,7 @@ function* find_descendants(fragment) { case 'SnippetBlock': case 'DebugTag': case 'ConstTag': + case 'DeclarationTag': case 'Comment': case 'ExpressionTag': break; diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index c511ee6da0..fbd7e86f12 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -603,6 +603,48 @@ const svelte_visitors = (comments) => ({ context.write('}'); }, + DeclarationTag(node, context) { + context.write('{'); + + // This is duplicated from esrap's handling of VariableDeclaration, + // which we need to do in order to omit the trailing semicolon that esrap would add. + const open = context.new(); + const join = context.new(); + const child_context = context.new(); + + context.append(child_context); + + child_context.write(`${node.declaration.kind} `); + child_context.append(open); + + const declarations = node.declaration.declarations; + let first = true; + + for (const d of declarations) { + if (!first) child_context.append(join); + first = false; + + child_context.visit(d); + } + + const length = child_context.measure() + 2 * (declarations.length - 1); + + const multiline = child_context.multiline || (declarations.length > 1 && length > 50); + + if (multiline) { + context.multiline = true; + + if (declarations.length > 1) open.indent(); + join.write(','); + join.newline(); + if (declarations.length > 1) context.dedent(); + } else { + join.write(', '); + } + + context.write('}'); + }, + DebugTag(node, context) { context.write('{@debug '); let started = false; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index c2c79f2b57..6d3ce17d03 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -161,6 +161,18 @@ export namespace AST { }; } + /** A `{let ...}` or `{const ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + /** If this declaration tag contains an await expression, or needs to wait on other async, this is set */ + promises_id?: Identifier; + }; + } + /** A `{@debug ...}` tag */ export interface DebugTag extends BaseNode { type: 'DebugTag'; @@ -623,6 +635,7 @@ export namespace AST { export type Tag = | AST.AttachTag | AST.ConstTag + | AST.DeclarationTag | AST.DebugTag | AST.ExpressionTag | AST.HtmlTag diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 6ac1356d8f..ad5a4ee418 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -686,7 +686,8 @@ export { if_builder as if, this_instance as this, null_instance as null, - debugger_builder as debugger + debugger_builder as debugger, + new_builder as new }; /** diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 737d859b93..6b5ec4f9c7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -417,7 +417,7 @@ export class Boundary { if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect); if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect); - current_batch.on_fork_commit(() => { + current_batch.oncommit(() => { this.#handle_error(error); }); } else { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 6a5ed9f01f..d47610fa9a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -242,7 +242,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var each_array = derived_safe_equal(() => { var collection = get_collection(); - return is_array(collection) ? collection : collection == null ? [] : array_from(collection); + return /** @type {V[]} */ ( + is_array(collection) ? collection : collection == null ? [] : array_from(collection) + ); }); if (DEV) { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 7e4e30d2c0..52ef9c95d6 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -343,6 +343,21 @@ function set_attributes( var setters = get_setters(element); + if ( + // the following login only make sense in a dev environment + current_renderer == null && + node_name(element) === INPUT_TAG && + 'type' in next && + ('value' in next || '__value' in next) + ) { + var type = next.type; + + if (type !== current.type || (type === undefined && element.hasAttribute('type'))) { + current.type = type; + set_attribute(element, 'type', type, skip_warning); + } + } + // since key is captured we use const for (const key in next) { // let instead of var because referenced in a closure diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 55e61c3774..dadcac369c 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -7,7 +7,6 @@ import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { tick, untrack } from '../../../runtime.js'; -import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; import { async_mode_flag } from '../../../../flags/index.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6f5d0d35e9..2f2d741447 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,4 +1,4 @@ -/** @import { Blocker, Effect, Value } from '#client' */ +/** @import { Blocker, Effect, Source, Value } from '#client' */ import { DESTROYED, STALE_REACTION } from '#client/constants'; import { DEV } from 'esm-env'; import { @@ -39,8 +39,22 @@ export function flatten(blockers, sync, async, fn) { // Filter out already-settled blockers - no need to wait for them var pending = blockers.filter((b) => !b.settled); + var deriveds = sync.map(d); + + if (DEV) { + deriveds.forEach((d, i) => { + // TODO this is kinda useful for debugging but a lousy implementation — + // maybe the compiler could pass through the template string + d.label = sync[i] + .toString() + .replace('() => ', '') + .replaceAll('$.eager(() => ', '$state.eager(') + .replace(/\$\.get\((.+?)\)/g, (_, id) => id); + }); + } + if (async.length === 0 && pending.length === 0) { - fn(sync.map(d)); + fn(deriveds); return; } @@ -54,8 +68,10 @@ export function flatten(blockers, sync, async, fn) { ? Promise.all(pending.map((b) => b.promise)) : null; - /** @param {Value[]} values */ - function finish(values) { + /** + * @param {Source[]} async + */ + function finish(async) { if ((parent.f & DESTROYED) !== 0) { return; } @@ -63,7 +79,7 @@ export function flatten(blockers, sync, async, fn) { restore(); try { - fn(values); + fn([...deriveds, ...async]); } catch (error) { invoke_error_boundary(error, parent); } @@ -75,17 +91,14 @@ export function flatten(blockers, sync, async, fn) { // Fast path: blockers but no async expressions if (async.length === 0) { - /** @type {Promise} */ (blocker_promise) - .then(() => finish(sync.map(d))) - .finally(decrement_pending); - + /** @type {Promise} */ (blocker_promise).then(() => finish([])).finally(decrement_pending); return; } // Full path: has async expressions function run() { Promise.all(async.map((expression) => async_derived(expression))) - .then((result) => finish([...sync.map(d), ...result])) + .then(finish) .catch((error) => invoke_error_boundary(error, parent)) .finally(decrement_pending); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8dbcacf429..3b82059788 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -140,12 +140,6 @@ export class Batch { */ #discard_callbacks = new Set(); - /** - * Callbacks that should run only when a fork is committed. - * @type {Set<(batch: Batch) => void>} - */ - #fork_commit_callbacks = new Set(); - /** * The number of async effects that are currently in flight */ @@ -634,7 +628,6 @@ export class Batch { discard() { for (const fn of this.#discard_callbacks) fn(this); this.#discard_callbacks.clear(); - this.#fork_commit_callbacks.clear(); this.#unlink(); this.#deferred?.resolve(); @@ -840,16 +833,6 @@ export class Batch { this.#discard_callbacks.add(fn); } - /** @param {(batch: Batch) => void} fn */ - on_fork_commit(fn) { - this.#fork_commit_callbacks.add(fn); - } - - run_fork_commit_callbacks() { - for (const fn of this.#fork_commit_callbacks) fn(this); - this.#fork_commit_callbacks.clear(); - } - settled() { return (this.#deferred ??= deferred()).promise; } @@ -1410,10 +1393,6 @@ export function fork(fn) { source.wv = increment_write_version(); } - batch.activate(); - batch.run_fork_commit_callbacks(); - batch.deactivate(); - // trigger any `$state.eager(...)` expressions with the new state. // eager effects don't get scheduled like other effects, so we // can't just encounter them during traversal, we need to diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 376783a7db..b14b129066 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -389,7 +389,9 @@ export function render_effect(fn, flags = 0) { */ export function template_effect(fn, sync = [], async = [], blockers = []) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get))); + create_effect(RENDER_EFFECT, () => { + fn(...values.map(get)); + }); }); } @@ -522,7 +524,7 @@ export function destroy_effect(effect, remove_dom = true) { removed = true; } - set_signal_status(effect, DESTROYING); + effect.f |= DESTROYING; destroy_effect_children(effect, remove_dom && !removed); remove_reactions(effect, 0); diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5626639a84..c2f3698809 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -49,11 +49,11 @@ export function update_pre_prop(fn, d = 1) { /** * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`). * Is passed the full `$$props` object and excludes the named props. - * @type {ProxyHandler<{ props: Record, exclude: Array, name?: string }>}} + * @type {ProxyHandler<{ props: Record, exclude: Set, name?: string }>}} */ const rest_props_handler = { get(target, key) { - if (target.exclude.includes(key)) return; + if (target.exclude.has(key)) return; return target.props[key]; }, set(target, key) { @@ -65,7 +65,7 @@ const rest_props_handler = { return false; }, getOwnPropertyDescriptor(target, key) { - if (target.exclude.includes(key)) return; + if (target.exclude.has(key)) return; if (key in target.props) { return { enumerable: true, @@ -75,17 +75,17 @@ const rest_props_handler = { } }, has(target, key) { - if (target.exclude.includes(key)) return false; + if (target.exclude.has(key)) return false; return key in target.props; }, ownKeys(target) { - return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key)); + return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key)); } }; /** * @param {Record} props - * @param {string[]} exclude + * @param {Set} exclude * @param {string} [name] * @returns {Record} */ diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 2e70bf518d..38cf3ebe4f 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -132,11 +132,12 @@ export class SvelteURLSearchParams extends URLSearchParams { * @returns {void} */ set(name, value) { - var previous = super.getAll(name).join(''); + var previous = super.getAll(name); super.set(name, value); // can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3 // if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true - if (previous !== super.getAll(name).join('')) { + var current = super.getAll(name); + if (previous.length !== current.length || previous.some((value, i) => value !== current[i])) { this.#update_url(); increment(this.#version); } diff --git a/packages/svelte/src/reactivity/url-search-params.test.ts b/packages/svelte/src/reactivity/url-search-params.test.ts index 27a413a553..b0c84872b0 100644 --- a/packages/svelte/src/reactivity/url-search-params.test.ts +++ b/packages/svelte/src/reactivity/url-search-params.test.ts @@ -55,6 +55,44 @@ test('URLSearchParams.set', () => { cleanup(); }); +test('URLSearchParams.set updates when duplicate values collapse to the same joined string', () => { + const params = new SvelteURLSearchParams('a=ab&a=c'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(params.toString()); + }); + }); + + flushSync(() => { + params.set('a', 'abc'); + }); + + assert.deepEqual(log, ['a=ab&a=c', 'a=abc']); + + cleanup(); +}); + +test('URLSearchParams.set updates when duplicate values collapse to the same comma-joined string', () => { + const params = new SvelteURLSearchParams('a=a&a=b'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(params.toString()); + }); + }); + + flushSync(() => { + params.set('a', 'a,b'); + }); + + assert.deepEqual(log, ['a=a&a=b', 'a=a%2Cb']); + + cleanup(); +}); + test('URLSearchParams.append', () => { const params = new SvelteURLSearchParams(); const log: any = []; diff --git a/packages/svelte/src/reactivity/url.test.ts b/packages/svelte/src/reactivity/url.test.ts index a76efeb2b2..a79aa32315 100644 --- a/packages/svelte/src/reactivity/url.test.ts +++ b/packages/svelte/src/reactivity/url.test.ts @@ -115,6 +115,25 @@ test('url.searchParams', () => { cleanup(); }); +test('url.searchParams.set updates url when duplicate values collapse to the same joined string', () => { + const url = new SvelteURL('https://svelte.dev?a=ab&a=c'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(url.href); + }); + }); + + flushSync(() => { + url.searchParams.set('a', 'abc'); + }); + + assert.deepEqual(log, ['https://svelte.dev/?a=ab&a=c', 'https://svelte.dev/?a=abc']); + + cleanup(); +}); + test('url.search normalizes value', () => { const url = new SvelteURL('https://svelte.dev'); const log: any = []; diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 638534d369..1b080d134a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.55.10'; +export const VERSION = '5.56.0'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js new file mode 100644 index 0000000000..2e605631c5 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +// A tag whose expression ends with a bare `/` at the end of the input used to +// make `find_matching_bracket` loop forever; it should error instead of hanging. +export default test({ + error: { + code: 'unexpected_eof', + message: 'Unexpected end of input', + position: [12, 12] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte new file mode 100644 index 0000000000..8ffdb88a18 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte @@ -0,0 +1 @@ +{let x = a / \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte new file mode 100644 index 0000000000..036d5283f5 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte @@ -0,0 +1,5 @@ +{#if true} + {let } + {const x = } + {let x = a / } +{/if} diff --git a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json new file mode 100644 index 0000000000..270d16af94 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json @@ -0,0 +1,145 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 54, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "IfBlock", + "elseif": false, + "start": 0, + "end": 54, + "test": { + "type": "Literal", + "start": 5, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 5 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "value": true, + "raw": "true" + }, + "consequent": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 10, + "end": 12, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "DeclarationTag", + "start": 12, + "end": 18, + "declaration": { + "type": "VariableDeclaration", + "kind": "let", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "Identifier", + "name": "", + "start": 17, + "end": 17 + }, + "init": null, + "start": 17, + "end": 17 + } + ], + "start": 13, + "end": 17 + } + }, + { + "type": "Text", + "start": 18, + "end": 20, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "DeclarationTag", + "start": 20, + "end": 32, + "declaration": { + "type": "VariableDeclaration", + "kind": "const", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "Identifier", + "name": "", + "start": 31, + "end": 31 + }, + "init": null, + "start": 31, + "end": 31 + } + ], + "start": 21, + "end": 31 + } + }, + { + "type": "Text", + "start": 32, + "end": 34, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "DeclarationTag", + "start": 34, + "end": 48, + "declaration": { + "type": "VariableDeclaration", + "kind": "let", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "Identifier", + "name": "", + "start": 47, + "end": 47 + }, + "init": null, + "start": 47, + "end": 47 + } + ], + "start": 35, + "end": 47 + } + }, + { + "type": "Text", + "start": 48, + "end": 49, + "raw": "\n", + "data": "\n" + } + ] + }, + "alternate": null + } + ] + }, + "options": null +} diff --git a/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte new file mode 100644 index 0000000000..fa4195996e --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte @@ -0,0 +1 @@ +{type instanceof /* probe */ Object} diff --git a/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json new file mode 100644 index 0000000000..4fe6468742 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json @@ -0,0 +1,92 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 36, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 0, + "end": 36, + "expression": { + "type": "BinaryExpression", + "start": 1, + "end": 35, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 35 + } + }, + "left": { + "type": "Identifier", + "start": 1, + "end": 5, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 5 + } + }, + "name": "type" + }, + "operator": "instanceof", + "right": { + "type": "Identifier", + "start": 29, + "end": 35, + "loc": { + "start": { + "line": 1, + "column": 29 + }, + "end": { + "line": 1, + "column": 35 + } + }, + "name": "Object", + "leadingComments": [ + { + "type": "Block", + "value": " probe ", + "start": 17, + "end": 28 + } + ] + } + } + } + ] + }, + "options": null, + "comments": [ + { + "type": "Block", + "value": " probe ", + "start": 17, + "end": 28, + "loc": { + "start": { + "line": 1, + "column": 17 + }, + "end": { + "line": 1, + "column": 28 + } + } + } + ] +} diff --git a/packages/svelte/tests/print/samples/declaration-tag/input.svelte b/packages/svelte/tests/print/samples/declaration-tag/input.svelte new file mode 100644 index 0000000000..6dbb2af2dd --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag/input.svelte @@ -0,0 +1,7 @@ +{#if visible} + {let count = 1} + {const doubled = count * 2} + {const label = 'count'} + {const format = (value) => `${label}: ${value}`} +

{format(doubled)}

+{/if} diff --git a/packages/svelte/tests/print/samples/declaration-tag/output.svelte b/packages/svelte/tests/print/samples/declaration-tag/output.svelte new file mode 100644 index 0000000000..6dbb2af2dd --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag/output.svelte @@ -0,0 +1,7 @@ +{#if visible} + {let count = 1} + {const doubled = count * 2} + {const label = 'count'} + {const format = (value) => `${label}: ${value}`} +

{format(doubled)}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/_config.js new file mode 100644 index 0000000000..0dd4b870d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: `

Hello, world!

5 01234 5 sync 6 5 0 10`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual(target.innerHTML, `

Hello, world!

5 01234 5 sync 6 5 0 10`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/main.svelte new file mode 100644 index 0000000000..f15065e4db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/main.svelte @@ -0,0 +1,31 @@ + + + + {const sync = 'sync'} + {const number = await Promise.resolve(5)} + {const after_async =number + 1} + {const { length, 0: first } = await '01234'} + + {#snippet greet()} + {const greeting = $derived(await `Hello, ${name}!`)} +

{greeting}

+ {number} + {#if number > 4 && after_async && greeting} + {const length = $derived(await number)} + {#each { length }, index} + {const i = $derived(await index)} + {i} + {/each} + {/if} + {/snippet} + + {@render greet()} + {number} {sync} {after_async} {length} {first} + + {#if sync} + {const double = $derived(number * 2)} + {double} + {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js new file mode 100644 index 0000000000..8fd2fc0976 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [top, change] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + change.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello other

+
nested Hi other
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte new file mode 100644 index 0000000000..4521ea2e41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte @@ -0,0 +1,20 @@ + + +{let name = $state(top_id)} + + +{#if id} + {let name = $state(await id)} + {let greeting = $derived(await `Hello ${name}`)} + + +

{greeting}

+
+ {const nested = 'nested'} + {const greeting2 = $derived(await `Hi ${name}`)} + {nested} {greeting2} +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js new file mode 100644 index 0000000000..c5846ba0bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

count: 0

doubled: 0

quadrupled: 0

`, + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `

count: 1

doubled: 2

quadrupled: 4

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte new file mode 100644 index 0000000000..d6fb542396 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte @@ -0,0 +1,10 @@ + +{let count = $state(0), doubled = $derived(count * 2)} + + +{ let quadrupled = $derived(doubled * 2) } + + +

count: {count}

+

doubled: {doubled}

+

quadrupled: {quadrupled}

diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js new file mode 100644 index 0000000000..901df42df7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '', + async test({ assert, target }) { + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte new file mode 100644 index 0000000000..da72bc69c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte @@ -0,0 +1,3 @@ +{let count = $state(0)} +{let doubled = $derived(count * 2)} + diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js new file mode 100644 index 0000000000..54e390ec83 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

4 total

nested
nested
`, + async test({ assert, target }) { + const [top, toggle, increment] = target.querySelectorAll('button'); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

6 total

nested
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte new file mode 100644 index 0000000000..5953104092 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte @@ -0,0 +1,29 @@ + + +{let top = $state(1)} +{let top_doubled = $derived(top * 2)} + + + + +{#if visible} + {let counter = $state({ value: initial })} + {let doubled = $derived(counter.value * 2)} + {const suffix = ' total'} + {const format = (value) => `${value}${suffix}`} + + +

{format(doubled)}

+
+ {const doubled = 'nested'} + {doubled} +
+{/if} + +
+ {const nested = 'nested'} + {nested} +
diff --git a/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js new file mode 100644 index 0000000000..7aeaf38eb0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +const value = 'line1\nline2\nline3'; + +export default test({ + mode: ['client'], + test({ assert, target }) { + const [spread_first, type_first] = target.querySelectorAll('input'); + + assert.equal(spread_first?.type, 'hidden'); + assert.equal(spread_first?.value, value); + assert.equal(type_first?.type, 'hidden'); + assert.equal(type_first?.value, value); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte new file mode 100644 index 0000000000..16e72ab704 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js index 0e2ceae197..a89ddd94eb 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js @@ -3,7 +3,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(`

`); +var root = $.from_html(`

`); export default function Async_const($$anchor) { var fragment = $.comment(); @@ -19,7 +19,7 @@ export default function Async_const($$anchor) { () => b = $.derived(() => $.get(a) + 1) ]); - var p = root_1(); + var p = root(); var text = $.child(p, true); $.reset(p); diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js new file mode 100644 index 0000000000..d0d474fa29 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js @@ -0,0 +1,44 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; + +var root = $.from_html(`

hello

`); +var root_1 = $.from_html(` `, 1); + +export default function Dedupe_templates($$anchor, $$props) { + var fragment = root_1(); + var node = $.first_child(fragment); + + { + var consequent = ($$anchor) => { + var p = root(); + + $.append($$anchor, p); + }; + + var alternate = ($$anchor) => { + var p_1 = root(); + + $.append($$anchor, p_1); + }; + + $.if(node, ($$render) => { + if ($$props.a) $$render(consequent); else $$render(alternate, -1); + }); + } + + var node_1 = $.sibling(node, 2); + + { + var consequent_1 = ($$anchor) => { + var p_2 = root(); + + $.append($$anchor, p_2); + }; + + $.if(node_1, ($$render) => { + if ($$props.b) $$render(consequent_1); + }); + } + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js new file mode 100644 index 0000000000..1f4a56779e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js @@ -0,0 +1,24 @@ +import * as $ from 'svelte/internal/server'; + +export default function Dedupe_templates($$renderer, $$props) { + let { a, b } = $$props; + + if (a) { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } else { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } + + $$renderer.push(` `); + + if (b) { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } else { + $$renderer.push(''); + } + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte b/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte new file mode 100644 index 0000000000..c75b5481a5 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte @@ -0,0 +1,14 @@ + + + +{#if a} +

hello

+{:else} +

hello

+{/if} + +{#if b} +

hello

+{/if} diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js index 98fc065144..630d9874a4 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -3,14 +3,14 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(``); +var root = $.from_html(``); export default function Delegated_locally_declared_shadowed($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => { - var button = root_1(); + var button = root(); $.set_attribute(button, 'data-index', index); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 36b446034b..fe4f245169 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -3,14 +3,14 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(`

`); +var root = $.from_html(`

`); export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { - var p = root_1(); + var p = root(); p.textContent = `index: ${i}`; $.append($$anchor, p); diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js index 6f7b2ef3ef..94e3606901 100644 --- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js @@ -2,10 +2,12 @@ import 'svelte/internal/init-operations'; import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; +var rest_excludes = new Set(['$$slots', '$$events', '$$legacy']); + export default function Props_identifier($$anchor, $$props) { $.push($$props, true); - let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']); + let props = $.rest_props($$props, rest_excludes); $$props.a; props[a]; diff --git a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js index 78feced63d..42468c4e04 100644 --- a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js @@ -5,63 +5,51 @@ import * as $ from 'svelte/internal/client'; import Option from './Option.svelte'; const opt = ($$anchor) => { - var option = root_1(); + var option = root(); $.append($$anchor, option); }; const option_snippet = ($$anchor) => { - var option_1 = root_2(); + var option_1 = root_1(); $.append($$anchor, option_1); }; const option_snippet2 = ($$anchor) => { - var option_2 = root_3(); + var option_2 = root_2(); $.append($$anchor, option_2); }; const conditional_option = ($$anchor) => { - var option_3 = root_4(); + var option_3 = root_3(); $.append($$anchor, option_3); }; -var root_1 = $.from_html(``); -var root_2 = $.from_html(``); -var root_3 = $.from_html(``); -var root_4 = $.from_html(``); +var root = $.from_html(``); +var root_1 = $.from_html(``); +var root_2 = $.from_html(``); +var root_3 = $.from_html(``); var option_content = $.from_html(`Rich`, 1); -var root_5 = $.from_html(``); -var root_6 = $.from_html(``); -var root_7 = $.from_html(``); +var root_4 = $.from_html(``); +var root_5 = $.from_html(``); +var root_6 = $.from_html(``); var select_content = $.from_html(``, 1); -var root_8 = $.from_html(``); var option_content_1 = $.from_html(`Bold`, 1); -var root_9 = $.from_html(``); var option_content_2 = $.from_html(`Italic text`, 1); var option_content_3 = $.from_html(` `, 1); -var root_10 = $.from_html(``); -var root_12 = $.from_html(``); -var root_13 = $.from_html(``); +var root_7 = $.from_html(``); +var root_8 = $.from_html(``); var option_content_4 = $.from_html(`Rich in boundary`, 1); -var root_14 = $.from_html(``); -var select_content_1 = $.from_html(``, 1); -var select_content_2 = $.from_html(``, 1); -var select_content_3 = $.from_html(``, 1); -var optgroup_content = $.from_html(``, 1); -var optgroup_content_1 = $.from_html(``, 1); -var option_content_5 = $.from_html(``, 1); -var select_content_4 = $.from_html(``, 1); -var select_content_5 = $.from_html(``, 1); -var root = $.from_html(` `, 1); +var root_9 = $.from_html(` `, 1); export default function Select_with_rich_content($$anchor) { let items = [1, 2, 3]; let show = true; let html = ''; - var fragment = root(); + var fragment = root_9(); var select = $.first_child(fragment); var option_4 = $.child(select); @@ -77,7 +65,7 @@ export default function Select_with_rich_content($$anchor) { var select_1 = $.sibling(select, 2); $.each(select_1, 5, () => items, $.index, ($$anchor, item) => { - var option_5 = root_5(); + var option_5 = root_4(); var text = $.child(option_5, true); $.reset(option_5); @@ -102,7 +90,7 @@ export default function Select_with_rich_content($$anchor) { { var consequent = ($$anchor) => { - var option_6 = root_6(); + var option_6 = root_5(); $.append($$anchor, option_6); }; @@ -118,7 +106,7 @@ export default function Select_with_rich_content($$anchor) { var node_1 = $.child(select_3); $.key(node_1, () => items, ($$anchor) => { - var option_7 = root_7(); + var option_7 = root_6(); $.append($$anchor, option_7); }); @@ -140,7 +128,7 @@ export default function Select_with_rich_content($$anchor) { $.each(select_5, 5, () => items, $.index, ($$anchor, item) => { const x = $.derived_safe_equal(() => $.get(item) * 2); - var option_8 = root_8(); + var option_8 = root_4(); var text_1 = $.child(option_8, true); $.reset(option_8); @@ -178,7 +166,7 @@ export default function Select_with_rich_content($$anchor) { var optgroup_1 = $.child(select_7); $.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => { - var option_10 = root_9(); + var option_10 = root_4(); var text_2 = $.child(option_10, true); $.reset(option_10); @@ -216,7 +204,7 @@ export default function Select_with_rich_content($$anchor) { var select_9 = $.sibling(select_8, 2); $.each(select_9, 5, () => items, $.index, ($$anchor, item) => { - var option_12 = root_10(); + var option_12 = root_7(); $.customizable_select(option_12, () => { var anchor_4 = $.child(option_12); @@ -243,7 +231,7 @@ export default function Select_with_rich_content($$anchor) { var node_4 = $.first_child(fragment_6); $.each(node_4, 1, () => items, $.index, ($$anchor, item) => { - var option_13 = root_12(); + var option_13 = root_4(); var text_4 = $.child(option_13, true); $.reset(option_13); @@ -275,7 +263,7 @@ export default function Select_with_rich_content($$anchor) { var node_5 = $.child(select_11); $.boundary(node_5, {}, ($$anchor) => { - var option_14 = root_13(); + var option_14 = root_8(); $.append($$anchor, option_14); }); @@ -286,7 +274,7 @@ export default function Select_with_rich_content($$anchor) { var node_6 = $.child(select_12); $.boundary(node_6, {}, ($$anchor) => { - var option_15 = root_14(); + var option_15 = root_7(); $.customizable_select(option_15, () => { var anchor_5 = $.child(option_15); @@ -304,7 +292,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_13, () => { var anchor_6 = $.child(select_13); - var fragment_8 = select_content_1(); + var fragment_8 = select_content(); var node_7 = $.first_child(fragment_8); Option(node_7, {}); @@ -315,7 +303,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_14, () => { var anchor_7 = $.child(select_14); - var fragment_9 = select_content_2(); + var fragment_9 = select_content(); var node_8 = $.first_child(fragment_9); option_snippet(node_8); @@ -326,7 +314,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_15, () => { var anchor_8 = $.child(select_15); - var fragment_10 = select_content_3(); + var fragment_10 = select_content(); var node_9 = $.first_child(fragment_10); $.html(node_9, () => html); @@ -338,7 +326,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_2, () => { var anchor_9 = $.child(optgroup_2); - var fragment_11 = optgroup_content(); + var fragment_11 = select_content(); var node_10 = $.first_child(fragment_11); Option(node_10, {}); @@ -352,7 +340,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_3, () => { var anchor_10 = $.child(optgroup_3); - var fragment_12 = optgroup_content_1(); + var fragment_12 = select_content(); var node_11 = $.first_child(fragment_12); option_snippet2(node_11); @@ -366,7 +354,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(option_16, () => { var anchor_11 = $.child(option_16); - var fragment_13 = option_content_5(); + var fragment_13 = select_content(); var node_12 = $.first_child(fragment_13); $.html(node_12, () => 'Bold HTML'); @@ -379,7 +367,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_19, () => { var anchor_12 = $.child(select_19); - var fragment_14 = select_content_4(); + var fragment_14 = select_content(); var node_13 = $.first_child(fragment_14); $.each(node_13, 1, () => items, $.index, ($$anchor, item) => { @@ -393,7 +381,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_20, () => { var anchor_13 = $.child(select_20); - var fragment_16 = select_content_5(); + var fragment_16 = select_content(); var node_14 = $.first_child(fragment_16); { diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte new file mode 100644 index 0000000000..8cfdf59c0f --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {function foo() {}} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json new file mode 100644 index 0000000000..a375ac5776 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 28, + "column": 2 + }, + "end": { + "line": 28, + "column": 20 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte new file mode 100644 index 0000000000..88295cad58 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte @@ -0,0 +1,29 @@ + + +{#if true} + + {type} + {type } + {type && foo} + {type || bar} + {type % 2} + {type .x} + {type ?.x} + {type ()} + {type [1]} + {type `tag`} + {type === 'all' ? 'All types' : type} + {type !== 'all'} + {type == 'all'} + {type != 'all'} + {type + 1} + {type - 1} + {type * 2} + {type / 2} + {type > 1} + {type instanceof Foo} + {type instanceof /* comment */ Object} + {type in foo} + + {type foo = boolean} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json new file mode 100644 index 0000000000..2a9b3c0140 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 5 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte new file mode 100644 index 0000000000..eb9fcd5e75 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {var foo = 1} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json new file mode 100644 index 0000000000..6b89f2eab8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_no_legacy_mode", + "message": "Declaration tags cannot be used in legacy mode", + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 19 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte new file mode 100644 index 0000000000..026d4eff1e --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte @@ -0,0 +1,5 @@ + + +{const foo = 'foo'} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte new file mode 100644 index 0000000000..081f242a81 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte @@ -0,0 +1,6 @@ + + +Usage when no explicit runes/legacy mode should be ok +{const foo = world} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte new file mode 100644 index 0000000000..90cc208e72 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte @@ -0,0 +1,9 @@ + +{let a = $state(0), b = $derived(a * 2)} +{let c = $state(0)} +{let d = $derived(c * 2)} + + +{let e = $state(0), f = e} + +{a}{b}{c}{d}{e}{f} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json new file mode 100644 index 0000000000..9b87a3afb6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_referenced_locally", + "message": "This reference only captures the initial value of `e`. Did you mean to reference it inside a closure instead?", + "start": { + "line": 7, + "column": 24 + }, + "end": { + "line": 7, + "column": 25 + } + } +] diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index dd264d3239..44277c647e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1308,6 +1308,12 @@ declare module 'svelte/compiler' { }; } + /** A `{let ...}` or `{const ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration; + } + /** A `{@debug ...}` tag */ export interface DebugTag extends BaseNode { type: 'DebugTag'; @@ -1618,6 +1624,7 @@ declare module 'svelte/compiler' { export type Tag = | AST.AttachTag | AST.ConstTag + | AST.DeclarationTag | AST.DebugTag | AST.ExpressionTag | AST.HtmlTag diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c077f0f6b..767af0515a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 25.0.1 version: 25.0.1 playwright: - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 prettier: specifier: ^3.2.4 version: 3.2.4 @@ -121,8 +121,8 @@ importers: specifier: ^0.3.25 version: 0.3.31 '@playwright/test': - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 '@rollup/plugin-commonjs': specifier: ^28.0.1 version: 28.0.1(rollup@4.60.1) @@ -864,8 +864,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.0': - resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} hasBin: true @@ -1096,6 +1096,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1459,8 +1462,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -1699,8 +1702,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2162,13 +2165,13 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - playwright-core@1.58.0: - resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.58.0: - resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -2307,6 +2310,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2403,8 +2411,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} term-size@2.2.1: @@ -3283,9 +3291,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.0': + '@playwright/test@1.60.0': dependencies: - playwright: 1.58.0 + playwright: 1.60.0 '@polka/url@1.0.0-next.25': {} @@ -3463,13 +3471,15 @@ snapshots: '@types/eslint@8.56.12': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -3840,10 +3850,10 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 enquirer@2.4.1: dependencies: @@ -3945,7 +3955,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@10.0.0): dependencies: eslint: 10.0.0 - semver: 7.7.4 + semver: 7.8.1 eslint-config-prettier@9.1.0(eslint@10.0.0): dependencies: @@ -3965,14 +3975,14 @@ snapshots: eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.22.1 eslint: 10.0.0 eslint-plugin-es-x: 7.8.0(eslint@10.0.0) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.7.4 + semver: 7.8.1 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - typescript @@ -4176,7 +4186,7 @@ snapshots: function-bind@1.1.2: {} - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4596,11 +4606,11 @@ snapshots: pify@4.0.1: {} - playwright-core@1.58.0: {} + playwright-core@1.60.0: {} - playwright@1.58.0: + playwright@1.60.0: dependencies: - playwright-core: 1.58.0 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 @@ -4742,6 +4752,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.1: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -4829,7 +4841,7 @@ snapshots: symbol-tree@3.2.4: {} - tapable@2.3.2: {} + tapable@2.3.3: {} term-size@2.2.1: {}