From 66a21552f64eea77a46e22a46eb46d4ff010d949 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 14 May 2025 21:58:58 +0200 Subject: [PATCH 1/2] feat: allow generics on snippets (#15915) * feat: allow generics on snippets * chore: fix lint * reuse bracket matching logic * remove some unused stuff * chore: update name, test and types * chore: fix lint --------- Co-authored-by: Rich Harris --- .changeset/ten-colts-grab.md | 5 + .../compiler/phases/1-parse/read/context.js | 75 +---- .../src/compiler/phases/1-parse/state/tag.js | 20 ++ .../compiler/phases/1-parse/utils/bracket.js | 113 +++++-- .../svelte/src/compiler/types/template.d.ts | 1 + .../samples/generic-snippets/input.svelte | 10 + .../samples/generic-snippets/output.json | 299 ++++++++++++++++++ packages/svelte/types/index.d.ts | 1 + 8 files changed, 421 insertions(+), 103 deletions(-) create mode 100644 .changeset/ten-colts-grab.md create mode 100644 packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/generic-snippets/output.json diff --git a/.changeset/ten-colts-grab.md b/.changeset/ten-colts-grab.md new file mode 100644 index 000000000000..6e0e20bc84d7 --- /dev/null +++ b/.changeset/ten-colts-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow generics on snippets diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index f4c73dcf403a..b1189018306c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -1,7 +1,7 @@ /** @import { Location } from 'locate-character' */ /** @import { Pattern } from 'estree' */ /** @import { Parser } from '../index.js' */ -import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js'; +import { match_bracket } from '../utils/bracket.js'; import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; @@ -33,7 +33,9 @@ export default function read_pattern(parser) { }; } - if (!is_bracket_open(parser.template[i])) { + const char = parser.template[i]; + + if (char !== '{' && char !== '[') { e.expected_pattern(i); } @@ -71,75 +73,6 @@ export default function read_pattern(parser) { } } -/** - * @param {Parser} parser - * @param {number} start - */ -function match_bracket(parser, start) { - const bracket_stack = []; - - let i = start; - - while (i < parser.template.length) { - let char = parser.template[i++]; - - if (char === "'" || char === '"' || char === '`') { - i = match_quote(parser, i, char); - continue; - } - - if (is_bracket_open(char)) { - bracket_stack.push(char); - } else if (is_bracket_close(char)) { - const popped = /** @type {string} */ (bracket_stack.pop()); - const expected = /** @type {string} */ (get_bracket_close(popped)); - - if (char !== expected) { - e.expected_token(i - 1, expected); - } - - if (bracket_stack.length === 0) { - return i; - } - } - } - - e.unexpected_eof(parser.template.length); -} - -/** - * @param {Parser} parser - * @param {number} start - * @param {string} quote - */ -function match_quote(parser, start, quote) { - let is_escaped = false; - let i = start; - - while (i < parser.template.length) { - const char = parser.template[i++]; - - if (is_escaped) { - is_escaped = false; - continue; - } - - if (char === quote) { - return i; - } - - if (char === '\\') { - is_escaped = true; - } - - if (quote === '`' && char === '$' && parser.template[i] === '{') { - i = match_bracket(parser, i); - } - } - - e.unterminated_string_constant(start); -} - /** * @param {Parser} parser * @returns {any} 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 0eb98c27e858..4153463c8361 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -8,9 +8,12 @@ import { parse_expression_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'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; +const pointy_bois = { '<': '>' }; + /** @param {Parser} parser */ export default function tag(parser) { const start = parser.index; @@ -351,6 +354,22 @@ function open(parser) { const params_start = parser.index; + // snippets could have a generic signature, e.g. `#snippet foo(...)` + /** @type {string | undefined} */ + let type_params; + + // if we match a generic opening + if (parser.ts && parser.match('<')) { + const start = parser.index; + const end = match_bracket(parser, start, pointy_bois); + + type_params = parser.template.slice(start + 1, end - 1); + + parser.index = end; + } + + parser.allow_whitespace(); + const matched = parser.eat('(', true, false); if (matched) { @@ -388,6 +407,7 @@ function open(parser) { end: name_end, name }, + typeParams: type_params, parameters: function_expression.params, body: create_fragment(), metadata: { 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 b7c8cb43cd00..8c69a58c9980 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -1,34 +1,5 @@ -const SQUARE_BRACKET_OPEN = '['; -const SQUARE_BRACKET_CLOSE = ']'; -const CURLY_BRACKET_OPEN = '{'; -const CURLY_BRACKET_CLOSE = '}'; -const PARENTHESES_OPEN = '('; -const PARENTHESES_CLOSE = ')'; - -/** @param {string} char */ -export function is_bracket_open(char) { - return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN; -} - -/** @param {string} char */ -export function is_bracket_close(char) { - return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE; -} - -/** @param {string} open */ -export function get_bracket_close(open) { - if (open === SQUARE_BRACKET_OPEN) { - return SQUARE_BRACKET_CLOSE; - } - - if (open === CURLY_BRACKET_OPEN) { - return CURLY_BRACKET_CLOSE; - } - - if (open === PARENTHESES_OPEN) { - return PARENTHESES_CLOSE; - } -} +/** @import { Parser } from '../index.js' */ +import * as e from '../../../errors.js'; /** * @param {number} num @@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) { * @returns {number | undefined} The index of the closing bracket, or undefined if not found. */ export function find_matching_bracket(template, index, open) { - const close = get_bracket_close(open); + const close = default_brackets[open]; let brackets = 1; let i = index; while (brackets > 0 && i < template.length) { @@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) { } return undefined; } + +/** @type {Record} */ +const default_brackets = { + '{': '}', + '(': ')', + '[': ']' +}; + +/** + * @param {Parser} parser + * @param {number} start + * @param {Record} brackets + */ +export function match_bracket(parser, start, brackets = default_brackets) { + const close = Object.values(brackets); + const bracket_stack = []; + + let i = start; + + while (i < parser.template.length) { + let char = parser.template[i++]; + + if (char === "'" || char === '"' || char === '`') { + i = match_quote(parser, i, char); + continue; + } + + if (char in brackets) { + bracket_stack.push(char); + } else if (close.includes(char)) { + const popped = /** @type {string} */ (bracket_stack.pop()); + const expected = /** @type {string} */ (brackets[popped]); + + if (char !== expected) { + e.expected_token(i - 1, expected); + } + + if (bracket_stack.length === 0) { + return i; + } + } + } + + e.unexpected_eof(parser.template.length); +} + +/** + * @param {Parser} parser + * @param {number} start + * @param {string} quote + */ +function match_quote(parser, start, quote) { + let is_escaped = false; + let i = start; + + while (i < parser.template.length) { + const char = parser.template[i++]; + + if (is_escaped) { + is_escaped = false; + continue; + } + + if (char === quote) { + return i; + } + + if (char === '\\') { + is_escaped = true; + } + + if (quote === '`' && char === '$' && parser.template[i] === '{') { + i = match_bracket(parser, i); + } + } + + e.unterminated_string_constant(start); +} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 54fdda92b845..6dec1f2dbe15 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -468,6 +468,7 @@ export namespace AST { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; /** @internal */ metadata: { diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte new file mode 100644 index 000000000000..4ee619728d17 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json new file mode 100644 index 000000000000..b66ee7288f2e --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -0,0 +1,299 @@ +{ + "css": null, + "js": [], + "start": 30, + "end": 192, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "typeParams": "T extends string", + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 74, + "end": 76, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 81, + "end": 82, + "raw": "\n", + "data": "\n" + } + ] + } + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">", + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 174, + "end": 176, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 181, + "end": 182, + "raw": "\n", + "data": "\n" + } + ] + } + } + ] + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index ff9764b88b1a..bb958c510807 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1301,6 +1301,7 @@ declare module 'svelte/compiler' { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; } From e2a13a3beb246fe3f3e471353921a2db5ca1c57d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 16:03:05 -0400 Subject: [PATCH 2/2] Version Packages (#15916) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/ten-colts-grab.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/ten-colts-grab.md diff --git a/.changeset/ten-colts-grab.md b/.changeset/ten-colts-grab.md deleted file mode 100644 index 6e0e20bc84d7..000000000000 --- a/.changeset/ten-colts-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow generics on snippets diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 136219af42da..b25235276b65 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.30.0 + +### Minor Changes + +- feat: allow generics on snippets ([#15915](https://github.com/sveltejs/svelte/pull/15915)) + ## 5.29.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 312aa50c2577..5c27d889523d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.29.0", + "version": "5.30.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 449937489674..69492b1bf2be 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.29.0'; +export const VERSION = '5.30.0'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy