diff --git a/package.json b/package.json index b822ce1b4..0cbb28a0a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint": "prettier --check . && eslint \"packages/**/*.{ts,js}\"" }, "dependencies": { - "typescript": "^4.7.2" + "typescript": "^4.7.3" }, "devDependencies": { "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.2.0", diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 1fcac077a..1a4133a9a 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -5,7 +5,9 @@ import { createSvelteSys } from './svelte-sys'; import { ensureRealSvelteFilePath, getExtensionFromScriptKind, - isVirtualSvelteFilePath + isSvelteFilePath, + isVirtualSvelteFilePath, + toVirtualSvelteFilePath } from './utils'; /** @@ -67,6 +69,38 @@ class ModuleResolutionCache { } } +class ImpliedNodeFormatResolver { + private alreadyResolved = new Map>(); + + resolve( + importIdxInFile: number, + sourceFile: ts.SourceFile | undefined, + compilerOptions: ts.CompilerOptions + ) { + let mode = undefined; + if (sourceFile) { + if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) { + // impliedNodeFormat is not set for Svelte files, because the TS function which + // calculates this works with a fixed set of file extensions, + // which .svelte is obv not part of. Make it work by faking a TS file. + if (!this.alreadyResolved.has(sourceFile.fileName)) { + sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile( + toVirtualSvelteFilePath(sourceFile.fileName) as any, + undefined, + ts.sys, + compilerOptions + ); + this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat); + } else { + sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName); + } + } + mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile); + } + return mode; + } +} + /** * Creates a module loader specifically for `.svelte` files. * @@ -85,6 +119,7 @@ export function createSvelteModuleLoader( ) { const svelteSys = createSvelteSys(getSnapshot); const moduleCache = new ModuleResolutionCache(); + const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(); return { fileExists: svelteSys.fileExists, @@ -103,14 +138,23 @@ export function createSvelteModuleLoader( function resolveModuleNames( moduleNames: string[], - containingFile: string + containingFile: string, + _reusedNames: string[] | undefined, + _redirectedReference: ts.ResolvedProjectReference | undefined, + _options: ts.CompilerOptions, + containingSourceFile?: ts.SourceFile | undefined ): Array { - return moduleNames.map((moduleName) => { + return moduleNames.map((moduleName, index) => { if (moduleCache.has(moduleName, containingFile)) { return moduleCache.get(moduleName, containingFile); } - const resolvedModule = resolveModuleName(moduleName, containingFile); + const resolvedModule = resolveModuleName( + moduleName, + containingFile, + containingSourceFile, + index + ); moduleCache.set(moduleName, containingFile, resolvedModule); return resolvedModule; }); @@ -118,8 +162,15 @@ export function createSvelteModuleLoader( function resolveModuleName( name: string, - containingFile: string + containingFile: string, + containingSourceFile: ts.SourceFile | undefined, + index: number ): ts.ResolvedModule | undefined { + const mode = impliedNodeFormatResolver.resolve( + index, + containingSourceFile, + compilerOptions + ); // Delegate to the TS resolver first. // If that does not bring up anything, try the Svelte Module loader // which is able to deal with .svelte files. @@ -127,7 +178,10 @@ export function createSvelteModuleLoader( name, containingFile, compilerOptions, - ts.sys + ts.sys, + undefined, + undefined, + mode ).resolvedModule; if (tsResolvedModule && !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) { return tsResolvedModule; @@ -137,7 +191,10 @@ export function createSvelteModuleLoader( name, containingFile, compilerOptions, - svelteSys + svelteSys, + undefined, + undefined, + mode ).resolvedModule; if ( !svelteResolvedModule || diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 3e964da5b..91c180ebc 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -292,8 +292,6 @@ async function createLanguageService( const forcedCompilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, target: ts.ScriptTarget.Latest, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true, noEmit: true, declaration: false, @@ -344,6 +342,25 @@ async function createLanguageService( ...parsedConfig.options, ...forcedCompilerOptions }; + if ( + !compilerOptions.moduleResolution || + compilerOptions.moduleResolution === ts.ModuleResolutionKind.Classic + ) { + compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs; + } + if ( + !compilerOptions.module || + [ + ts.ModuleKind.AMD, + ts.ModuleKind.CommonJS, + ts.ModuleKind.ES2015, + ts.ModuleKind.None, + ts.ModuleKind.System, + ts.ModuleKind.UMD + ].includes(compilerOptions.module) + ) { + compilerOptions.module = ts.ModuleKind.ESNext; + } // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) { diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts index 0e99f3c10..2e04817ff 100644 --- a/packages/language-server/src/plugins/typescript/utils.ts +++ b/packages/language-server/src/plugins/typescript/utils.ts @@ -76,6 +76,10 @@ export function toRealSvelteFilePath(filePath: string) { return filePath.slice(0, -'.ts'.length); } +export function toVirtualSvelteFilePath(filePath: string) { + return filePath.endsWith('.ts') ? filePath : filePath + '.ts'; +} + export function ensureRealSvelteFilePath(filePath: string) { return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath; } diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/bar.js b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/bar.js new file mode 100644 index 000000000..160c145f1 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/bar.js @@ -0,0 +1,2 @@ +export const foo = true; +export const baz = true; diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expected.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expected.json new file mode 100644 index 000000000..f17503b0b --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expected.json @@ -0,0 +1,10 @@ +[ + { + "range": { "start": { "line": 4, "character": 22 }, "end": { "line": 4, "character": 29 } }, + "severity": 1, + "source": "ts", + "message": "Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './bar.js'?", + "code": 2835, + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expectedv2.json new file mode 100644 index 000000000..f17503b0b --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/expectedv2.json @@ -0,0 +1,10 @@ +[ + { + "range": { "start": { "line": 4, "character": 22 }, "end": { "line": 4, "character": 29 } }, + "severity": 1, + "source": "ts", + "message": "Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './bar.js'?", + "code": 2835, + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/input.svelte new file mode 100644 index 000000000..2b2a30987 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/package.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/package.json new file mode 100644 index 000000000..472002573 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/tsconfig.json new file mode 100644 index 000000000..cdd78a581 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/node16/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "module": "Node16", + "moduleResolution": "Node16", + /** + This is actually not needed, but makes the tests faster + because TS does not look up other types. + */ + "types": ["svelte"] + } +} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json index 7a6e55774..f8add4155 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json @@ -7,5 +7,5 @@ */ "types": ["svelte"] }, - "exclude": ["./svelte-native/**/*"] + "exclude": ["./svelte-native/**/*", "./node16/**/*"] } 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 51fe58f2c..5ecbbe150 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -47,7 +47,10 @@ describe('createSvelteModuleLoader', () => { const { resolveStub, moduleResolver, compilerOptions } = setup(resolvedModule); const result = moduleResolver.resolveModuleNames( ['./normal.ts'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [resolvedModule]); @@ -55,7 +58,10 @@ describe('createSvelteModuleLoader', () => { './normal.ts', 'C:/somerepo/somefile.svelte', compilerOptions, - ts.sys + ts.sys, + undefined, + undefined, + undefined ]); }); @@ -67,7 +73,10 @@ describe('createSvelteModuleLoader', () => { const { resolveStub, moduleResolver, compilerOptions } = setup(resolvedModule); const result = moduleResolver.resolveModuleNames( ['/@/normal'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [resolvedModule]); @@ -75,7 +84,10 @@ describe('createSvelteModuleLoader', () => { '/@/normal', 'C:/somerepo/somefile.svelte', compilerOptions, - ts.sys + ts.sys, + undefined, + undefined, + undefined ]); }); @@ -87,7 +99,10 @@ describe('createSvelteModuleLoader', () => { const { resolveStub, moduleResolver, compilerOptions } = setup(resolvedModule); const result = moduleResolver.resolveModuleNames( ['./normal.ts'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [resolvedModule]); @@ -95,7 +110,10 @@ describe('createSvelteModuleLoader', () => { './normal.ts', 'C:/somerepo/somefile.svelte', compilerOptions, - ts.sys + ts.sys, + undefined, + undefined, + undefined ]); }); @@ -108,7 +126,10 @@ describe('createSvelteModuleLoader', () => { setup(resolvedModule); const result = moduleResolver.resolveModuleNames( ['./svelte.svelte'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [ @@ -122,7 +143,10 @@ describe('createSvelteModuleLoader', () => { './svelte.svelte', 'C:/somerepo/somefile.svelte', compilerOptions, - svelteSys + svelteSys, + undefined, + undefined, + undefined ]); assert.deepStrictEqual(lastCall(getSvelteSnapshotStub).args, ['filename.svelte']); }); @@ -136,7 +160,10 @@ describe('createSvelteModuleLoader', () => { setup(resolvedModule); const result = moduleResolver.resolveModuleNames( ['/@/svelte.svelte'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [ @@ -150,7 +177,10 @@ describe('createSvelteModuleLoader', () => { '/@/svelte.svelte', 'C:/somerepo/somefile.svelte', compilerOptions, - svelteSys + svelteSys, + undefined, + undefined, + undefined ]); assert.deepStrictEqual(lastCall(getSvelteSnapshotStub).args, ['filename.svelte']); }); @@ -162,11 +192,20 @@ describe('createSvelteModuleLoader', () => { }; const { resolveStub, moduleResolver } = setup(resolvedModule); // first call - moduleResolver.resolveModuleNames(['./normal.ts'], 'C:/somerepo/somefile.svelte'); + moduleResolver.resolveModuleNames( + ['./normal.ts'], + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any + ); // second call, which should be from cache const result = moduleResolver.resolveModuleNames( ['./normal.ts'], - 'C:/somerepo/somefile.svelte' + 'C:/somerepo/somefile.svelte', + undefined, + undefined, + undefined as any ); assert.deepStrictEqual(result, [resolvedModule]); diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 0118a084a..801ee1ea1 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -37,7 +37,7 @@ "svelte": "~3.48.0", "tiny-glob": "^0.2.6", "tslib": "^1.10.0", - "typescript": "^4.7.2" + "typescript": "^4.7.3" }, "peerDependencies": { "svelte": "^3.24", diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts index 036b6a2f8..0ccc69e02 100644 --- a/packages/typescript-plugin/src/module-loader.ts +++ b/packages/typescript-plugin/src/module-loader.ts @@ -88,7 +88,8 @@ export function patchModuleLoader( containingFile: string, reusedNames: string[] | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, - compilerOptions: ts.CompilerOptions + compilerOptions: ts.CompilerOptions, + containingSourceFile?: ts.SourceFile ): Array { logger.log('Resolving modules names for ' + containingFile); // Try resolving all module names with the original method first. @@ -101,7 +102,8 @@ export function patchModuleLoader( containingFile, reusedNames, redirectedReference, - compilerOptions + compilerOptions, + containingSourceFile ) || Array.from(Array(moduleNames.length)); if (!configManager.getConfig().enable) { @@ -130,6 +132,10 @@ export function patchModuleLoader( containingFile: string, compilerOptions: ts.CompilerOptions ): ts.ResolvedModule | undefined { + // If we were to do this correctly, we'd need to check the module resolution mode for + // Node16/NodeNext in order to determine whether or not someone needs to have .js file + // endings in relative import paths. But because we don't do diagnostics within Svelte + // files in this TS plugin, we can ignore this and deal with some false positive resolutions const svelteResolvedModule = typescript.resolveModuleName( name, containingFile, diff --git a/yarn.lock b/yarn.lock index c2cdac512..58dc093e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,10 +2634,10 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typescript@*, typescript@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" - integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== +typescript@*,typescript@^4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" + integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== unist-util-stringify-position@^2.0.0: version "2.0.3"