Skip to content

Commit

Permalink
(feat) support TS resolution mode node16/nodenext (#1526)
Browse files Browse the repository at this point in the history
#1522

- only force module/resolutionmode if not set to something valid by user
- TS plugin: add new param to ts resolver to fix diagnostics not appearing
  • Loading branch information
dummdidumm committed Jun 18, 2022
1 parent 83d92c5 commit 5922491
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 30 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
71 changes: 64 additions & 7 deletions packages/language-server/src/plugins/typescript/module-loader.ts
Expand Up @@ -5,7 +5,9 @@ import { createSvelteSys } from './svelte-sys';
import {
ensureRealSvelteFilePath,
getExtensionFromScriptKind,
isVirtualSvelteFilePath
isSvelteFilePath,
isVirtualSvelteFilePath,
toVirtualSvelteFilePath
} from './utils';

/**
Expand Down Expand Up @@ -67,6 +69,38 @@ class ModuleResolutionCache {
}
}

class ImpliedNodeFormatResolver {
private alreadyResolved = new Map<string, ReturnType<typeof ts.getModeForResolutionAtIndex>>();

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.
*
Expand All @@ -85,6 +119,7 @@ export function createSvelteModuleLoader(
) {
const svelteSys = createSvelteSys(getSnapshot);
const moduleCache = new ModuleResolutionCache();
const impliedNodeFormatResolver = new ImpliedNodeFormatResolver();

return {
fileExists: svelteSys.fileExists,
Expand All @@ -103,31 +138,50 @@ 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<ts.ResolvedModule | undefined> {
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;
});
}

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.
const tsResolvedModule = ts.resolveModuleName(
name,
containingFile,
compilerOptions,
ts.sys
ts.sys,
undefined,
undefined,
mode
).resolvedModule;
if (tsResolvedModule && !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) {
return tsResolvedModule;
Expand All @@ -137,7 +191,10 @@ export function createSvelteModuleLoader(
name,
containingFile,
compilerOptions,
svelteSys
svelteSys,
undefined,
undefined,
mode
).resolvedModule;
if (
!svelteResolvedModule ||
Expand Down
21 changes: 19 additions & 2 deletions packages/language-server/src/plugins/typescript/service.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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')) {
Expand Down
4 changes: 4 additions & 0 deletions packages/language-server/src/plugins/typescript/utils.ts
Expand Up @@ -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;
}
Expand Down
@@ -0,0 +1,2 @@
export const foo = true;
export const baz = true;
@@ -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": []
}
]
@@ -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": []
}
]
@@ -0,0 +1,8 @@
<script lang="ts">
// valid
import { foo } from './bar.js';
// invalid
import { baz } from './bar';
foo;baz;
</script>
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -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"]
}
}
Expand Up @@ -7,5 +7,5 @@
*/
"types": ["svelte"]
},
"exclude": ["./svelte-native/**/*"]
"exclude": ["./svelte-native/**/*", "./node16/**/*"]
}
Expand Up @@ -47,15 +47,21 @@ 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]);
assert.deepStrictEqual(lastCall(resolveStub).args, [
'./normal.ts',
'C:/somerepo/somefile.svelte',
compilerOptions,
ts.sys
ts.sys,
undefined,
undefined,
undefined
]);
});

Expand All @@ -67,15 +73,21 @@ 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]);
assert.deepStrictEqual(lastCall(resolveStub).args, [
'/@/normal',
'C:/somerepo/somefile.svelte',
compilerOptions,
ts.sys
ts.sys,
undefined,
undefined,
undefined
]);
});

Expand All @@ -87,15 +99,21 @@ 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]);
assert.deepStrictEqual(lastCall(resolveStub).args, [
'./normal.ts',
'C:/somerepo/somefile.svelte',
compilerOptions,
ts.sys
ts.sys,
undefined,
undefined,
undefined
]);
});

Expand All @@ -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, [
Expand All @@ -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']);
});
Expand All @@ -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, [
Expand All @@ -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']);
});
Expand All @@ -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]);
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte2tsx/package.json
Expand Up @@ -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",
Expand Down

0 comments on commit 5922491

Please sign in to comment.