Skip to content

Commit

Permalink
fix: make sure snapshots are shared per ProjectService
Browse files Browse the repository at this point in the history
The ProjectService contains snapshots. Multiple TypeScript plugins may be instantiated for one projectservice. Ensure we share our internal Svelte snapshots between those instances.

#2602
  • Loading branch information
dummdidumm committed Dec 3, 2024
1 parent fda35fe commit 5c3c9ad
Showing 1 changed file with 76 additions and 84 deletions.
160 changes: 76 additions & 84 deletions packages/typescript-plugin/src/svelte-snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export class SvelteSnapshot {
}

export class SvelteSnapshotManager {
private snapshots = new Map<string, SvelteSnapshot>();
private snapshots: Map<string, SvelteSnapshot>;

constructor(
private typescript: typeof ts,
Expand All @@ -280,7 +280,9 @@ export class SvelteSnapshotManager {
/** undefined if no node_modules with Svelte next to tsconfig.json */
private svelteCompiler: typeof import('svelte/compiler') | undefined
) {
this.patchProjectServiceReadFile();
this.patchProjectService();
// @ts-expect-error
this.snapshots = this.projectService[snapshots];
}

get(fileName: string) {
Expand Down Expand Up @@ -321,92 +323,78 @@ export class SvelteSnapshotManager {
return snapshot;
}

private patchProjectServiceReadFile() {
// @ts-ignore The projectService is shared across some instances, make sure we patch readFile only once
if (!this.projectService.host[onReadSvelteFile]) {
this.logger.log('patching projectService host readFile');
private patchProjectService() {
// @ts-expect-error The projectService is shared across some instances, make sure we patch only once
if (this.projectService[snapshots]) return;

// @ts-ignore
this.projectService.host[onReadSvelteFile] = [];
this.logger.log('patching projectService');

const readFile = this.projectService.host.readFile;
this.projectService.host.readFile = (path: string, encoding?: string | undefined) => {
if (!this.configManager.getConfig().enable) {
return readFile(path, encoding);
}
// @ts-expect-error Snapshots are stored on the projectService, so they are shared across all instances
this.snapshots = this.projectService[snapshots] = new Map();

// The following (very hacky) first two checks make sure that the ambient module definitions
// that tell TS "every import ending with .svelte is a valid module" are removed.
// They exist in svelte2tsx and svelte to make sure that people don't
// get errors in their TS files when importing Svelte files and not using our TS plugin.
// If someone wants to get back the behavior they can add an ambient module definition
// on their own.
const normalizedPath = path.replace(/\\/g, '/');
if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
return '';
} else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) {
// Remove the dom lib reference to not load these ambient types in case
// the user has a tsconfig.json with different lib settings like in
// https://github.com/sveltejs/language-tools/issues/1733
const originalText = readFile(path) || '';
const toReplace = '/// <reference lib="dom" />';
return originalText.replace(toReplace, ' '.repeat(toReplace.length));
} else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) {
let originalText = readFile(path) || '';
if (!originalText.includes('// -- start svelte-ls-remove --')) {
return originalText; // uses an older version of svelte2tsx or is already patched
}
const startIdx = originalText.indexOf('// -- start svelte-ls-remove --');
const endIdx = originalText.indexOf('// -- end svelte-ls-remove --');
originalText =
originalText.substring(0, startIdx) +
' '.repeat(endIdx - startIdx) +
originalText.substring(endIdx);
return originalText;
} else if (isSvelteFilePath(path)) {
this.logger.debug('Read Svelte file:', path);
const svelteCode = readFile(path) || '';
const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js.
let code: string;
let mapper: SourceMapper;

try {
const result = svelte2tsx(svelteCode, {
filename: path.split('/').pop(),
isTsFile,
mode: 'ts',
typingsNamespace: this.svelteOptions.namespace,
// Don't search for compiler from current path - could be a different one from which we have loaded the svelte2tsx globals
parse: this.svelteCompiler?.parse,
version: this.svelteCompiler?.VERSION
});
code = result.code;
mapper = new SourceMapper(result.map.mappings);
this.logger.log('Successfully read Svelte file contents of', path);
} catch (e) {
this.logger.log('Error loading Svelte file:', path, ' Using fallback.');
this.logger.debug('Error:', e);
// Return something either way, else "X is not a module" errors will appear
// in the TS files that use this file.
code = 'export default class extends Svelte2TsxComponent<any,any,any> {}';
mapper = new SourceMapper('');
}

// @ts-ignore
this.projectService.host[onReadSvelteFile].forEach((listener) =>
listener(path, svelteCode, isTsFile, mapper)
);
const readFile = this.projectService.host.readFile;
this.projectService.host.readFile = (path: string, encoding?: string | undefined) => {
if (!this.configManager.getConfig().enable) {
return readFile(path, encoding);
}

return code;
} else {
return readFile(path, encoding);
// The following (very hacky) first two checks make sure that the ambient module definitions
// that tell TS "every import ending with .svelte is a valid module" are removed.
// They exist in svelte2tsx and svelte to make sure that people don't
// get errors in their TS files when importing Svelte files and not using our TS plugin.
// If someone wants to get back the behavior they can add an ambient module definition
// on their own.
const normalizedPath = path.replace(/\\/g, '/');
if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
return '';
} else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) {
// Remove the dom lib reference to not load these ambient types in case
// the user has a tsconfig.json with different lib settings like in
// https://github.com/sveltejs/language-tools/issues/1733
const originalText = readFile(path) || '';
const toReplace = '/// <reference lib="dom" />';
return originalText.replace(toReplace, ' '.repeat(toReplace.length));
} else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) {
let originalText = readFile(path) || '';
if (!originalText.includes('// -- start svelte-ls-remove --')) {
return originalText; // uses an older version of svelte2tsx or is already patched
}
const startIdx = originalText.indexOf('// -- start svelte-ls-remove --');
const endIdx = originalText.indexOf('// -- end svelte-ls-remove --');
originalText =
originalText.substring(0, startIdx) +
' '.repeat(endIdx - startIdx) +
originalText.substring(endIdx);
return originalText;
} else if (isSvelteFilePath(path)) {
this.logger.debug('Read Svelte file:', path);
const svelteCode = readFile(path) || '';
const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js.
let code: string;
let mapper: SourceMapper;

try {
const result = svelte2tsx(svelteCode, {
filename: path.split('/').pop(),
isTsFile,
mode: 'ts',
typingsNamespace: this.svelteOptions.namespace,
// Don't search for compiler from current path - could be a different one from which we have loaded the svelte2tsx globals
parse: this.svelteCompiler?.parse,
version: this.svelteCompiler?.VERSION
});
code = result.code;
mapper = new SourceMapper(result.map.mappings);
this.logger.log('Successfully read Svelte file contents of', path);
} catch (e) {
this.logger.log('Error loading Svelte file:', path, ' Using fallback.');
this.logger.debug('Error:', e);
// Return something either way, else "X is not a module" errors will appear
// in the TS files that use this file.
code = 'export default class extends Svelte2TsxComponent<any,any,any> {}';
mapper = new SourceMapper('');
}
};
}

// @ts-ignore
this.projectService.host[onReadSvelteFile].push(
(path: string, svelteCode: string, isTsFile: boolean, mapper: SourceMapper) => {
const canonicalFilePath = this.projectService.toCanonicalFileName(path);
const existingSnapshot = this.snapshots.get(canonicalFilePath);
if (existingSnapshot) {
Expand All @@ -424,9 +412,13 @@ export class SvelteSnapshotManager {
)
);
}

return code;
} else {
return readFile(path, encoding);
}
);
};
}
}

const onReadSvelteFile = Symbol('sveltePluginPatchSymbol');
const snapshots = Symbol('sveltePluginPatchSymbol');

0 comments on commit 5c3c9ad

Please sign in to comment.