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: prevent crash in moduleResolution Node16+ #2230

Merged
merged 2 commits into from
Dec 13, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
ensureRealSvelteFilePath,
findTsConfigPath,
getNearestWorkspaceUri,
hasTsExtensions
hasTsExtensions,
isSvelteFilePath
} from './utils';

export interface LanguageServiceContainer {
Expand Down Expand Up @@ -84,6 +85,7 @@ const extendedConfigToTsConfigPath = new FileMap<FileSet>();
const configFileModifiedTime = new FileMap<Date | undefined>();
const configFileForOpenFiles = new FileMap<string>();
const pendingReloads = new FileSet();
const documentRegistries = new Map<string, ts.DocumentRegistry>();

/**
* For testing only: Reset the cache for services.
Expand Down Expand Up @@ -295,7 +297,12 @@ async function createLanguageService(
hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions
};

let languageService = ts.createLanguageService(host);
const documentRegistry = getOrCreateDocumentRegistry(
host.getCurrentDirectory(),
tsSystem.useCaseSensitiveFileNames
);

const languageService = ts.createLanguageService(host, documentRegistry);
const transformationConfig: SvelteSnapshotOptions = {
parse: svelteCompiler?.parse,
version: svelteCompiler?.VERSION,
Expand Down Expand Up @@ -366,11 +373,6 @@ async function createLanguageService(
const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig);

snapshotManager.set(filePath, newSnapshot);
if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) {
// Restart language service as it doesn't handle script kind changes.
languageService.dispose();
languageService = ts.createLanguageService(host);
}

return newSnapshot;
}
Expand Down Expand Up @@ -853,3 +855,53 @@ function scheduleReload(fileName: string) {
// where a file update is received before the service is reloaded, swallowing the update
pendingReloads.add(fileName);
}

function getOrCreateDocumentRegistry(
currentDirectory: string,
useCaseSensitiveFileNames: boolean
): ts.DocumentRegistry {
// unless it's a multi root workspace, there's only one registry
const key = [currentDirectory, useCaseSensitiveFileNames].join('|');

let registry = documentRegistries.get(key);
if (registry) {
return registry;
}

registry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory);

// impliedNodeFormat is always undefined when the svelte source file is created
// We might patched it later but the registry doesn't know about it
const releaseDocumentWithKey = registry.releaseDocumentWithKey;
registry.releaseDocumentWithKey = (
path: ts.Path,
key: ts.DocumentRegistryBucketKey,
scriptKind: ts.ScriptKind,
impliedNodeFormat?: ts.ResolutionMode
) => {
if (isSvelteFilePath(path)) {
releaseDocumentWithKey(path, key, scriptKind, undefined);
return;
}

releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat);
};

registry.releaseDocument = (
fileName: string,
compilationSettings: ts.CompilerOptions,
scriptKind: ts.ScriptKind,
impliedNodeFormat?: ts.ResolutionMode
) => {
if (isSvelteFilePath(fileName)) {
registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined);
return;
}

registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat);
};

documentRegistries.set(key, registry);

return registry;
}
47 changes: 47 additions & 0 deletions packages/language-server/test/plugins/typescript/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,53 @@ describe('service', () => {
});
});

it('patch release document so dispose do not throw', async () => {
// testing this because the patch rely on ts implementation details
// and we want to be aware of the changes

const dirPath = getRandomVirtualDirPath(testDir);
const { virtualSystem, lsDocumentContext, rootUris } = setup();

virtualSystem.writeFile(
path.join(dirPath, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
module: 'NodeNext',
moduleResolution: 'NodeNext'
}
})
);

const ls = await getService(
path.join(dirPath, 'random.svelte'),
rootUris,
lsDocumentContext
);

const document = new Document(pathToUrl(path.join(dirPath, 'random.svelte')), '');
document.openedByClient = true;
ls.updateSnapshot(document);

const document2 = new Document(
pathToUrl(path.join(dirPath, 'random2.svelte')),
'<script>import Random from "./random.svelte";</script>'
);
document.openedByClient = true;
ls.updateSnapshot(document2);

const lang = ls.getService();

lang.getProgram();

// ensure updated document also works
document2.update(' ', 0, 0);
lang.getProgram();

assert.doesNotThrow(() => {
lang.dispose();
});
});

function createReloadTester(
docContext: LanguageServiceDocumentContext,
testAfterReload: () => Promise<void>
Expand Down