From 99ed00a7675948d1f9530e6d22110e24e316c6dd Mon Sep 17 00:00:00 2001 From: seroperson Date: Tue, 24 Mar 2026 00:26:48 +0300 Subject: [PATCH 1/2] fix(svelte): infer props for generic Svelte components Closes #14669 --- .changeset/spotty-birds-glow.md | 5 ++ packages/integrations/svelte/src/editor.cts | 53 ++++++++++++- .../integrations/svelte/test/check.test.js | 78 +++++++++++++++++++ .../tsconfig.generics-arrow-fail.json | 4 + .../tsconfig.generics-arrow-pass.json | 4 + .../prop-types/tsconfig.generics-fail.json | 4 + .../tsconfig.generics-multi-fail.json | 4 + .../tsconfig.generics-multi-pass.json | 4 + .../prop-types/tsconfig.generics-pass.json | 4 + .../types/generics/ArrowGeneric.svelte | 10 +++ .../types/generics/GenericList.svelte | 14 ++++ .../types/generics/MultiGeneric.svelte | 10 +++ .../types/generics/arrow-fail.astro | 5 ++ .../types/generics/arrow-pass.astro | 5 ++ .../prop-types/types/generics/fail.astro | 5 ++ .../types/generics/multi-fail.astro | 5 ++ .../types/generics/multi-pass.astro | 5 ++ .../prop-types/types/generics/pass.astro | 6 ++ 18 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 .changeset/spotty-birds-glow.md create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-fail.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-pass.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-fail.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-fail.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-pass.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-pass.json create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/ArrowGeneric.svelte create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/GenericList.svelte create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/MultiGeneric.svelte create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-fail.astro create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-pass.astro create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/fail.astro create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-fail.astro create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-pass.astro create mode 100644 packages/integrations/svelte/test/fixtures/prop-types/types/generics/pass.astro diff --git a/.changeset/spotty-birds-glow.md b/.changeset/spotty-birds-glow.md new file mode 100644 index 000000000000..b15d8167ed06 --- /dev/null +++ b/.changeset/spotty-birds-glow.md @@ -0,0 +1,5 @@ +--- +'@astrojs/svelte': patch +--- + +Using a Svelte component with generic type parameters now correctly infer props in .astro files diff --git a/packages/integrations/svelte/src/editor.cts b/packages/integrations/svelte/src/editor.cts index 6d7fcf3390bd..4c3ef01bb6ee 100644 --- a/packages/integrations/svelte/src/editor.cts +++ b/packages/integrations/svelte/src/editor.cts @@ -12,10 +12,19 @@ export function toTSX(code: string, className: string): string { // New svelte2tsx output (Svelte 5) if (tsx.includes('export default $$Component;')) { - result = tsx.replace( - 'export default $$Component;', - `export default function ${className}__AstroComponent_(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives>): any {}`, - ); + const generics = extractGenerics(tsx); + if (generics) { + // Generic component: preserve type params using __sveltets_Render + result = tsx.replace( + 'export default $$Component;', + `export default function ${className}__AstroComponent_<${generics.params}>(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives['props']>>): any {}`, + ); + } else { + result = tsx.replace( + 'export default $$Component;', + `export default function ${className}__AstroComponent_(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives>): any {}`, + ); + } } else { // Old svelte2tsx output result = tsx.replace( @@ -29,3 +38,39 @@ export function toTSX(code: string, className: string): string { return result; } + +function extractGenerics(tsx: string): { params: string; names: string } | null { + const marker = 'class __sveltets_Render<'; + const startIdx = tsx.indexOf(marker); + if (startIdx === -1) return null; + + const genericStart = startIdx + marker.length; + let depth = 1; + let i = genericStart; + while (i < tsx.length && depth > 0) { + if (tsx[i] === '<') depth++; + if (tsx[i] === '>' && tsx[i - 1] !== '=') depth--; + i++; + } + const params = tsx.substring(genericStart, i - 1); + + // Extract just the type parameter names by splitting at top-level commas + const names: string[] = []; + let current = ''; + let d = 0; + for (const ch of params) { + if (ch === '<') d++; + if (ch === '>') d--; + if (ch === ',' && d === 0) { + const name = /^(\w+)/.exec(current.trim())?.[1]; + if (name) names.push(name); + current = ''; + } else { + current += ch; + } + } + const lastName = /^(\w+)/.exec(current.trim())?.[1]; + if (lastName) names.push(lastName); + + return { params, names: names.join(', ') }; +} diff --git a/packages/integrations/svelte/test/check.test.js b/packages/integrations/svelte/test/check.test.js index 207180c8f5a5..6a99bc20d5e5 100644 --- a/packages/integrations/svelte/test/check.test.js +++ b/packages/integrations/svelte/test/check.test.js @@ -68,6 +68,84 @@ describe('Svelte Check', () => { ); }); + it('should pass check for generic component with correct props', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-pass.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode, stdout, stderr } = await getResult(); + + if (exitCode !== 0) { + console.error(stdout); + console.error(stderr); + } + assert.equal(exitCode, 0, 'Expected check to pass (exit code 0)'); + }); + + it('should fail check for generic component with incorrect props', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-fail.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode } = await getResult(); + + assert.equal(exitCode, 1, 'Expected check to fail (exit code 1)'); + }); + + it('should pass check for multi-param generic with nested angle brackets', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-multi-pass.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode, stdout, stderr } = await getResult(); + + if (exitCode !== 0) { + console.error(stdout); + console.error(stderr); + } + assert.equal(exitCode, 0, 'Expected check to pass (exit code 0)'); + }); + + it('should fail check for multi-param generic with invalid key', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-multi-fail.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode } = await getResult(); + + assert.equal(exitCode, 1, 'Expected check to fail (exit code 1)'); + }); + + it('should pass check for generic with arrow function constraint', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-arrow-pass.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode, stdout, stderr } = await getResult(); + + if (exitCode !== 0) { + console.error(stdout); + console.error(stderr); + } + assert.equal(exitCode, 0, 'Expected check to pass (exit code 0)'); + }); + + it('should fail check for generic with arrow function returning wrong type', async () => { + const root = fileURLToPath(new URL('./fixtures/prop-types/types/generics', import.meta.url)); + const tsConfigPath = fileURLToPath( + new URL('./fixtures/prop-types/tsconfig.generics-arrow-fail.json', import.meta.url), + ); + const { getResult } = cli('check', '--tsconfig', tsConfigPath, '--root', root); + const { exitCode } = await getResult(); + + assert.equal(exitCode, 1, 'Expected check to fail (exit code 1)'); + }); + it('should fail check on invalid element children', { skip: true }, async () => { const root = fileURLToPath(new URL('./fixtures/prop-types/types/children', import.meta.url)); const tsConfigPath = fileURLToPath( diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-fail.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-fail.json new file mode 100644 index 000000000000..440c7a75bec1 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-fail.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/arrow-fail.astro", "types/generics/ArrowGeneric.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-pass.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-pass.json new file mode 100644 index 000000000000..5fa47bef7c8d --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-arrow-pass.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/arrow-pass.astro", "types/generics/ArrowGeneric.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-fail.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-fail.json new file mode 100644 index 000000000000..eeb847e5fe53 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-fail.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/fail.astro", "types/generics/GenericList.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-fail.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-fail.json new file mode 100644 index 000000000000..dd5b1dd757b3 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-fail.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/multi-fail.astro", "types/generics/MultiGeneric.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-pass.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-pass.json new file mode 100644 index 000000000000..99efeb9f69a3 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-multi-pass.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/multi-pass.astro", "types/generics/MultiGeneric.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-pass.json b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-pass.json new file mode 100644 index 000000000000..15b20363c4c7 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/tsconfig.generics-pass.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["types/generics/pass.astro", "types/generics/GenericList.svelte"] +} diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/ArrowGeneric.svelte b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/ArrowGeneric.svelte new file mode 100644 index 000000000000..8ee526c6bf45 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/ArrowGeneric.svelte @@ -0,0 +1,10 @@ + + +

{predicate(value)}

diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/GenericList.svelte b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/GenericList.svelte new file mode 100644 index 000000000000..502d0621d41e --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/GenericList.svelte @@ -0,0 +1,14 @@ + + +
    + {#each items as item} +
  • {item} ({variant})
  • + {/each} +
diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/MultiGeneric.svelte b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/MultiGeneric.svelte new file mode 100644 index 000000000000..0bcd3bd8f067 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/MultiGeneric.svelte @@ -0,0 +1,10 @@ + + +

{String(data[key])}

diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-fail.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-fail.astro new file mode 100644 index 000000000000..bc126dc250c1 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-fail.astro @@ -0,0 +1,5 @@ +--- +import ArrowGeneric from './ArrowGeneric.svelte'; +--- + + 123} value="test" /> diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-pass.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-pass.astro new file mode 100644 index 000000000000..b77b9fdc8733 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/arrow-pass.astro @@ -0,0 +1,5 @@ +--- +import ArrowGeneric from './ArrowGeneric.svelte'; +--- + + val.length > 0} value="test" /> diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/fail.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/fail.astro new file mode 100644 index 000000000000..759e67f4d330 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/fail.astro @@ -0,0 +1,5 @@ +--- +import GenericList from './GenericList.svelte'; +--- + + diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-fail.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-fail.astro new file mode 100644 index 000000000000..953011304afc --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-fail.astro @@ -0,0 +1,5 @@ +--- +import MultiGeneric from './MultiGeneric.svelte'; +--- + + diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-pass.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-pass.astro new file mode 100644 index 000000000000..bf5f0067cd0b --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/multi-pass.astro @@ -0,0 +1,5 @@ +--- +import MultiGeneric from './MultiGeneric.svelte'; +--- + + diff --git a/packages/integrations/svelte/test/fixtures/prop-types/types/generics/pass.astro b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/pass.astro new file mode 100644 index 000000000000..d19c88ff5308 --- /dev/null +++ b/packages/integrations/svelte/test/fixtures/prop-types/types/generics/pass.astro @@ -0,0 +1,6 @@ +--- +import GenericList from './GenericList.svelte'; +--- + + + From de2dab0979102dd954f6f101b4434bcd8628b54d Mon Sep 17 00:00:00 2001 From: seroperson Date: Thu, 26 Mar 2026 15:33:35 +0300 Subject: [PATCH 2/2] Added unit-tests for extractGenerics --- packages/integrations/svelte/src/editor.cts | 55 ++++--- .../svelte/test/extract-generics.test.js | 146 ++++++++++++++++++ 2 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 packages/integrations/svelte/test/extract-generics.test.js diff --git a/packages/integrations/svelte/src/editor.cts b/packages/integrations/svelte/src/editor.cts index 4c3ef01bb6ee..a13559dc46ed 100644 --- a/packages/integrations/svelte/src/editor.cts +++ b/packages/integrations/svelte/src/editor.cts @@ -1,5 +1,7 @@ import { svelte2tsx } from 'svelte2tsx'; +const genericNameRE = /^(?:const |in |out )*(\w+)/; + export function toTSX(code: string, className: string): string { let result = ` let ${className}__AstroComponent_: Error @@ -13,18 +15,14 @@ export function toTSX(code: string, className: string): string { // New svelte2tsx output (Svelte 5) if (tsx.includes('export default $$Component;')) { const generics = extractGenerics(tsx); - if (generics) { - // Generic component: preserve type params using __sveltets_Render - result = tsx.replace( - 'export default $$Component;', - `export default function ${className}__AstroComponent_<${generics.params}>(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives['props']>>): any {}`, - ); - } else { - result = tsx.replace( - 'export default $$Component;', - `export default function ${className}__AstroComponent_(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives>): any {}`, - ); - } + const genericSuffix = generics ? `<${generics.params}>` : ''; + const innerType = generics + ? `ReturnType<__sveltets_Render<${generics.names}>['props']>` + : `import('svelte').ComponentProps`; + result = tsx.replace( + 'export default $$Component;', + `export default function ${className}__AstroComponent_${genericSuffix}(_props: import('@astrojs/svelte/svelte-shims.d.ts').PropsWithClientDirectives<${innerType}>): any {}`, + ); } else { // Old svelte2tsx output result = tsx.replace( @@ -39,11 +37,22 @@ export function toTSX(code: string, className: string): string { return result; } -function extractGenerics(tsx: string): { params: string; names: string } | null { +// Extracts generic type parameters from svelte2tsx output for generic Svelte components +// +// Given: `class __sveltets_Render, U extends keyof T>`, +// Returns: `{ params: "T extends Record, U extends keyof T", names: "T, U" }` +// +// `params` is the full declaration (used to define the generic function), +// `names` is just the identifiers (used to pass type args to __sveltets_Render). +// +// Returns null when the input has no __sveltets_Render class (non-generic component). +export function extractGenerics(tsx: string): { params: string; names: string } | null { const marker = 'class __sveltets_Render<'; const startIdx = tsx.indexOf(marker); if (startIdx === -1) return null; + // Find the matching `>` that closes the generic parameter list, + // tracking `<>` depth and skipping `=>` (arrow return types) const genericStart = startIdx + marker.length; let depth = 1; let i = genericStart; @@ -54,22 +63,28 @@ function extractGenerics(tsx: string): { params: string; names: string } | null } const params = tsx.substring(genericStart, i - 1); - // Extract just the type parameter names by splitting at top-level commas + // Split params by top-level commas to extract individual type parameter names. + // Tracks bracket depth for `<>`, `()`, `{}`, `[]` to skip commas inside + // nested types like `Record`, `[string, number]`, + // `{ a: string, b: number }`, or `(a: number, b: string) => void` const names: string[] = []; let current = ''; - let d = 0; + let depth2 = 0; + let prev = ''; for (const ch of params) { - if (ch === '<') d++; - if (ch === '>') d--; - if (ch === ',' && d === 0) { - const name = /^(\w+)/.exec(current.trim())?.[1]; + if (ch === '<' || ch === '(' || ch === '{' || ch === '[') depth2++; + if ((ch === '>' && prev !== '=') || ch === ')' || ch === '}' || ch === ']') depth2--; + if (ch === ',' && depth2 === 0) { + // Skip variance/const modifiers to get the actual type parameter name + const name = genericNameRE.exec(current.trim())?.[1]; if (name) names.push(name); current = ''; } else { current += ch; } + prev = ch; } - const lastName = /^(\w+)/.exec(current.trim())?.[1]; + const lastName = genericNameRE.exec(current.trim())?.[1]; if (lastName) names.push(lastName); return { params, names: names.join(', ') }; diff --git a/packages/integrations/svelte/test/extract-generics.test.js b/packages/integrations/svelte/test/extract-generics.test.js new file mode 100644 index 000000000000..9c20408b116a --- /dev/null +++ b/packages/integrations/svelte/test/extract-generics.test.js @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { extractGenerics } from '../dist/editor.cjs'; + +describe('extractGenerics', () => { + it('should return null when no __sveltets_Render class is present', () => { + const tsx = 'const $$Component = __sveltets_2_isomorphic_component($$render());'; + assert.equal(extractGenerics(tsx), null); + }); + + it('should extract a single type parameter', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { params: 'T', names: 'T' }); + }); + + it('should extract a single type parameter with constraint', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { params: 'T extends boolean = false', names: 'T' }); + }); + + it('should extract multiple type parameters', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { params: 'T, U', names: 'T, U' }); + }); + + it('should extract multiple type parameters with constraints', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends string, U extends number = 0', + names: 'T, U', + }); + }); + + it('should handle nested angle brackets in constraints', () => { + const tsx = 'class __sveltets_Render, U extends keyof T> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends Record, U extends keyof T', + names: 'T, U', + }); + }); + + it('should handle deeply nested angle brackets', () => { + const tsx = 'class __sveltets_Render>> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends Map>', + names: 'T', + }); + }); + + it('should handle arrow function types in constraints', () => { + const tsx = 'class __sveltets_Render boolean> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends (val: string) => boolean', + names: 'T', + }); + }); + + it('should handle arrow function returning generic type', () => { + const tsx = 'class __sveltets_Render Promise> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends (x: number) => Promise', + names: 'T', + }); + }); + + it('should handle arrow function with multiple params after comma', () => { + const tsx = 'class __sveltets_Render boolean, U extends T> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends (a: string) => boolean, U extends T', + names: 'T, U', + }); + }); + + it('should handle const modifier on type parameter', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'const T extends readonly string[]', + names: 'T', + }); + }); + + it('should handle object literal with commas in constraint', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends { a: string, b: number }', + names: 'T', + }); + }); + + it('should handle object literal with commas and a second param', () => { + const tsx = + 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends { a: string, b: number }, U extends keyof T', + names: 'T, U', + }); + }); + + it('should handle tuple type with commas in constraint', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends [string, number]', + names: 'T', + }); + }); + + it('should handle parenthesized function params with commas', () => { + const tsx = 'class __sveltets_Render void> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends (a: number, b: string) => void', + names: 'T', + }); + }); + + it('should handle nested object literals with commas', () => { + const tsx = 'class __sveltets_Render { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends { a: { x: string }, b: number }', + names: 'T', + }); + }); + + it('should handle nested arrow function in constraint', () => { + const tsx = 'class __sveltets_Render number) => boolean> { }'; + const result = extractGenerics(tsx); + assert.deepEqual(result, { + params: 'T extends (f: (x: string) => number) => boolean', + names: 'T', + }); + }); +});