forked from sveltejs/language-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) add experimal support for generics (sveltejs#1053)
By defining a type which uses the `$$Generic` type, it's now possible to definine generics. Example: // definition type T = $$Generic; type K = $$Generic<keyof T>; // interpreted as "K extends keyof T" // usage export let t: T; export let k: K; The content of a $$Generic type is moved to the render function: Its identifier is the generic name, a possibly type argument is the extends clause. During that, the seemingly unnecessary typing of props/slots/events as being optional on the render function is removed because it makes TS throw wrong errors in strict mode sveltejs#442 sveltejs#273 sveltejs/rfcs#38
- Loading branch information
1 parent
d9b6ebf
commit 1992d63
Showing
18 changed files
with
862 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
...language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-generics.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<script lang="ts"> | ||
import Generics from './generics.svelte'; | ||
</script> | ||
|
||
<!-- valid --> | ||
<Generics a={['a', 'b']} b={'anchor'} c={false} on:b={(e) => e.detail === 'str'} let:a> | ||
{a === 'str'} | ||
</Generics> | ||
|
||
<!-- invalid --> | ||
<Generics a={['a', 'b']} b={'asd'} c={''} on:b={(e) => e.detail === true} let:a> | ||
{a === true} | ||
</Generics> |
16 changes: 16 additions & 0 deletions
16
packages/language-server/test/plugins/typescript/testfiles/diagnostics/generics.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<script lang="ts"> | ||
import { createEventDispatcher } from 'svelte'; | ||
type A = $$Generic; | ||
type B = $$Generic<keyof A>; | ||
type C = $$Generic<boolean>; | ||
export let a: A[]; | ||
export let b: B; | ||
export let c: C; | ||
const dispatch = createEventDispatcher<{ b: A }>(); | ||
dispatch('b', a[0]); | ||
</script> | ||
|
||
<slot a={a[0]} /> |
206 changes: 206 additions & 0 deletions
206
packages/svelte2tsx/src/svelte2tsx/addComponentExport.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
import { pascalCase } from 'pascal-case'; | ||
import path from 'path'; | ||
import { createClassGetters } from './nodes/exportgetters'; | ||
import { createClassAccessors } from './nodes/exportaccessors'; | ||
import MagicString from 'magic-string'; | ||
import { ExportedNames } from './nodes/ExportedNames'; | ||
import { ComponentDocumentation } from './nodes/ComponentDocumentation'; | ||
import { Generics } from './nodes/Generics'; | ||
|
||
export interface AddComponentExportPara { | ||
str: MagicString; | ||
uses$$propsOr$$restProps: boolean; | ||
/** | ||
* If true, not fallback to `any` | ||
* -> all unknown events will throw a type error | ||
* */ | ||
strictEvents: boolean; | ||
isTsFile: boolean; | ||
getters: Set<string>; | ||
usesAccessors: boolean; | ||
exportedNames: ExportedNames; | ||
fileName?: string; | ||
componentDocumentation: ComponentDocumentation; | ||
mode: 'dts' | 'tsx'; | ||
generics: Generics; | ||
} | ||
|
||
/** | ||
* A component class name suffix is necessary to prevent class name clashes | ||
* like reported in https://github.com/sveltejs/language-tools/issues/294 | ||
*/ | ||
export const COMPONENT_SUFFIX = '__SvelteComponent_'; | ||
|
||
export function addComponentExport(params: AddComponentExportPara) { | ||
if (params.generics.has()) { | ||
addGenericsComponentExport(params); | ||
} else { | ||
addSimpleComponentExport(params); | ||
} | ||
} | ||
|
||
function addGenericsComponentExport({ | ||
strictEvents, | ||
isTsFile, | ||
uses$$propsOr$$restProps, | ||
exportedNames, | ||
componentDocumentation, | ||
fileName, | ||
mode, | ||
getters, | ||
usesAccessors, | ||
str, | ||
generics | ||
}: AddComponentExportPara) { | ||
const genericsDef = generics.toDefinitionString(); | ||
const genericsRef = generics.toReferencesString(); | ||
|
||
const doc = componentDocumentation.getFormatted(); | ||
const className = fileName && classNameFromFilename(fileName, mode !== 'dts'); | ||
|
||
function returnType(forPart: string) { | ||
return `ReturnType<__sveltets_Render${genericsRef}['${forPart}']>`; | ||
} | ||
|
||
let statement = ` | ||
class __sveltets_Render${genericsDef} { | ||
props() { | ||
return ${props( | ||
isTsFile, | ||
uses$$propsOr$$restProps, | ||
exportedNames, | ||
`render${genericsRef}()` | ||
)}.props; | ||
} | ||
events() { | ||
return ${events(strictEvents, `render${genericsRef}()`)}.events; | ||
} | ||
slots() { | ||
return render${genericsRef}().slots; | ||
} | ||
} | ||
`; | ||
|
||
if (mode === 'dts') { | ||
statement += | ||
`export type ${className}Props${genericsDef} = ${returnType('props')};\n` + | ||
`export type ${className}Events${genericsDef} = ${returnType('events')};\n` + | ||
`export type ${className}Slots${genericsDef} = ${returnType('slots')};\n` + | ||
`\n${doc}export default class${ | ||
className ? ` ${className}` : '' | ||
}${genericsDef} extends SvelteComponentTyped<${className}Props${genericsRef}, ${className}Events${genericsRef}, ${className}Slots${genericsRef}> {` + // eslint-disable-line max-len | ||
createClassGetters(getters) + | ||
(usesAccessors ? createClassAccessors(getters, exportedNames) : '') + | ||
'\n}'; | ||
} else { | ||
statement += | ||
`\n\n${doc}export default class${ | ||
className ? ` ${className}` : '' | ||
}${genericsDef} extends Svelte2TsxComponent<${returnType('props')}, ${returnType( | ||
'events' | ||
)}, ${returnType('slots')}> {` + | ||
createClassGetters(getters) + | ||
(usesAccessors ? createClassAccessors(getters, exportedNames) : '') + | ||
'\n}'; | ||
} | ||
|
||
str.append(statement); | ||
} | ||
|
||
function addSimpleComponentExport({ | ||
strictEvents, | ||
isTsFile, | ||
uses$$propsOr$$restProps, | ||
exportedNames, | ||
componentDocumentation, | ||
fileName, | ||
mode, | ||
getters, | ||
usesAccessors, | ||
str | ||
}: AddComponentExportPara) { | ||
const propDef = props( | ||
isTsFile, | ||
uses$$propsOr$$restProps, | ||
exportedNames, | ||
events(strictEvents, 'render()') | ||
); | ||
|
||
const doc = componentDocumentation.getFormatted(); | ||
const className = fileName && classNameFromFilename(fileName, mode !== 'dts'); | ||
|
||
let statement: string; | ||
if (mode === 'dts') { | ||
statement = | ||
`\nconst __propDef = ${propDef};\n` + | ||
`export type ${className}Props = typeof __propDef.props;\n` + | ||
`export type ${className}Events = typeof __propDef.events;\n` + | ||
`export type ${className}Slots = typeof __propDef.slots;\n` + | ||
`\n${doc}export default class${ | ||
className ? ` ${className}` : '' | ||
} extends SvelteComponentTyped<${className}Props, ${className}Events, ${className}Slots> {` + // eslint-disable-line max-len | ||
createClassGetters(getters) + | ||
(usesAccessors ? createClassAccessors(getters, exportedNames) : '') + | ||
'\n}'; | ||
} else { | ||
statement = | ||
`\n\n${doc}export default class${ | ||
className ? ` ${className}` : '' | ||
} extends createSvelte2TsxComponent(${propDef}) {` + | ||
createClassGetters(getters) + | ||
(usesAccessors ? createClassAccessors(getters, exportedNames) : '') + | ||
'\n}'; | ||
} | ||
|
||
str.append(statement); | ||
} | ||
|
||
function events(strictEvents: boolean, renderStr: string) { | ||
return strictEvents ? renderStr : `__sveltets_with_any_event(${renderStr})`; | ||
} | ||
|
||
function props( | ||
isTsFile: boolean, | ||
uses$$propsOr$$restProps: boolean, | ||
exportedNames: ExportedNames, | ||
renderStr: string | ||
) { | ||
if (isTsFile) { | ||
return uses$$propsOr$$restProps ? `__sveltets_with_any(${renderStr})` : renderStr; | ||
} else { | ||
const optionalProps = exportedNames.createOptionalPropsArray(); | ||
const partial = `__sveltets_partial${uses$$propsOr$$restProps ? '_with_any' : ''}`; | ||
return optionalProps.length > 0 | ||
? `${partial}([${optionalProps.join(',')}], ${renderStr})` | ||
: `${partial}(${renderStr})`; | ||
} | ||
} | ||
|
||
/** | ||
* Returns a Svelte-compatible component name from a filename. Svelte | ||
* components must use capitalized tags, so we try to transform the filename. | ||
* | ||
* https://svelte.dev/docs#Tags | ||
*/ | ||
function classNameFromFilename(filename: string, appendSuffix: boolean): string | undefined { | ||
try { | ||
const withoutExtensions = path.parse(filename).name?.split('.')[0]; | ||
const withoutInvalidCharacters = withoutExtensions | ||
.split('') | ||
// Although "-" is invalid, we leave it in, pascal-case-handling will throw it out later | ||
.filter((char) => /[A-Za-z$_\d-]/.test(char)) | ||
.join(''); | ||
const firstValidCharIdx = withoutInvalidCharacters | ||
.split('') | ||
// Although _ and $ are valid first characters for classes, they are invalid first characters | ||
// for tag names. For a better import autocompletion experience, we therefore throw them out. | ||
.findIndex((char) => /[A-Za-z]/.test(char)); | ||
const withoutLeadingInvalidCharacters = withoutInvalidCharacters.substr(firstValidCharIdx); | ||
const inPascalCase = pascalCase(withoutLeadingInvalidCharacters); | ||
const finalName = firstValidCharIdx === -1 ? `A${inPascalCase}` : inPascalCase; | ||
return `${finalName}${appendSuffix ? COMPONENT_SUFFIX : ''}`; | ||
} catch (error) { | ||
console.warn(`Failed to create a name for the component class from filename ${filename}`); | ||
return undefined; | ||
} | ||
} |
Oops, something went wrong.