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 error when the script tag is removed in nodenext projects #2635

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
32 changes: 3 additions & 29 deletions packages/language-server/src/plugins/typescript/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
ensureRealSvelteFilePath,
getExtensionFromScriptKind,
isSvelteFilePath,
isVirtualSvelteFilePath,
toVirtualSvelteFilePath
isVirtualSvelteFilePath
} from './utils';

const CACHE_KEY_SEPARATOR = ':::';
Expand Down Expand Up @@ -89,8 +88,6 @@ class ModuleResolutionCache {
}

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

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

resolve(
Expand All @@ -106,39 +103,17 @@ class ImpliedNodeFormatResolver {

let mode: ReturnType<typeof ts.getModeForResolutionAtIndex> = undefined;
if (sourceFile) {
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);
}
return mode;
}

private cacheImpliedNodeFormat(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions) {
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,
this.tsSystem,
compilerOptions
);
this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat);
} else {
sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName);
}
}
}

resolveForTypeReference(
entry: string | ts.FileReference,
sourceFile: ts.SourceFile | undefined,
compilerOptions: ts.CompilerOptions
sourceFile: ts.SourceFile | undefined
) {
let mode = undefined;
if (sourceFile) {
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
mode = ts.getModeForFileReference(entry, sourceFile?.impliedNodeFormat);
}
return mode;
Expand Down Expand Up @@ -315,8 +290,7 @@ export function createSvelteModuleLoader(
const entry = getTypeReferenceResolutionName(typeDirectiveName);
const mode = impliedNodeFormatResolver.resolveForTypeReference(
entry,
containingSourceFile,
options
containingSourceFile
);

const key = `${entry}|${mode}`;
Expand Down
98 changes: 77 additions & 21 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
findTsConfigPath,
getNearestWorkspaceUri,
hasTsExtensions,
isSvelteFilePath
isSvelteFilePath,
toVirtualSvelteFilePath
} from './utils';
import { createProject, ProjectService } from './serviceCache';
import { internalHelpers } from 'svelte2tsx';
Expand Down Expand Up @@ -1376,38 +1377,93 @@ function getOrCreateDocumentRegistry(

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 = (
const acquireDocumentWithKey = registry.acquireDocumentWithKey;
registry.acquireDocumentWithKey = (
fileName: string,
path: ts.Path,
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
key: ts.DocumentRegistryBucketKey,
scriptKind: ts.ScriptKind,
impliedNodeFormat?: ts.ResolutionMode
scriptSnapshot: ts.IScriptSnapshot,
version: string,
scriptKind?: ts.ScriptKind,
sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget
) => {
if (isSvelteFilePath(path)) {
releaseDocumentWithKey(path, key, scriptKind, undefined);
return;
}
ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions);

releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat);
return acquireDocumentWithKey(
fileName,
path,
compilationSettingsOrHost,
key,
scriptSnapshot,
version,
scriptKind,
sourceFileOptions
);
};

registry.releaseDocument = (
const updateDocumentWithKey = registry.updateDocumentWithKey;
registry.updateDocumentWithKey = (
fileName: string,
compilationSettings: ts.CompilerOptions,
scriptKind: ts.ScriptKind,
impliedNodeFormat?: ts.ResolutionMode
path: ts.Path,
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
key: ts.DocumentRegistryBucketKey,
scriptSnapshot: ts.IScriptSnapshot,
version: string,
scriptKind?: ts.ScriptKind,
sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget
) => {
if (isSvelteFilePath(fileName)) {
registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined);
return;
}
ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions);

registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat);
return updateDocumentWithKey(
fileName,
path,
compilationSettingsOrHost,
key,
scriptSnapshot,
version,
scriptKind,
sourceFileOptions
);
};

documentRegistries.set(key, registry);

return registry;

function ensureImpliedNodeFormat(
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
fileName: string,
sourceFileOptions: ts.CreateSourceFileOptions | ts.ScriptTarget | undefined
) {
const compilationSettings = getCompilationSettings(compilationSettingsOrHost);
const host: ts.MinimalResolutionCacheHost | undefined =
compilationSettingsOrHost === compilationSettings
? undefined
: (compilationSettingsOrHost as ts.MinimalResolutionCacheHost);
if (
host &&
isSvelteFilePath(fileName) &&
typeof sourceFileOptions === 'object' &&
!sourceFileOptions.impliedNodeFormat
) {
const format = ts.getImpliedNodeFormatForFile(
toVirtualSvelteFilePath(fileName),
host?.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(),
host,
compilationSettings
);

sourceFileOptions.impliedNodeFormat = format;
}
}

function getCompilationSettings(
settingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost
) {
if (typeof settingsOrHost.getCompilationSettings === 'function') {
return (settingsOrHost as ts.MinimalResolutionCacheHost).getCompilationSettings();
}
return settingsOrHost as ts.CompilerOptions;
}
}
51 changes: 51 additions & 0 deletions packages/language-server/test/plugins/typescript/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,57 @@ describe('service', () => {
});
});

it('do not throw when script tag is nuked', 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'
}
})
);

virtualSystem.writeFile(
path.join(dirPath, 'random.svelte'),
'<script>const a: number = null;</script>'
);
virtualSystem.writeFile(
path.join(dirPath, 'random2.svelte'),
'<script lang="ts">import Random from "./random.svelte";</script>'
);

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')),
virtualSystem.readFile(path.join(dirPath, 'random2.svelte'))!
);
document.openedByClient = true;
ls.updateSnapshot(document2);

const lang = ls.getService();
lang.getProgram();

document2.update('<script', 0, document2.getTextLength());
ls.updateSnapshot(document2);
ls.getService();
});

function createReloadTester(
docContext: LanguageServiceDocumentContext,
testAfterReload: (reloadingConfigs: string[]) => Promise<boolean>
Expand Down
Loading