Skip to content

Commit 9ee7e9c

Browse files
authored
fix: prevent error when the script tag is removed in nodenext projects (#2635)
* fix error when script tag is removed in node16/nodenext * need to have syntax error * two more cases tracked it in git so we can reenable it later * cleanup * patch update as well * Mark it as clean first so at least it doesn't stuck in an old version even if lang="ts" is added back * try finally instead
1 parent bcd6dd0 commit 9ee7e9c

File tree

3 files changed

+138
-52
lines changed

3 files changed

+138
-52
lines changed

packages/language-server/src/plugins/typescript/module-loader.ts

+3-29
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import {
77
ensureRealSvelteFilePath,
88
getExtensionFromScriptKind,
99
isSvelteFilePath,
10-
isVirtualSvelteFilePath,
11-
toVirtualSvelteFilePath
10+
isVirtualSvelteFilePath
1211
} from './utils';
1312

1413
const CACHE_KEY_SEPARATOR = ':::';
@@ -89,8 +88,6 @@ class ModuleResolutionCache {
8988
}
9089

9190
class ImpliedNodeFormatResolver {
92-
private alreadyResolved = new FileMap<ReturnType<typeof ts.getModeForResolutionAtIndex>>();
93-
9491
constructor(private readonly tsSystem: ts.System) {}
9592

9693
resolve(
@@ -106,39 +103,17 @@ class ImpliedNodeFormatResolver {
106103

107104
let mode: ReturnType<typeof ts.getModeForResolutionAtIndex> = undefined;
108105
if (sourceFile) {
109-
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
110106
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);
111107
}
112108
return mode;
113109
}
114110

115-
private cacheImpliedNodeFormat(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions) {
116-
if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) {
117-
// impliedNodeFormat is not set for Svelte files, because the TS function which
118-
// calculates this works with a fixed set of file extensions,
119-
// which .svelte is obv not part of. Make it work by faking a TS file.
120-
if (!this.alreadyResolved.has(sourceFile.fileName)) {
121-
sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile(
122-
toVirtualSvelteFilePath(sourceFile.fileName) as any,
123-
undefined,
124-
this.tsSystem,
125-
compilerOptions
126-
);
127-
this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat);
128-
} else {
129-
sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName);
130-
}
131-
}
132-
}
133-
134111
resolveForTypeReference(
135112
entry: string | ts.FileReference,
136-
sourceFile: ts.SourceFile | undefined,
137-
compilerOptions: ts.CompilerOptions
113+
sourceFile: ts.SourceFile | undefined
138114
) {
139115
let mode = undefined;
140116
if (sourceFile) {
141-
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
142117
mode = ts.getModeForFileReference(entry, sourceFile?.impliedNodeFormat);
143118
}
144119
return mode;
@@ -315,8 +290,7 @@ export function createSvelteModuleLoader(
315290
const entry = getTypeReferenceResolutionName(typeDirectiveName);
316291
const mode = impliedNodeFormatResolver.resolveForTypeReference(
317292
entry,
318-
containingSourceFile,
319-
options
293+
containingSourceFile
320294
);
321295

322296
const key = `${entry}|${mode}`;

packages/language-server/src/plugins/typescript/service.ts

+84-23
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
findTsConfigPath,
2727
getNearestWorkspaceUri,
2828
hasTsExtensions,
29-
isSvelteFilePath
29+
isSvelteFilePath,
30+
toVirtualSvelteFilePath
3031
} from './utils';
3132
import { createProject, ProjectService } from './serviceCache';
3233
import { internalHelpers } from 'svelte2tsx';
@@ -974,14 +975,19 @@ async function createLanguageService(
974975
}
975976

976977
const oldProgram = project?.program;
977-
const program = languageService.getProgram();
978+
let program: ts.Program | undefined;
979+
try {
980+
program = languageService.getProgram();
981+
} finally {
982+
// mark as clean even if the update fails, at least we can still try again next time there is a change
983+
dirty = false;
984+
}
978985
svelteModuleLoader.clearPendingInvalidations();
979986

980987
if (project) {
981988
project.program = program;
982989
}
983990

984-
dirty = false;
985991
compilerHost = undefined;
986992

987993
if (!skipSvelteInputCheck) {
@@ -1376,38 +1382,93 @@ function getOrCreateDocumentRegistry(
13761382

13771383
registry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory);
13781384

1379-
// impliedNodeFormat is always undefined when the svelte source file is created
1380-
// We might patched it later but the registry doesn't know about it
1381-
const releaseDocumentWithKey = registry.releaseDocumentWithKey;
1382-
registry.releaseDocumentWithKey = (
1385+
const acquireDocumentWithKey = registry.acquireDocumentWithKey;
1386+
registry.acquireDocumentWithKey = (
1387+
fileName: string,
13831388
path: ts.Path,
1389+
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
13841390
key: ts.DocumentRegistryBucketKey,
1385-
scriptKind: ts.ScriptKind,
1386-
impliedNodeFormat?: ts.ResolutionMode
1391+
scriptSnapshot: ts.IScriptSnapshot,
1392+
version: string,
1393+
scriptKind?: ts.ScriptKind,
1394+
sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget
13871395
) => {
1388-
if (isSvelteFilePath(path)) {
1389-
releaseDocumentWithKey(path, key, scriptKind, undefined);
1390-
return;
1391-
}
1396+
ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions);
13921397

1393-
releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat);
1398+
return acquireDocumentWithKey(
1399+
fileName,
1400+
path,
1401+
compilationSettingsOrHost,
1402+
key,
1403+
scriptSnapshot,
1404+
version,
1405+
scriptKind,
1406+
sourceFileOptions
1407+
);
13941408
};
13951409

1396-
registry.releaseDocument = (
1410+
const updateDocumentWithKey = registry.updateDocumentWithKey;
1411+
registry.updateDocumentWithKey = (
13971412
fileName: string,
1398-
compilationSettings: ts.CompilerOptions,
1399-
scriptKind: ts.ScriptKind,
1400-
impliedNodeFormat?: ts.ResolutionMode
1413+
path: ts.Path,
1414+
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
1415+
key: ts.DocumentRegistryBucketKey,
1416+
scriptSnapshot: ts.IScriptSnapshot,
1417+
version: string,
1418+
scriptKind?: ts.ScriptKind,
1419+
sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget
14011420
) => {
1402-
if (isSvelteFilePath(fileName)) {
1403-
registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined);
1404-
return;
1405-
}
1421+
ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions);
14061422

1407-
registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat);
1423+
return updateDocumentWithKey(
1424+
fileName,
1425+
path,
1426+
compilationSettingsOrHost,
1427+
key,
1428+
scriptSnapshot,
1429+
version,
1430+
scriptKind,
1431+
sourceFileOptions
1432+
);
14081433
};
14091434

14101435
documentRegistries.set(key, registry);
14111436

14121437
return registry;
1438+
1439+
function ensureImpliedNodeFormat(
1440+
compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost,
1441+
fileName: string,
1442+
sourceFileOptions: ts.CreateSourceFileOptions | ts.ScriptTarget | undefined
1443+
) {
1444+
const compilationSettings = getCompilationSettings(compilationSettingsOrHost);
1445+
const host: ts.MinimalResolutionCacheHost | undefined =
1446+
compilationSettingsOrHost === compilationSettings
1447+
? undefined
1448+
: (compilationSettingsOrHost as ts.MinimalResolutionCacheHost);
1449+
if (
1450+
host &&
1451+
isSvelteFilePath(fileName) &&
1452+
typeof sourceFileOptions === 'object' &&
1453+
!sourceFileOptions.impliedNodeFormat
1454+
) {
1455+
const format = ts.getImpliedNodeFormatForFile(
1456+
toVirtualSvelteFilePath(fileName),
1457+
host?.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(),
1458+
host,
1459+
compilationSettings
1460+
);
1461+
1462+
sourceFileOptions.impliedNodeFormat = format;
1463+
}
1464+
}
1465+
1466+
function getCompilationSettings(
1467+
settingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost
1468+
) {
1469+
if (typeof settingsOrHost.getCompilationSettings === 'function') {
1470+
return (settingsOrHost as ts.MinimalResolutionCacheHost).getCompilationSettings();
1471+
}
1472+
return settingsOrHost as ts.CompilerOptions;
1473+
}
14131474
}

packages/language-server/test/plugins/typescript/service.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,57 @@ describe('service', () => {
281281
});
282282
});
283283

284+
it('do not throw when script tag is nuked', async () => {
285+
// testing this because the patch rely on ts implementation details
286+
// and we want to be aware of the changes
287+
288+
const dirPath = getRandomVirtualDirPath(testDir);
289+
const { virtualSystem, lsDocumentContext, rootUris } = setup();
290+
291+
virtualSystem.writeFile(
292+
path.join(dirPath, 'tsconfig.json'),
293+
JSON.stringify({
294+
compilerOptions: {
295+
module: 'NodeNext',
296+
moduleResolution: 'NodeNext'
297+
}
298+
})
299+
);
300+
301+
virtualSystem.writeFile(
302+
path.join(dirPath, 'random.svelte'),
303+
'<script>const a: number = null;</script>'
304+
);
305+
virtualSystem.writeFile(
306+
path.join(dirPath, 'random2.svelte'),
307+
'<script lang="ts">import Random from "./random.svelte";</script>'
308+
);
309+
310+
const ls = await getService(
311+
path.join(dirPath, 'random.svelte'),
312+
rootUris,
313+
lsDocumentContext
314+
);
315+
316+
const document = new Document(pathToUrl(path.join(dirPath, 'random.svelte')), '');
317+
document.openedByClient = true;
318+
ls.updateSnapshot(document);
319+
320+
const document2 = new Document(
321+
pathToUrl(path.join(dirPath, 'random2.svelte')),
322+
virtualSystem.readFile(path.join(dirPath, 'random2.svelte'))!
323+
);
324+
document.openedByClient = true;
325+
ls.updateSnapshot(document2);
326+
327+
const lang = ls.getService();
328+
lang.getProgram();
329+
330+
document2.update('<script', 0, document2.getTextLength());
331+
ls.updateSnapshot(document2);
332+
ls.getService();
333+
});
334+
284335
function createReloadTester(
285336
docContext: LanguageServiceDocumentContext,
286337
testAfterReload: (reloadingConfigs: string[]) => Promise<boolean>

0 commit comments

Comments
 (0)