From 64d7b773adfb136b0f4b59e31d03eea7ada1d508 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:25:13 +0100 Subject: [PATCH] feat: experimental support for Svelte 5 (#2198) * handle $props() * handle .svelte.ts files * remove $ from wordpattern to get proper runes autocompletion; adjust store autocompletion as a consequence * snippets * use walk from estree-walker * more ts-ignore for types that don't exist anymore * handle the cjs case for 5, too * fix * mark test as skipped for now * lint * load Svelte 5 compiler if applicable * bump svelte-preprocess --- packages/language-server/package.json | 2 +- packages/language-server/src/importPackage.ts | 2 +- .../src/lib/documents/configLoader.ts | 2 + .../src/plugins/svelte/SvelteDocument.ts | 6 +- .../src/plugins/svelte/features/SvelteTags.ts | 18 ++- .../features/getCodeActions/getQuickfixes.ts | 1 + .../plugins/svelte/features/getCompletions.ts | 9 +- .../plugins/svelte/features/getDiagnostics.ts | 1 + .../plugins/svelte/features/getHoverInfo.ts | 3 + .../plugins/typescript/DocumentSnapshot.ts | 3 + .../typescript/features/CompletionProvider.ts | 113 +++++++++++------- .../src/plugins/typescript/module-loader.ts | 3 +- .../src/plugins/typescript/service.ts | 22 +++- .../plugins/typescript/svelte-ast-utils.ts | 3 +- .../src/plugins/typescript/svelte-sys.ts | 32 ++++- .../plugins/svelte/SvelteDocument.test.ts | 3 +- .../svelte/features/getCompletions.test.ts | 11 +- .../features/CompletionProvider.test.ts | 16 ++- .../fixtures/svelte-ts-file/expectedv2.json | 19 +++ .../fixtures/svelte-ts-file/foo.svelte.ts | 1 + .../fixtures/svelte-ts-file/input.svelte | 4 + .../features/diagnostics/index.test.ts | 2 +- .../plugins/typescript/module-loader.test.ts | 2 + packages/svelte-check/package.json | 4 +- packages/svelte-vscode/snippets/svelte.json | 4 + packages/svelte-vscode/src/extension.ts | 2 +- .../syntaxes/svelte.tmLanguage.src.yaml | 4 +- packages/svelte2tsx/index.d.ts | 4 + packages/svelte2tsx/package.json | 2 +- .../svelte2tsx/src/htmlxtojsx_v2/index.ts | 21 +++- .../src/htmlxtojsx_v2/nodes/RenderTag.ts | 23 ++++ .../src/htmlxtojsx_v2/nodes/SnippetBlock.ts | 68 +++++++++++ .../src/htmlxtojsx_v2/nodes/Text.ts | 1 + .../src/htmlxtojsx_v2/utils/node-utils.ts | 12 +- packages/svelte2tsx/src/interfaces.ts | 1 + packages/svelte2tsx/src/svelte2tsx/index.ts | 14 ++- .../src/svelte2tsx/nodes/ExportedNames.ts | 62 +++++++++- .../nodes/handleScopeAndResolveForSlot.ts | 1 + .../svelte2tsx/src/svelte2tsx/nodes/slot.ts | 1 + packages/svelte2tsx/src/utils/htmlxparser.ts | 9 +- packages/svelte2tsx/test/helpers.ts | 2 + packages/svelte2tsx/test/htmlx2jsx/index.ts | 3 +- .../samples/snippet.skip/expectedv2.js | 13 ++ .../samples/snippet.skip/input.svelte | 13 ++ packages/svelte2tsx/test/htmlxparser/index.ts | 3 +- .../svelte2tsx/samples/runes/expectedv2.ts | 16 +++ .../svelte2tsx/samples/runes/input.svelte | 8 ++ .../samples/ts-runes-generics/expectedv2.ts | 15 +++ .../samples/ts-runes-generics/input.svelte | 7 ++ .../svelte2tsx/samples/ts-runes/expectedv2.ts | 28 +++++ .../svelte2tsx/samples/ts-runes/input.svelte | 7 ++ pnpm-lock.yaml | 70 ++--------- 52 files changed, 539 insertions(+), 157 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/expectedv2.json create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/foo.svelte.ts create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/input.svelte create mode 100644 packages/svelte2tsx/src/htmlxtojsx_v2/nodes/RenderTag.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/runes/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/runes/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/input.svelte diff --git a/packages/language-server/package.json b/packages/language-server/package.json index e8a3c35a5..59b3b0d58 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -55,7 +55,7 @@ "prettier": "~2.8.8", "prettier-plugin-svelte": "~2.10.1", "svelte": "^3.57.0", - "svelte-preprocess": "~5.0.4", + "svelte-preprocess": "~5.1.0", "svelte2tsx": "workspace:~", "typescript": "^5.2.2", "vscode-css-languageservice": "~6.2.0", diff --git a/packages/language-server/src/importPackage.ts b/packages/language-server/src/importPackage.ts index defa2f1a5..b51c23a3b 100644 --- a/packages/language-server/src/importPackage.ts +++ b/packages/language-server/src/importPackage.ts @@ -58,7 +58,7 @@ export function importSvelte(fromPath: string): typeof svelte { const pkg = getPackageInfo('svelte', fromPath); const main = resolve(pkg.path, 'compiler'); Logger.debug('Using Svelte v' + pkg.version.full, 'from', main); - return dynamicRequire(main + (pkg.version.major === 4 ? '.cjs' : '')); + return dynamicRequire(main + (pkg.version.major >= 4 ? '.cjs' : '')); } export function importSveltePreprocess(fromPath: string): typeof sveltePreprocess { diff --git a/packages/language-server/src/lib/documents/configLoader.ts b/packages/language-server/src/lib/documents/configLoader.ts index 6ffd0a9ea..2cea65b7e 100644 --- a/packages/language-server/src/lib/documents/configLoader.ts +++ b/packages/language-server/src/lib/documents/configLoader.ts @@ -1,5 +1,7 @@ import { Logger } from '../../logger'; +// @ts-ignore import { CompileOptions } from 'svelte/types/compiler/interfaces'; +// @ts-ignore import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; import { importSveltePreprocess } from '../../importPackage'; import _glob from 'fast-glob'; diff --git a/packages/language-server/src/plugins/svelte/SvelteDocument.ts b/packages/language-server/src/plugins/svelte/SvelteDocument.ts index 324b9d96f..b3100b430 100644 --- a/packages/language-server/src/plugins/svelte/SvelteDocument.ts +++ b/packages/language-server/src/plugins/svelte/SvelteDocument.ts @@ -1,6 +1,8 @@ import { TraceMap } from '@jridgewell/trace-mapping'; import type { compile } from 'svelte/compiler'; +// @ts-ignore import { CompileOptions } from 'svelte/types/compiler/interfaces'; +// @ts-ignore import { PreprocessorGroup, Processed } from 'svelte/types/compiler/preprocess'; import { Position } from 'vscode-languageserver'; import { getPackageInfo, importSvelte } from '../../importPackage'; @@ -367,7 +369,7 @@ export class SvelteFragmentMapper implements PositionMapper { */ function wrapPreprocessors(preprocessors: PreprocessorGroup | PreprocessorGroup[] = []) { preprocessors = Array.isArray(preprocessors) ? preprocessors : [preprocessors]; - return preprocessors.map((preprocessor) => { + return preprocessors.map((preprocessor: any) => { const wrappedPreprocessor: PreprocessorGroup = { markup: preprocessor.markup }; if (preprocessor.script) { @@ -404,7 +406,7 @@ async function transpile( const processedScripts: Processed[] = []; const processedStyles: Processed[] = []; - const wrappedPreprocessors = preprocessors.map((preprocessor) => { + const wrappedPreprocessors = preprocessors.map((preprocessor: any) => { const wrappedPreprocessor: PreprocessorGroup = { markup: preprocessor.markup }; if (preprocessor.script) { diff --git a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts b/packages/language-server/src/plugins/svelte/features/SvelteTags.ts index 6994c217a..12d53a1b5 100644 --- a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts +++ b/packages/language-server/src/plugins/svelte/features/SvelteTags.ts @@ -3,12 +3,12 @@ import { SvelteDocument } from '../SvelteDocument'; /** * Special svelte syntax tags that do template logic. */ -export type SvelteLogicTag = 'each' | 'if' | 'await' | 'key'; +export type SvelteLogicTag = 'each' | 'if' | 'await' | 'key' | 'snippet'; /** * Special svelte syntax tags. */ -export type SvelteTag = SvelteLogicTag | 'html' | 'debug' | 'const'; +export type SvelteTag = SvelteLogicTag | 'html' | 'debug' | 'const' | 'render'; /** * For each tag, a documentation in markdown format. @@ -52,6 +52,13 @@ When used around components, this will cause them to be reinstantiated and reini \`{#key expression}...{/key}\`\\ \\ https://svelte.dev/docs#template-syntax-key +`, + snippet: `\`{#snippet identifier(parameter)}...{/snippet}\`\\ +Snippets allow you to create reusable UI blocks you can render with the {@render ...} tag. +They also function as slot props for components. +`, + render: `\`{@render ...}\`\\ +Renders a snippet with the given parameters. `, html: `\`{@html ...}\`\\ @@ -80,9 +87,11 @@ It accepts a comma-separated list of variable names (not arbitrary expressions). https://svelte.dev/docs#template-syntax-debug `, const: `\`{@const ...}\`\\ -TODO +Defines a local constant}\\ #### Usage: \`{@const a = b + c}\`\\ +\\ +https://svelte.dev/docs/special-tags#const ` }; @@ -102,7 +111,8 @@ export function getLatestOpeningTag( idxOfLastOpeningTag(content, 'each'), idxOfLastOpeningTag(content, 'if'), idxOfLastOpeningTag(content, 'await'), - idxOfLastOpeningTag(content, 'key') + idxOfLastOpeningTag(content, 'key'), + idxOfLastOpeningTag(content, 'snippet') ]; const lastIdx = lastIdxs.sort((i1, i2) => i2.lastIdx - i1.lastIdx); return lastIdx[0].lastIdx === -1 ? null : lastIdx[0].tag; diff --git a/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts b/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts index 78c4539cb..917325c88 100644 --- a/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts +++ b/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts @@ -1,5 +1,6 @@ import { walk } from 'estree-walker'; import { EOL } from 'os'; +// @ts-ignore import { TemplateNode } from 'svelte/types/compiler/interfaces'; import { CodeAction, diff --git a/packages/language-server/src/plugins/svelte/features/getCompletions.ts b/packages/language-server/src/plugins/svelte/features/getCompletions.ts index 606b36c0f..eefe5c3f7 100644 --- a/packages/language-server/src/plugins/svelte/features/getCompletions.ts +++ b/packages/language-server/src/plugins/svelte/features/getCompletions.ts @@ -125,7 +125,8 @@ function getCompletionsWithRegardToTriggerCharacter( return createCompletionItems([ { tag: 'html', label: 'html' }, { tag: 'debug', label: 'debug' }, - { tag: 'const', label: 'const' } + { tag: 'const', label: 'const' }, + { tag: 'render', label: 'render' } ]); } @@ -143,7 +144,8 @@ function getCompletionsWithRegardToTriggerCharacter( label: 'await then', insertText: 'await $1 then $2}\n\t$3\n{/await' }, - { tag: 'key', label: 'key', insertText: 'key $1}\n\t$2\n{/key' } + { tag: 'key', label: 'key', insertText: 'key $1}\n\t$2\n{/key' }, + { tag: 'snippet', label: 'snippet', insertText: 'snippet $1($2)}\n\t$3\n{/snippet' } ]); } @@ -207,6 +209,7 @@ function showCompletionWithRegardsToOpenedTags( ifOpen: CompletionList; awaitOpen: CompletionList; keyOpen?: CompletionList; + snippetOpen?: CompletionList; }, svelteDoc: SvelteDocument, offset: number @@ -220,6 +223,8 @@ function showCompletionWithRegardsToOpenedTags( return on.awaitOpen; case 'key': return on?.keyOpen ?? null; + case 'snippet': + return on.snippetOpen ?? null; default: return null; } diff --git a/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts b/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts index 2491d8b3a..c2125cb45 100644 --- a/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts +++ b/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts @@ -1,3 +1,4 @@ +// @ts-ignore import { Warning } from 'svelte/types/compiler/interfaces'; import { Diagnostic, DiagnosticSeverity, Position, Range } from 'vscode-languageserver'; import { diff --git a/packages/language-server/src/plugins/svelte/features/getHoverInfo.ts b/packages/language-server/src/plugins/svelte/features/getHoverInfo.ts index 4bd1a69e1..86d90577b 100644 --- a/packages/language-server/src/plugins/svelte/features/getHoverInfo.ts +++ b/packages/language-server/src/plugins/svelte/features/getHoverInfo.ts @@ -109,10 +109,13 @@ const tagPossibilities: Array<{ tag: SvelteTag | ':else'; values: string[] }> = { tag: 'await' as const, values: ['#await', '/await', ':then', ':catch'] }, // key { tag: 'key' as const, values: ['#key', '/key'] }, + // snippet + { tag: 'snippet' as const, values: ['#snippet', '/snippet'] }, // @ { tag: 'html' as const, values: ['@html'] }, { tag: 'debug' as const, values: ['@debug'] }, { tag: 'const' as const, values: ['@const'] }, + { tag: 'render' as const, values: ['@render'] }, // this tag has multiple possibilities { tag: ':else' as const, values: [':else'] } ]; diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts index 72895882a..5c1552c90 100644 --- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts +++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts @@ -1,4 +1,5 @@ import { EncodedSourceMap, TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; +// @ts-ignore import { TemplateNode } from 'svelte/types/compiler/interfaces'; import { svelte2tsx, IExportedNames, internalHelpers } from 'svelte2tsx'; import ts from 'typescript'; @@ -66,6 +67,7 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot, DocumentMapper { * Options that apply to svelte files. */ export interface SvelteSnapshotOptions { + parse: typeof import('svelte/compiler').parse | undefined; transformOnTemplateError: boolean; typingsNamespace: string; } @@ -196,6 +198,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions try { const tsx = svelte2tsx(text, { + parse: options.parse, filename: document.getFilePath() ?? undefined, isTsFile: scriptKind === ts.ScriptKind.TS, mode: 'ts', diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 8f9724f43..96db365f9 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -268,6 +268,31 @@ export class CompletionsProviderImpl implements CompletionsProvider { + if (isValidCompletion(entry)) { + let completion = this.toCompletionItem( + tsDoc, + entry, + fileUrl, + position, + isCompletionInTag, + addCommitCharacters, + asStore, + existingImports + ); + if (completion) { + completionItems.push( + this.fixTextEditRange( + wordRangeStartPosition, + mapCompletionItemToOriginal(tsDoc, completion) + ) + ); + } + } + }; + // If completion is about a store which is not imported yet, do another // completion request at the beginning of the file to get all global // import completions and then filter them down to likely matches. @@ -275,41 +300,31 @@ export class CompletionsProviderImpl implements CompletionsProvider entry.source && entry.name.startsWith(storeName) - ) || []; - completions.push(...storeImportCompletions); + const pos = (tsDoc.scriptInfo || tsDoc.moduleScriptInfo)?.endPos ?? { + line: 0, + character: 0 + }; + const virtualOffset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(pos)); + const storeCompletions = lang.getCompletionsAtPosition( + filePath, + virtualOffset, + { + ...userPreferences, + triggerCharacter: validTriggerCharacter + }, + formatSettings + ); + for (const entry of storeCompletions?.entries || []) { + if (entry.name.startsWith(storeName)) { + addCompletion(entry, true); + } + } } } - const completionItems = completions - .filter(isValidCompletion(document, position, !!tsDoc.parserError)) - .map((comp) => - this.toCompletionItem( - tsDoc, - comp, - fileUrl, - position, - isCompletionInTag, - addCommitCharacters, - existingImports - ) - ) - .filter(isNotNullOrUndefined) - .map((comp) => mapCompletionItemToOriginal(tsDoc, comp)) - .map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp)) - .concat(eventAndSlotLetCompletions); + for (const entry of completions) { + addCompletion(entry, false); + } // Add ./$types imports for SvelteKit since TypeScript is bad at it if (basename(filePath).startsWith('+')) { @@ -455,6 +470,7 @@ export class CompletionsProviderImpl implements CompletionsProvider ): AppCompletionItem | null { const completionLabelAndInsert = this.getCompletionLabelAndInsert(snapshot, comp); @@ -462,7 +478,8 @@ export class CompletionsProviderImpl implements CompletionsProvider= 5 + ? importSvelte(tsconfigPath || workspacePath) + : undefined; const isSvelte3 = sveltePackageInfo.version.major === 3; const svelteHtmlDeclaration = isSvelte3 @@ -291,6 +297,7 @@ async function createLanguageService( let languageService = ts.createLanguageService(host); const transformationConfig: SvelteSnapshotOptions = { + parse: svelteCompiler?.parse, transformOnTemplateError: docContext.transformOnTemplateError, typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' }; @@ -390,9 +397,9 @@ async function createLanguageService( * Otherwise, deleteUnresolvedResolutionsFromCache won't be called when the file is created again */ function getSnapshotIfExists(fileName: string): DocumentSnapshot | undefined { - fileName = ensureRealSvelteFilePath(fileName); + const svelteFileName = ensureRealSvelteFilePath(fileName); - let doc = snapshotManager.get(fileName); + let doc = snapshotManager.get(fileName) ?? snapshotManager.get(svelteFileName); if (doc) { return doc; } @@ -401,13 +408,16 @@ async function createLanguageService( return undefined; } - return createSnapshot(fileName, doc); + return createSnapshot( + svelteModuleLoader.svelteFileExists(fileName) ? svelteFileName : fileName, + doc + ); } function getSnapshot(fileName: string): DocumentSnapshot { - fileName = ensureRealSvelteFilePath(fileName); + const svelteFileName = ensureRealSvelteFilePath(fileName); - let doc = snapshotManager.get(fileName); + let doc = snapshotManager.get(fileName) ?? snapshotManager.get(svelteFileName); if (doc) { return doc; } diff --git a/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts b/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts index 359251f29..df51edb26 100644 --- a/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts +++ b/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts @@ -1,5 +1,6 @@ import { Node } from 'estree'; -import { walk } from 'svelte/compiler'; +import { walk } from 'estree-walker'; +// @ts-ignore import { TemplateNode } from 'svelte/types/compiler/interfaces'; export interface SvelteNode { diff --git a/packages/language-server/src/plugins/typescript/svelte-sys.ts b/packages/language-server/src/plugins/typescript/svelte-sys.ts index 07548d7f9..4fb3c205c 100644 --- a/packages/language-server/src/plugins/typescript/svelte-sys.ts +++ b/packages/language-server/src/plugins/typescript/svelte-sys.ts @@ -8,17 +8,35 @@ import { FileMap } from '../../lib/documents/fileCollection'; export function createSvelteSys(tsSystem: ts.System) { const fileExistsCache = new FileMap(); - const svelteSys: ts.System & { deleteFromCache: (path: string) => void } = { + function svelteFileExists(path: string) { + if (isVirtualSvelteFilePath(path)) { + const sveltePath = toRealSvelteFilePath(path); + const sveltePathExists = + fileExistsCache.get(sveltePath) ?? tsSystem.fileExists(sveltePath); + fileExistsCache.set(sveltePath, sveltePathExists); + return sveltePathExists; + } else { + return false; + } + } + + const svelteSys: ts.System & { + deleteFromCache: (path: string) => void; + svelteFileExists: (path: string) => boolean; + } = { ...tsSystem, + svelteFileExists, fileExists(path: string) { - path = ensureRealSvelteFilePath(path); - const exists = fileExistsCache.get(path) ?? tsSystem.fileExists(path); + // We need to check both .svelte and .svelte.ts/js because that's how Svelte 5 will likely mark files with runes in them + const sveltePathExists = svelteFileExists(path); + const exists = + sveltePathExists || (fileExistsCache.get(path) ?? tsSystem.fileExists(path)); fileExistsCache.set(path, exists); return exists; }, readFile(path: string) { // No getSnapshot here, because TS will very rarely call this and only for files that are not in the project - return tsSystem.readFile(ensureRealSvelteFilePath(path)); + return tsSystem.readFile(svelteFileExists(path) ? toRealSvelteFilePath(path) : path); }, readDirectory(path, extensions, exclude, include, depth) { const extensionsWithSvelte = extensions ? [...extensions, '.svelte'] : undefined; @@ -26,18 +44,22 @@ export function createSvelteSys(tsSystem: ts.System) { return tsSystem.readDirectory(path, extensionsWithSvelte, exclude, include, depth); }, deleteFile(path) { + // assumption: never a foo.svelte.ts file next to a foo.svelte file fileExistsCache.delete(ensureRealSvelteFilePath(path)); + fileExistsCache.delete(path); return tsSystem.deleteFile?.(path); }, deleteFromCache(path) { + // assumption: never a foo.svelte.ts file next to a foo.svelte file fileExistsCache.delete(ensureRealSvelteFilePath(path)); + fileExistsCache.delete(path); } }; if (tsSystem.realpath) { const realpath = tsSystem.realpath; svelteSys.realpath = function (path) { - if (isVirtualSvelteFilePath(path)) { + if (svelteFileExists(path)) { return realpath(toRealSvelteFilePath(path)) + '.ts'; } return realpath(path); diff --git a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts index 01ab5133a..4a907948c 100644 --- a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts +++ b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts @@ -8,6 +8,7 @@ import { ITranspiledSvelteDocument } from '../../../src/plugins/svelte/SvelteDocument'; import { configLoader, SvelteConfig } from '../../../src/lib/documents/configLoader'; +// @ts-ignore import { Preprocessor } from 'svelte/types/compiler/preprocess'; describe('Svelte Document', () => { @@ -97,6 +98,7 @@ describe('Svelte Document', () => { sinon .stub(importPackage, 'getPackageInfo') .returns({ path: '', version: { full: '', major: 3, minor: 31, patch: 0 } }); + // @ts-ignore sinon.stub(importPackage, 'importSvelte').returns({ preprocess: (text, preprocessor) => { preprocessor = Array.isArray(preprocessor) ? preprocessor : [preprocessor]; @@ -108,7 +110,6 @@ describe('Svelte Document', () => { map: null }); }, - walk: null, VERSION: '', compile: null, parse: null diff --git a/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts b/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts index c4964e2a0..d86b1cfd1 100644 --- a/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts +++ b/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts @@ -52,11 +52,18 @@ describe('SveltePlugin#getCompletions', () => { }); it('should return completions for #', () => { - expectCompletionsFor('{#').toEqual(['if', 'each', 'await :then', 'await then', 'key']); + expectCompletionsFor('{#').toEqual([ + 'if', + 'each', + 'await :then', + 'await then', + 'key', + 'snippet' + ]); }); it('should return completions for @', () => { - expectCompletionsFor('{@').toEqual(['html', 'debug', 'const']); + expectCompletionsFor('{@').toEqual(['html', 'debug', 'const', 'render']); }); describe('should return no completions for :', () => { diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index d9e56a39a..ef22b06ee 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -1466,14 +1466,15 @@ describe('CompletionProviderImpl', function () { } ); - const item = completions?.items.find((item) => item.label === 'store'); + const item = completions?.items.find((item) => item.label === '$store'); + assert.ok(item); assert.equal(item?.data?.source?.endsWith('completions/to-import'), true); - delete item?.data; + const { data, ...itemWithoutData } = item; - assert.deepStrictEqual(item, { - label: 'store', + assert.deepStrictEqual(itemWithoutData, { + label: '$store', kind: CompletionItemKind.Constant, sortText: '16', preselect: undefined, @@ -1483,6 +1484,13 @@ describe('CompletionProviderImpl', function () { textEdit: undefined, labelDetails: undefined }); + + const { detail } = await completionProvider.resolveCompletion(document, item); + + assert.deepStrictEqual( + detail, + 'Add import from "./to-import"\n\nconst store: Writable' + ); }); // Hacky, but it works. Needed due to testing both new and old transformation diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/expectedv2.json new file mode 100644 index 000000000..37e4d3fd7 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/expectedv2.json @@ -0,0 +1,19 @@ +[ + { + "code": 2305, + "message": "Module '\"./foo.svelte\"' has no exported member 'bar'.", + "range": { + "end": { + "character": 14, + "line": 1 + }, + "start": { + "character": 11, + "line": 1 + } + }, + "severity": 1, + "source": "ts", + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/foo.svelte.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/foo.svelte.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/foo.svelte.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/input.svelte new file mode 100644 index 000000000..ce1eeb216 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-ts-file/input.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/index.test.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/index.test.ts index 378e6dcfe..6d8ae0bcd 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/index.test.ts +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/index.test.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { readFileSync, existsSync, readdirSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import ts from 'typescript'; import { Document, DocumentManager } from '../../../../../src/lib/documents'; diff --git a/packages/language-server/test/plugins/typescript/module-loader.test.ts b/packages/language-server/test/plugins/typescript/module-loader.test.ts index 1b3adbbac..d08e42682 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -139,6 +139,7 @@ describe('createSvelteModuleLoader', () => { }; const { resolveStub, svelteSys, moduleResolver, compilerOptions, getSvelteSnapshotStub } = setup(resolvedModule); + resolveStub.onFirstCall().returns({ resolvedModule: undefined }); const result = moduleResolver.resolveModuleNames( ['./svelte.svelte'], 'C:/somerepo/somefile.svelte', @@ -173,6 +174,7 @@ describe('createSvelteModuleLoader', () => { }; const { resolveStub, svelteSys, moduleResolver, compilerOptions, getSvelteSnapshotStub } = setup(resolvedModule); + resolveStub.onFirstCall().returns({ resolvedModule: undefined }); const result = moduleResolver.resolveModuleNames( ['/@/svelte.svelte'], 'C:/somerepo/somefile.svelte', diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 47511bb2a..3e239ef07 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -29,11 +29,11 @@ "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^5.0.4", + "svelte-preprocess": "^5.1.0", "typescript": "^5.0.3" }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" }, "scripts": { "build": "rollup -c && node ./dist/src/index.js --workspace ./test --tsconfig ./tsconfig.json", diff --git a/packages/svelte-vscode/snippets/svelte.json b/packages/svelte-vscode/snippets/svelte.json index 502e1591d..ab583903a 100644 --- a/packages/svelte-vscode/snippets/svelte.json +++ b/packages/svelte-vscode/snippets/svelte.json @@ -18,5 +18,9 @@ "key": { "body": ["{#key ${1:key}}", "\t$TM_SELECTED_TEXT$0", "{/key}"], "description": "key block" + }, + "snippet": { + "body": ["{#snippet ${1:snippet}(${2:parameter})}", "\t$TM_SELECTED_TEXT$0", "{/snippet}"], + "description": "snippet block" } } diff --git a/packages/svelte-vscode/src/extension.ts b/packages/svelte-vscode/src/extension.ts index 986cc264c..dcea00d3b 100644 --- a/packages/svelte-vscode/src/extension.ts +++ b/packages/svelte-vscode/src/extension.ts @@ -268,7 +268,7 @@ export function activateSvelteLanguageServer(context: ExtensionContext) { // any of the following: `~!@$^&*()=+[{]}\|;:'",.<>/ // wordPattern: - /(-?\d*\.\d\w*)|([^\`\~\!\@\$\#\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, + /(-?\d*\.\d\w*)|([^\`\~\!\@\#\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, onEnterRules: [ { // Matches an opening tag that: diff --git a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml index b40ff7f06..4c5c2bfb1 100644 --- a/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml +++ b/packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml @@ -224,7 +224,9 @@ repository: { match: if|else\s+if|else, name: keyword.control.conditional.svelte }, { match: each|key, name: keyword.control.svelte }, { match: await|then|catch, name: keyword.control.flow.svelte }, + { match: snippet, name: keyword.control.svelte }, { match: html, name: keyword.other.svelte }, + { match: render, name: keyword.other.svelte }, { match: debug, name: keyword.other.debugger.svelte }, { match: const, name: storage.type.svelte }]} @@ -232,7 +234,7 @@ repository: special-tags-modes: patterns: # Expressions or simple values. - - begin: (?<=(if|key|then|catch|html).*?)\G + - begin: (?<=(if|key|then|catch|snippet|html|render).*?)\G end: (?=}) name: meta.embedded.expression.svelte source.ts patterns: [ include: source.ts ] diff --git a/packages/svelte2tsx/index.d.ts b/packages/svelte2tsx/index.d.ts index 11e5becad..d65765175 100644 --- a/packages/svelte2tsx/index.d.ts +++ b/packages/svelte2tsx/index.d.ts @@ -75,6 +75,10 @@ export function svelte2tsx( * see https://svelte.dev/docs#svelte_compile for more info */ accessors?: boolean + /** + * The Svelte parser to use. Defaults to the one bundled with `svelte2tsx`. + */ + parse?: typeof import('svelte/compiler').parse; } ): SvelteCompiledToTsx diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index e602be856..80d94cc59 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -43,7 +43,7 @@ "typescript": "^5.2.2" }, "peerDependencies": { - "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0", + "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" }, "scripts": { diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index 3756a9454..38673b556 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -1,5 +1,6 @@ import MagicString from 'magic-string'; -import { walk } from 'svelte/compiler'; +import { walk } from 'estree-walker'; +// @ts-ignore import { TemplateNode, Text } from 'svelte/types/compiler/interfaces'; import { Attribute, BaseNode, BaseDirective, StyleDirective, ConstTag } from '../interfaces'; import { parseHtmlx } from '../utils/htmlxparser'; @@ -25,6 +26,8 @@ import { handleSpread } from './nodes/Spread'; import { handleStyleDirective } from './nodes/StyleDirective'; import { handleText } from './nodes/Text'; import { handleTransitionDirective } from './nodes/Transition'; +import { handleSnippet } from './nodes/SnippetBlock'; +import { handleRenderTag } from './nodes/RenderTag'; type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void; @@ -74,6 +77,16 @@ export function convertHtmlxToJsx( case 'KeyBlock': handleKey(str, node); break; + case 'SnippetBlock': + handleSnippet( + str, + node, + element instanceof InlineComponent && + estreeTypedParent.type === 'InlineComponent' + ? element + : undefined + ); + break; case 'MustacheTag': handleMustacheTag(str, node, parent); break; @@ -86,6 +99,9 @@ export function convertHtmlxToJsx( case 'ConstTag': handleConstTag(str, node as ConstTag); break; + case 'RenderTag': + handleRenderTag(str, node); + break; case 'InlineComponent': if (element) { element.child = new InlineComponent(str, node, element); @@ -220,13 +236,14 @@ export function convertHtmlxToJsx( */ export function htmlx2jsx( htmlx: string, + parse: typeof import('svelte/compiler').parse, options?: { emitOnTemplateError?: boolean; preserveAttributeCase: boolean; typingsNamespace: string; } ) { - const ast = parseHtmlx(htmlx, { ...options }).htmlxAst; + const ast = parseHtmlx(htmlx, parse, { ...options }).htmlxAst; const str = new MagicString(htmlx); convertHtmlxToJsx(str, ast, null, null, options); diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/RenderTag.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/RenderTag.ts new file mode 100644 index 000000000..a1652826d --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/RenderTag.ts @@ -0,0 +1,23 @@ +import MagicString from 'magic-string'; +import { withTrailingPropertyAccess } from '../utils/node-utils'; +import { BaseNode } from '../../interfaces'; + +/** + * `{@render foo(x)}` --> `;foo(x);` + */ +export function handleRenderTag(str: MagicString, renderTag: BaseNode): void { + str.overwrite(renderTag.start, renderTag.expression.start, ';', { contentOnly: true }); + if (renderTag.argument) { + str.overwrite( + withTrailingPropertyAccess(str.original, renderTag.argument.end), + renderTag.end, + ');' + ); + } else { + str.overwrite( + withTrailingPropertyAccess(str.original, renderTag.expression.end), + renderTag.end, + '();' + ); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts new file mode 100644 index 000000000..45def4455 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts @@ -0,0 +1,68 @@ +import MagicString from 'magic-string'; +import { BaseNode } from '../../interfaces'; +import { transform, TransformationArray } from '../utils/node-utils'; +import { InlineComponent } from './InlineComponent'; + +/** + * Transform #snippet into a function + * + * ```html + * {#snippet foo(bar)} + * .. + * {/snippet} + * ``` + * --> if standalone: + * ```ts + * const foo = (bar) => { + * .. + * } + * ``` + * --> if slot prop: + * ```ts + * foo: (bar) => { + * .. + * } + * ``` + */ +export function handleSnippet( + str: MagicString, + snippetBlock: BaseNode, + element?: InlineComponent +): void { + const endSnippet = str.original.lastIndexOf('{', snippetBlock.end - 1); + str.overwrite(endSnippet, snippetBlock.end, '}', { + contentOnly: true + }); + + const startEnd = + str.original.indexOf('}', snippetBlock.context?.end || snippetBlock.expression.end) + 1; + + if (element !== undefined) { + str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true }); + const transforms: TransformationArray = ['(']; + if (snippetBlock.context) { + transforms.push([snippetBlock.context.start, snippetBlock.context.end]); + str.overwrite(snippetBlock.expression.end, snippetBlock.context.start, '', { + contentOnly: true + }); + str.overwrite(snippetBlock.context.end, startEnd, '', { contentOnly: true }); + } else { + str.overwrite(snippetBlock.expression.end, startEnd, '', { contentOnly: true }); + } + transforms.push(') => {'); + transforms.push([startEnd, snippetBlock.end]); + element.addProp([[snippetBlock.expression.start, snippetBlock.expression.end]], transforms); + } else { + const transforms: TransformationArray = [ + 'const ', + [snippetBlock.expression.start, snippetBlock.expression.end], + ' = (' + ]; + if (snippetBlock.context) { + transforms.push([snippetBlock.context.start, snippetBlock.context.end]); + } + transforms.push(') => {'); + + transform(str, snippetBlock.start, startEnd, startEnd, transforms); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Text.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Text.ts index e98c3a0a2..3f09af731 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Text.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Text.ts @@ -1,4 +1,5 @@ import MagicString from 'magic-string'; +// @ts-ignore import { Text } from 'svelte/types/compiler/interfaces'; import { BaseNode } from '../../interfaces'; diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts index 08079505f..bef382f9b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts @@ -87,12 +87,20 @@ export function transform( if (deletePos !== moves.length && removeStart > deleteDest) { str.move(removeStart, transformation[0], end); } - // Use one space because of hover etc: This will make map deleted characters to the whitespace - str.overwrite(removeStart, transformation[0], ' ', { contentOnly: true }); + if (transformation[0] < end) { + // Use one space because of hover etc: This will make map deleted characters to the whitespace + str.overwrite(removeStart, transformation[0], ' ', { contentOnly: true }); + } } removeStart = transformation[1]; } + if (removeStart > end) { + // Reset the end to the last transformation before the end if there were transformations after the end + // so we still delete the correct range afterwards + removeStart = moves.find((m) => m[1] < end)?.[1] ?? end; + } + if (removeStart < end) { // Completely delete the first character afterwards. This makes the mapping more correct, // so that autocompletion triggered on the last character works correctly. diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts index 3777fd6f0..ebdfe4d93 100644 --- a/packages/svelte2tsx/src/interfaces.ts +++ b/packages/svelte2tsx/src/interfaces.ts @@ -1,4 +1,5 @@ import { ArrayPattern, Identifier, ObjectPattern, Node } from 'estree'; +// @ts-ignore import { DirectiveType, TemplateNode } from 'svelte/types/compiler/interfaces'; export interface NodeRange { diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 7e26c5f0f..1b9f8f546 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -21,8 +21,10 @@ import { ScopeStack } from './utils/Scope'; import { Generics } from './nodes/Generics'; import { addComponentExport } from './addComponentExport'; import { createRenderFunction } from './createRenderFunction'; +// @ts-ignore import { TemplateNode } from 'svelte/types/compiler/interfaces'; import path from 'path'; +import { parse } from 'svelte/compiler'; type TemplateProcessResult = { /** @@ -44,7 +46,8 @@ type TemplateProcessResult = { function processSvelteTemplate( str: MagicString, - options?: { + parse: typeof import('svelte/compiler').parse, + options: { emitOnTemplateError?: boolean; namespace?: string; accessors?: boolean; @@ -52,9 +55,7 @@ function processSvelteTemplate( typingsNamespace?: string; } ): TemplateProcessResult { - const { htmlxAst, tags } = parseHtmlx(str.original, { - ...options - }); + const { htmlxAst, tags } = parseHtmlx(str.original, parse, options); let uses$$props = false; let uses$$restProps = false; @@ -304,6 +305,7 @@ function processSvelteTemplate( export function svelte2tsx( svelte: string, options: { + parse?: typeof import('svelte/compiler').parse; filename?: string; isTsFile?: boolean; emitOnTemplateError?: boolean; @@ -312,7 +314,7 @@ export function svelte2tsx( accessors?: boolean; typingsNamespace?: string; noSvelteComponentTyped?: boolean; - } = {} + } = { parse } ) { options.mode = options.mode || 'ts'; @@ -331,7 +333,7 @@ export function svelte2tsx( componentDocumentation, resolvedStores, usesAccessors - } = processSvelteTemplate(str, options); + } = processSvelteTemplate(str, options.parse || parse, options); /* Rearrange the script tags so that module is first, and instance second followed finally by the template * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index ebdc452fe..bce8150a7 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -21,9 +21,17 @@ interface ExportedName { export class ExportedNames { /** - * Uses the $$Props type + * Uses the `$$Props` type */ public uses$$Props = false; + /** + * The `$props()` rune's type info as a string, if it exists. + * If using TS, this returns the generic string, if using JS, returns the `@type {..}` string. + */ + private $props = { + comment: '', + generic: '' + }; private exports = new Map(); private possibleExports = new Map< string, @@ -72,6 +80,17 @@ export class ExportedNames { node.declarationList, this.addPossibleExport.bind(this) ); + for (const declaration of node.declarationList.declarations) { + if ( + declaration.initializer !== undefined && + ts.isCallExpression(declaration.initializer) && + declaration.initializer.expression.getText() === '$props' + ) { + // @ts-expect-error TS is too stupid to narrow this properly + this.handle$propsRune(declaration); + break; + } + } } } @@ -105,6 +124,30 @@ export class ExportedNames { } } + private handle$propsRune( + node: ts.VariableDeclaration & { initializer: ts.CallExpression } + ): void { + if (node.initializer.typeArguments?.length > 0) { + this.$props.generic = node.initializer.typeArguments[0].getText(); + } else { + const nodeText = node.getFullText(); + let comments = ts + .getLeadingCommentRanges(nodeText, 0) + ?.map((c) => nodeText.substring(c.pos, c.end)) + .find((c) => c.includes('@type')); + if (!comments) { + const parentText = node.parent.getFullText(); + comments = ts + .getLeadingCommentRanges(parentText, node.parent.pos) + ?.map((c) => parentText.substring(c.pos, c.end)) + .find((c) => c.includes('@type')); + } + + // We don't bother extracting the type, we just use the comment as-is + this.$props.comment = comments || ''; + } + } + private removeExport(start: number, end: number) { const exportStart = this.str.original.indexOf('export', start + this.astOffset); const exportEnd = exportStart + (end - start); @@ -386,6 +429,23 @@ export class ExportedNames { createPropsStr(isTsFile: boolean, uses$$propsOr$$restProps: boolean): string { const names = Array.from(this.exports.entries()); + if (this.$props.generic) { + const others = names.filter(([, { isLet }]) => !isLet); + return ( + '{} as any as ' + + this.$props.generic + + (others.length + ? ' & { ' + this.createReturnElementsType(others).join(',') + ' }' + : '') + ); + } + + if (this.$props.comment) { + // TODO: createReturnElements would need to be incorporated here, but don't bother for now. + // In the long run it's probably better to have them on a different object anyway. + return this.$props.comment + '({})'; + } + if (this.uses$$Props) { const lets = names.filter(([, { isLet }]) => isLet); const others = names.filter(([, { isLet }]) => !isLet); diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts index d7a7b0a33..cb17cc941 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts @@ -4,6 +4,7 @@ import TemplateScope from './TemplateScope'; import { SlotHandler } from './slot'; import { isIdentifier, isDestructuringPatterns } from '../../utils/svelteAst'; import { extract_identifiers as extractIdentifiers } from 'periscopic'; +// @ts-ignore import { Directive } from 'svelte/types/compiler/interfaces'; export function handleScopeAndResolveForSlot({ diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts index 249a9ee02..f5ebb1fb4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts @@ -10,6 +10,7 @@ import { } from '../../utils/svelteAst'; import TemplateScope from './TemplateScope'; import { SvelteIdentifier, WithName } from '../../interfaces'; +// @ts-ignore import { Directive } from 'svelte/types/compiler/interfaces'; import ts from 'typescript'; import { isInterfaceOrTypeDeclaration } from '../utils/tsAst'; diff --git a/packages/svelte2tsx/src/utils/htmlxparser.ts b/packages/svelte2tsx/src/utils/htmlxparser.ts index a2682cfdd..4c41cdcf0 100644 --- a/packages/svelte2tsx/src/utils/htmlxparser.ts +++ b/packages/svelte2tsx/src/utils/htmlxparser.ts @@ -1,4 +1,3 @@ -import { parse } from 'svelte/compiler'; import { Node } from 'estree-walker'; function parseAttributes(str: string, start: number) { @@ -106,14 +105,18 @@ function blankVerbatimContent(htmlx: string, verbatimElements: Node[]) { return output; } -export function parseHtmlx(htmlx: string, options?: { emitOnTemplateError?: boolean }) { +export function parseHtmlx( + htmlx: string, + parse: typeof import('svelte/compiler').parse, + options: { emitOnTemplateError?: boolean } +) { //Svelte tries to parse style and script tags which doesn't play well with typescript, so we blank them out. //HTMLx spec says they should just be retained after processing as is, so this is fine const verbatimElements = findVerbatimElements(htmlx); const deconstructed = blankVerbatimContent(htmlx, verbatimElements); //extract the html content parsed as htmlx this excludes our script and style tags - const parsingCode = options?.emitOnTemplateError + const parsingCode = options.emitOnTemplateError ? blankPossiblyErrorOperatorOrPropertyAccess(deconstructed) : deconstructed; const htmlxAst = parse(parsingCode).html; diff --git a/packages/svelte2tsx/test/helpers.ts b/packages/svelte2tsx/test/helpers.ts index 8ba702e71..dd4ded1a9 100644 --- a/packages/svelte2tsx/test/helpers.ts +++ b/packages/svelte2tsx/test/helpers.ts @@ -221,6 +221,8 @@ const enum TestError { export function test_samples(dir: string, transform: TransformSampleFn, js: 'js' | 'ts') { for (const sample of each_sample(dir)) { + if (sample.name.endsWith('.skip')) continue; + const svelteFile = sample.find_file('*.svelte'); const config = { filename: svelteFile, diff --git a/packages/svelte2tsx/test/htmlx2jsx/index.ts b/packages/svelte2tsx/test/htmlx2jsx/index.ts index 19350fd79..ff7fe41f5 100644 --- a/packages/svelte2tsx/test/htmlx2jsx/index.ts +++ b/packages/svelte2tsx/test/htmlx2jsx/index.ts @@ -1,3 +1,4 @@ +import { parse } from 'svelte/compiler'; import { htmlx2jsx } from '../build'; import { test_samples } from '../helpers'; @@ -5,7 +6,7 @@ describe('htmlx2jsx', () => { test_samples( __dirname, (input, { emitOnTemplateError, preserveAttributeCase }) => { - return htmlx2jsx(input, { + return htmlx2jsx(input, parse, { emitOnTemplateError, preserveAttributeCase, typingsNamespace: 'svelteHTML' diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/expectedv2.js new file mode 100644 index 000000000..4114552db --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/expectedv2.js @@ -0,0 +1,13 @@ + const foo = (x) => { + { svelteHTML.createElement("div", {}); x; } +} + +;foo(1); +;foo(); + + { const $$_tnenopmoC0C = __sveltets_2_ensureComponent(Component); new $$_tnenopmoC0C({ target: __sveltets_2_any(), props: {bar:(x) => { + { svelteHTML.createElement("div", {}); x; } + },}}); + { svelteHTML.createElement("div", {});asd; } + + Component} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/input.svelte new file mode 100644 index 000000000..947e2e65c --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/snippet.skip/input.svelte @@ -0,0 +1,13 @@ +{#snippet foo(x)} +
asd{x}
+{/snippet} + +{@render foo(1)} +{@render foo()} + + +
{asd}
+ {#snippet bar(x)} +
asd{x}
+ {/snippet} +
\ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlxparser/index.ts b/packages/svelte2tsx/test/htmlxparser/index.ts index 924e68b2f..9301f1389 100644 --- a/packages/svelte2tsx/test/htmlxparser/index.ts +++ b/packages/svelte2tsx/test/htmlxparser/index.ts @@ -1,6 +1,7 @@ import { htmlx2jsx } from '../build'; import assert from 'assert'; import { benchmark } from '../helpers'; +import { parse } from 'svelte/compiler'; describe('htmlxparser', () => { it('parses in a reasonable time', () => { @@ -9,7 +10,7 @@ describe('htmlxparser', () => { for (let i = 0; i !== 17; i++) random += Math.random().toString(26).slice(2); for (let i = 0; i !== 1137; i++) str += `${random} - line\t${i}\n`; const duration = benchmark( - htmlx2jsx.bind(null, `` + ``) + htmlx2jsx.bind(null, `` + ``, parse) ); assert(duration <= 1000, `Parsing took ${duration} ms, which was longer than 1000ms`); }); diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes/expectedv2.ts new file mode 100644 index 000000000..7d5a49b02 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes/expectedv2.ts @@ -0,0 +1,16 @@ +/// +;function render() { + + /** @type {a: number, b: string} */ + let { a, b } = $props(); + let x = $state(0); + let y = $derived(x * 2); + +/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/; +async () => { + + { __sveltets_createSlot("default", { x,y,});}}; +return { props: /** @type {a: number, b: string} */({}), slots: {'default': {x:x, y:y}}, events: {} }} + +export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(__sveltets_2_with_any_event(render()))) { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/runes/input.svelte new file mode 100644 index 000000000..8053c403f --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes/input.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/expectedv2.ts new file mode 100644 index 000000000..a554a264d --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/expectedv2.ts @@ -0,0 +1,15 @@ +/// +;function render() { + + let { a, b } = $props<{ a: number, b: string }>(); + let x = $state(0); + let y = $derived(x * 2); + +/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/; +async () => { + + { __sveltets_createSlot("default", { x,y,});}}; +return { props: {} as any as { a: number, b: string }, slots: {'default': {x:x, y:y}}, events: {} }} + +export default class Input__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_with_any_event(render())) { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/input.svelte new file mode 100644 index 000000000..f938f3025 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-generics/input.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/expectedv2.ts new file mode 100644 index 000000000..3101de48a --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/expectedv2.ts @@ -0,0 +1,28 @@ +/// +;function render() { + + let { a, b } = $props<{ a: T, b: string }>(); + let x = $state(0); + let y = $derived(x * 2); + +/*Ωignore_startΩ*/;const __sveltets_createSlot = __sveltets_2_createCreateSlot();/*Ωignore_endΩ*/; +async () => { + + { __sveltets_createSlot("default", { x,y,});}}; +return { props: {} as any as { a: T, b: string }, slots: {'default': {x:x, y:y}}, events: {} }} +class __sveltets_Render { + props() { + return render().props; + } + events() { + return __sveltets_2_with_any_event(render()).events; + } + slots() { + return render().slots; + } +} + + +import { SvelteComponentTyped as __SvelteComponentTyped__ } from "svelte" +export default class Input__SvelteComponent_ extends __SvelteComponentTyped__['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/input.svelte new file mode 100644 index 000000000..121a1d625 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes/input.svelte @@ -0,0 +1,7 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4475a68f..59fb88159 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ importers: specifier: ^3.57.0 version: 3.57.0 svelte-preprocess: - specifier: ~5.0.4 - version: 5.0.4(svelte@3.57.0)(typescript@5.2.2) + specifier: ~5.1.0 + version: 5.1.0(svelte@3.57.0)(typescript@5.2.2) svelte2tsx: specifier: workspace:~ version: link:../svelte2tsx @@ -131,11 +131,11 @@ importers: specifier: ^1.7.4 version: 1.8.1 svelte: - specifier: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 - version: 3.59.2 + specifier: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + version: 3.57.0 svelte-preprocess: - specifier: ^5.0.4 - version: 5.0.4(svelte@3.59.2)(typescript@5.2.2) + specifier: ^5.1.0 + version: 5.1.0(svelte@3.57.0)(typescript@5.2.2) typescript: specifier: ^5.0.3 version: 5.2.2 @@ -1821,8 +1821,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /svelte-preprocess@5.0.4(svelte@3.57.0)(typescript@5.2.2): - resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} + /svelte-preprocess@5.1.0(svelte@3.57.0)(typescript@5.2.2): + resolution: {integrity: sha512-EkErPiDzHAc0k2MF5m6vBNmRUh338h2myhinUw/xaqsLs7/ZvsgREiLGj03VrSzbY/TB5ZXgBOsKraFee5yceA==} engines: {node: '>= 14.10.0'} requiresBuild: true peerDependencies: @@ -1835,7 +1835,7 @@ packages: sass: ^1.26.8 stylus: ^0.55.0 sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' peerDependenciesMeta: '@babel/core': @@ -1868,62 +1868,10 @@ packages: typescript: 5.2.2 dev: false - /svelte-preprocess@5.0.4(svelte@3.59.2)(typescript@5.2.2): - resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} - engines: {node: '>= 14.10.0'} - requiresBuild: true - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - dependencies: - '@types/pug': 2.0.6 - detect-indent: 6.1.0 - magic-string: 0.27.0 - sorcery: 0.11.0 - strip-indent: 3.0.0 - svelte: 3.59.2 - typescript: 5.2.2 - dev: false - /svelte@3.57.0: resolution: {integrity: sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==} engines: {node: '>= 8'} - /svelte@3.59.2: - resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} - engines: {node: '>= 8'} - dev: false - /tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: