Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix) auto import in workspace without tsconfig/jsconfig #1543

Merged
merged 7 commits into from Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -96,12 +96,13 @@ export namespace DocumentSnapshot {
export function fromFilePath(
filePath: string,
createDocument: (filePath: string, text: string) => Document,
options: SvelteSnapshotOptions
options: SvelteSnapshotOptions,
tsSystem: ts.System
) {
if (isSvelteFilePath(filePath)) {
return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options);
} else {
return DocumentSnapshot.fromNonSvelteFilePath(filePath);
return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem);
}
}

Expand All @@ -110,7 +111,7 @@ export namespace DocumentSnapshot {
* @param filePath path to the js/ts file
* @param options options that apply in case it's a svelte file
*/
export function fromNonSvelteFilePath(filePath: string) {
export function fromNonSvelteFilePath(filePath: string, tsSystem: ts.System) {
let originalText = '';

// The following (very hacky) code makes sure that the ambient module definitions
Expand All @@ -121,7 +122,7 @@ export namespace DocumentSnapshot {
// on their own.
const normalizedPath = filePath.replace(/\\/g, '/');
if (!normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
originalText = ts.sys.readFile(filePath) || '';
originalText = tsSystem.readFile(filePath) || '';
}
if (
normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts') ||
Expand Down
@@ -1,3 +1,4 @@
import { dirname } from 'path';
import ts from 'typescript';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { Document, DocumentManager } from '../../lib/documents';
Expand Down Expand Up @@ -27,6 +28,7 @@ interface LSAndTSDocResolverOptions {

onProjectReloaded?: () => void;
watchTsConfig?: boolean;
tsSystem?: ts.System;
}

export class LSAndTSDocResolver {
Expand Down Expand Up @@ -69,7 +71,7 @@ export class LSAndTSDocResolver {
return document;
};

private globalSnapshotsManager = new GlobalSnapshotsManager();
private globalSnapshotsManager = new GlobalSnapshotsManager(this.lsDocumentContext.tsSystem);
private extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();

private get lsDocumentContext(): LanguageServiceDocumentContext {
Expand All @@ -83,7 +85,7 @@ export class LSAndTSDocResolver {
extendedConfigCache: this.extendedConfigCache,
onProjectReloaded: this.options?.onProjectReloaded,
watchTsConfig: !!this.options?.watchTsConfig,
tsSystem: ts.sys
tsSystem: this.options?.tsSystem ?? ts.sys
};
}

Expand Down Expand Up @@ -168,7 +170,11 @@ export class LSAndTSDocResolver {

async getTSService(filePath?: string): Promise<LanguageServiceContainer> {
if (this.options?.tsconfigPath) {
return getServiceForTsconfig(this.options?.tsconfigPath, this.lsDocumentContext);
return getServiceForTsconfig(
this.options?.tsconfigPath,
dirname(this.options.tsconfigPath),
this.lsDocumentContext
);
}
if (!filePath) {
throw new Error('Cannot call getTSService without filePath and without tsconfigPath');
Expand Down
Expand Up @@ -16,6 +16,8 @@ export class GlobalSnapshotsManager {
private emitter = new EventEmitter();
private documents = new Map<string, DocumentSnapshot>();

constructor(private readonly tsSystem: ts.System) {}

get(fileName: string) {
fileName = normalizePath(fileName);
return this.documents.get(fileName);
Expand Down Expand Up @@ -48,7 +50,7 @@ export class GlobalSnapshotsManager {
this.emitter.emit('change', fileName, previousSnapshot);
return previousSnapshot;
} else {
const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);
const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName, this.tsSystem);

if (previousSnapshot) {
newSnapshot.version = previousSnapshot.version + 1;
Expand Down
Expand Up @@ -121,9 +121,10 @@ class ImpliedNodeFormatResolver {
*/
export function createSvelteModuleLoader(
getSnapshot: (fileName: string) => DocumentSnapshot,
compilerOptions: ts.CompilerOptions
compilerOptions: ts.CompilerOptions,
tsSystem: ts.System
) {
const svelteSys = createSvelteSys(getSnapshot);
const svelteSys = createSvelteSys(getSnapshot, tsSystem);
const moduleCache = new ModuleResolutionCache();
const impliedNodeFormatResolver = new ImpliedNodeFormatResolver();

Expand Down
41 changes: 29 additions & 12 deletions packages/language-server/src/plugins/typescript/service.ts
Expand Up @@ -5,15 +5,15 @@ import { getPackageInfo } from '../../importPackage';
import { Document } from '../../lib/documents';
import { configLoader } from '../../lib/documents/configLoader';
import { Logger } from '../../logger';
import { normalizePath } from '../../utils';
import { normalizePath, urlToPath } from '../../utils';
import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot';
import { createSvelteModuleLoader } from './module-loader';
import {
GlobalSnapshotsManager,
ignoredBuildDirectories,
SnapshotManager
} from './SnapshotManager';
import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './utils';
import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions, isSubPath } from './utils';

export interface LanguageServiceContainer {
readonly tsconfigPath: string;
Expand Down Expand Up @@ -78,7 +78,19 @@ export async function getService(
docContext: LanguageServiceDocumentContext
): Promise<LanguageServiceContainer> {
const tsconfigPath = findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists);
return getServiceForTsconfig(tsconfigPath, docContext);

if (tsconfigPath) {
return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext);
}

const nearestWorkspaceUri = workspaceUris.find((workspaceUri) => isSubPath(workspaceUri, path));

return getServiceForTsconfig(
tsconfigPath,
(nearestWorkspaceUri && urlToPath(nearestWorkspaceUri)) ??
docContext.tsSystem.getCurrentDirectory(),
docContext
);
}

export async function forAllServices(
Expand All @@ -95,11 +107,14 @@ export async function forAllServices(
*/
export async function getServiceForTsconfig(
tsconfigPath: string,
workspacePath: string,
docContext: LanguageServiceDocumentContext
): Promise<LanguageServiceContainer> {
const tsconfigPathOrWorkspacePath = tsconfigPath || workspacePath;

let service: LanguageServiceContainer;
if (services.has(tsconfigPath)) {
service = await services.get(tsconfigPath)!;
if (services.has(tsconfigPathOrWorkspacePath)) {
service = await services.get(tsconfigPathOrWorkspacePath)!;
} else {
const reloading = pendingReloads.has(tsconfigPath);

Expand All @@ -110,8 +125,8 @@ export async function getServiceForTsconfig(
}

pendingReloads.delete(tsconfigPath);
const newService = createLanguageService(tsconfigPath, docContext);
services.set(tsconfigPath, newService);
const newService = createLanguageService(tsconfigPath, workspacePath, docContext);
services.set(tsconfigPathOrWorkspacePath, newService);
service = await newService;
}

Expand All @@ -120,9 +135,9 @@ export async function getServiceForTsconfig(

async function createLanguageService(
tsconfigPath: string,
workspacePath: string,
docContext: LanguageServiceDocumentContext
): Promise<LanguageServiceContainer> {
const workspacePath = tsconfigPath ? dirname(tsconfigPath) : '';
const { tsSystem } = docContext;

const {
Expand All @@ -137,15 +152,15 @@ async function createLanguageService(
docContext.globalSnapshotsManager,
files,
raw,
workspacePath || process.cwd()
workspacePath
);

// Load all configs within the tsconfig scope and the one above so that they are all loaded
// by the time they need to be accessed synchronously by DocumentSnapshots to determine
// the default language.
await configLoader.loadConfigs(workspacePath);

const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions);
const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions, tsSystem);

let svelteTsPath: string;
try {
Expand Down Expand Up @@ -263,7 +278,8 @@ async function createLanguageService(
const newSnapshot = DocumentSnapshot.fromFilePath(
filePath,
docContext.createDocument,
transformationConfig
transformationConfig,
tsSystem
);
snapshotManager.set(filePath, newSnapshot);
return newSnapshot;
Expand All @@ -281,7 +297,8 @@ async function createLanguageService(
doc = DocumentSnapshot.fromFilePath(
fileName,
docContext.createDocument,
transformationConfig
transformationConfig,
tsSystem
);
snapshotManager.set(fileName, doc);
return doc;
Expand Down
17 changes: 10 additions & 7 deletions packages/language-server/src/plugins/typescript/svelte-sys.ts
Expand Up @@ -5,14 +5,17 @@ import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath
/**
* This should only be accessed by TS svelte module resolution.
*/
export function createSvelteSys(getSnapshot: (fileName: string) => DocumentSnapshot) {
export function createSvelteSys(
getSnapshot: (fileName: string) => DocumentSnapshot,
tsSystem: ts.System
) {
const fileExistsCache = new Map<string, boolean>();

const svelteSys: ts.System & { deleteFromCache: (path: string) => void } = {
...ts.sys,
...tsSystem,
fileExists(path: string) {
path = ensureRealSvelteFilePath(path);
const exists = fileExistsCache.get(path) ?? ts.sys.fileExists(path);
const exists = fileExistsCache.get(path) ?? tsSystem.fileExists(path);
fileExistsCache.set(path, exists);
return exists;
},
Expand All @@ -23,19 +26,19 @@ export function createSvelteSys(getSnapshot: (fileName: string) => DocumentSnaps
readDirectory(path, extensions, exclude, include, depth) {
const extensionsWithSvelte = (extensions ?? []).concat('.svelte');

return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth);
return tsSystem.readDirectory(path, extensionsWithSvelte, exclude, include, depth);
},
deleteFile(path) {
fileExistsCache.delete(ensureRealSvelteFilePath(path));
return ts.sys.deleteFile?.(path);
return tsSystem.deleteFile?.(path);
},
deleteFromCache(path) {
fileExistsCache.delete(ensureRealSvelteFilePath(path));
}
};

if (ts.sys.realpath) {
const realpath = ts.sys.realpath;
if (tsSystem.realpath) {
const realpath = tsSystem.realpath;
svelteSys.realpath = function (path) {
if (isVirtualSvelteFilePath(path)) {
return realpath(toRealSvelteFilePath(path)) + '.ts';
Expand Down
Expand Up @@ -23,6 +23,7 @@ import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDo
import { sortBy } from 'lodash';
import { LSConfigManager } from '../../../../src/ls-config';
import { __resetCache } from '../../../../src/plugins/typescript/service';
import { getRandomVirtualDirPath, setupVirtualEnvironment } from '../test-utils';

const testDir = join(__dirname, '..');
const testFilesDir = join(testDir, 'testfiles', 'completions');
Expand Down Expand Up @@ -1320,6 +1321,52 @@ function test(useNewTransformation: boolean) {
);
});

it('can auto import in workspace without tsconfig/jsconfig', async () => {
const virtualTestDir = getRandomVirtualDirPath(testFilesDir);
const { docManager, document, lsAndTsDocResolver, lsConfigManager, virtualSystem } =
setupVirtualEnvironment({
filename: 'index.svelte',
fileContent: '<script>f</script>',
testDir: virtualTestDir,
useNewTransformation
});

const mockPackageDir = join(virtualTestDir, 'node_modules', '@types/random-package');

// the main problem is how ts resolve reference type directive
// it would start with a relative url and fail to auto import
virtualSystem.writeFile(
join(mockPackageDir, 'index.d.ts'),
'/// <reference types="random-package2" />' + '\nexport function bar(): string'
);

virtualSystem.writeFile(
join(virtualTestDir, 'node_modules', '@types', 'random-package2', 'index.d.ts'),
'declare function foo(): string\n' + 'export = foo'
);

const completionProvider = new CompletionsProviderImpl(
lsAndTsDocResolver,
lsConfigManager
);

// let the language service aware of random-package and random-package2
docManager.openDocument({
text: '<script>import {} from "random-package";</script>',
uri: pathToUrl(join(virtualTestDir, 'test.svelte'))
});

const completions = await completionProvider.getCompletions(document, {
line: 0,
character: 9
});
const item = completions?.items.find((item) => item.label === 'foo');

const { detail } = await completionProvider.resolveCompletion(document, item!);

assert.strictEqual(detail, 'Auto import from random-package2\nfunction foo(): string');
});

// Hacky, but it works. Needed due to testing both new and old transformation
after(() => {
__resetCache();
Expand Down
Expand Up @@ -24,7 +24,11 @@ describe('createSvelteModuleLoader', () => {
sinon.stub(svS, 'createSvelteSys').returns(svelteSys);

const compilerOptions: ts.CompilerOptions = { strict: true, paths: { '/@/*': [] } };
const moduleResolver = createSvelteModuleLoader(getSvelteSnapshotStub, compilerOptions);
const moduleResolver = createSvelteModuleLoader(
getSvelteSnapshotStub,
compilerOptions,
ts.sys
);

return {
getSvelteSnapshotStub,
Expand Down