Skip to content

Commit

Permalink
fix: improve ts extraction error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Mar 7, 2023
1 parent 6f906e9 commit 89ad8bb
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 131 deletions.
6 changes: 6 additions & 0 deletions .changeset/little-pans-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@marko/language-server": patch
"marko-vscode": patch
---

Avoid crashing ts plugin if there's an error in Marko's ts plugin. Better handle some script extraction errors.
2 changes: 1 addition & 1 deletion packages/language-server/src/service/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ const requiredTSCompilerOptions: ts.CompilerOptions = {
emitDecoratorMetadata: false,
};
const defaultTSConfig = {
include: [],
compilerOptions: {
lib: ["dom", "node", "esnext"],
} satisfies ts.CompilerOptions,
include: [],
};
const extraTSCompilerExtensions: readonly ts.FileExtensionInfo[] = [
{
Expand Down
30 changes: 18 additions & 12 deletions packages/language-server/src/ts-plugin/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ExtractedSnapshot extends Extracted {
export function patch(
ts: typeof import("typescript/lib/tsserverlibrary"),
scriptLang: ScriptLang,
cache: Map<string, ExtractedSnapshot>,
cache: Map<string, ExtractedSnapshot | { snapshot: ts.IScriptSnapshot }>,
host: ts.LanguageServiceHost
) {
const projectTypeLibs = getProjectTypeLibs(
Expand Down Expand Up @@ -71,17 +71,23 @@ export function patch(
if (!cached) {
const code = host.readFile(filename, "utf-8") || "";
const dir = path.dirname(filename);
const markoProject = getMarkoProject(dir);
cached = extractScript({
ts,
parsed: parse(code, filename),
lookup: markoProject.getLookup(dir),
scriptLang: getScriptLang(filename, ts, host, scriptLang),
runtimeTypesCode: projectTypeLibs.markoTypesCode,
componentFilename: getComponentFilename(filename),
}) as ExtractedSnapshot;

cached.snapshot = ts.ScriptSnapshot.fromString(cached.toString());

try {
const markoProject = getMarkoProject(dir);
cached = extractScript({
ts,
parsed: parse(code, filename),
lookup: markoProject.getLookup(dir),
scriptLang: getScriptLang(filename, ts, host, scriptLang),
runtimeTypesCode: projectTypeLibs.markoTypesCode,
componentFilename: getComponentFilename(filename),
}) as ExtractedSnapshot;

cached.snapshot = ts.ScriptSnapshot.fromString(cached.toString());
} catch {
cached = { snapshot: ts.ScriptSnapshot.fromString("") };
}

cache.set(filename, cached);
}

Expand Down
242 changes: 125 additions & 117 deletions packages/language-server/src/ts-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,145 +27,153 @@ export function init({ typescript: ts }: InitOptions): ts.server.PluginModule {
languageService: ls,
languageServiceHost: lsh,
} = info;
const { projectService: ps } = tsProject;
const extraExtensions = (ps as any)?.hostConfiguration
?.extraFileExtensions;

if (
extraExtensions &&
!extraExtensions.some(
(it: { extension: string }) => it.extension === markoExt
)
) {
// The first time we install the plugin we update the config to allow `.marko` extensions.
// This will cause the plugin to be called again, so we check that the extension is not already added.
ps.setHostConfiguration({
extraFileExtensions: extraExtensions.concat({
extension: markoExt,
isMixedContent: false,
scriptKind: ts.ScriptKind.Deferred,
}),
});
}

const markoScriptLang = /[/\\]tsconfig.json$/.test(
getConfigFilePath(tsProject) || ""
)
? // If we have a `tsconfig.json` then Marko files will be processed as ts, otherwise js.
ScriptLang.ts
: ScriptLang.js;
const extractCache = new Map<string, ExtractedSnapshot>();
patch(ts, markoScriptLang, extractCache, lsh);

/**
* Here we invalidate our snapshot cache when TypeScript invalidates the file.
*/
const onSourceFileChanged = (ps as any).onSourceFileChanged.bind(ps);
(ps as any).onSourceFileChanged = (
info: ts.server.ScriptInfo,
eventKind: ts.FileWatcherEventKind
) => {
try {
const { projectService: ps } = tsProject;
const extraExtensions = (ps as any)?.hostConfiguration
?.extraFileExtensions;

if (
eventKind === ts.FileWatcherEventKind.Changed
? markoTaglibFilesReg.test(info.fileName)
: markoExtReg.test(info.fileName) ||
markoTaglibFilesReg.test(info.fileName)
extraExtensions &&
!extraExtensions.some(
(it: { extension: string }) => it.extension === markoExt
)
) {
if (markoTaglibFilesReg.test(info.fileName)) {
for (const project of getMarkoProjects()) {
project.cache.clear();
}
}
// The first time we install the plugin we update the config to allow `.marko` extensions.
// This will cause the plugin to be called again, so we check that the extension is not already added.
ps.setHostConfiguration({
extraFileExtensions: extraExtensions.concat({
extension: markoExt,
isMixedContent: false,
scriptKind: ts.ScriptKind.Deferred,
}),
});
}

extractCache.delete(info.fileName);
return onSourceFileChanged(info, eventKind);
};

/**
* Whenever TypeScript requests line/character info we return with the source
* file line/character if it exists.
*/
const toLineColumnOffset = (
ls.toLineColumnOffset || getStartLineCharacter
).bind(ls);
ls.toLineColumnOffset = (fileName, pos) => {
if (pos === 0) return START_POSITION;

const extracted = extractCache.get(fileName);
if (extracted) {
return extracted.sourcePositionAt(pos) || START_POSITION;
const markoScriptLang = /[/\\]tsconfig.json$/.test(
getConfigFilePath(tsProject) || ""
)
? // If we have a `tsconfig.json` then Marko files will be processed as ts, otherwise js.
ScriptLang.ts
: ScriptLang.js;
const extractCache = new Map<string, ExtractedSnapshot>();
patch(ts, markoScriptLang, extractCache, lsh);

/**
* Here we invalidate our snapshot cache when TypeScript invalidates the file.
*/
const onSourceFileChanged = (ps as any).onSourceFileChanged?.bind(ps);

if (onSourceFileChanged) {
(ps as any).onSourceFileChanged = (
info: ts.server.ScriptInfo,
eventKind: ts.FileWatcherEventKind
) => {
if (
eventKind === ts.FileWatcherEventKind.Changed
? markoTaglibFilesReg.test(info.fileName)
: markoExtReg.test(info.fileName) ||
markoTaglibFilesReg.test(info.fileName)
) {
if (markoTaglibFilesReg.test(info.fileName)) {
for (const project of getMarkoProjects()) {
project.cache.clear();
}
}
}

extractCache.delete(info.fileName);
return onSourceFileChanged(info, eventKind);
};
}

return toLineColumnOffset(fileName, pos);
};
/**
* Whenever TypeScript requests line/character info we return with the source
* file line/character if it exists.
*/
const toLineColumnOffset = (
ls.toLineColumnOffset || getStartLineCharacter
).bind(ls);
ls.toLineColumnOffset = (fileName, pos) => {
if (pos === 0) return START_POSITION;

const extracted = extractCache.get(fileName);
if (extracted) {
return extracted.sourcePositionAt(pos) || START_POSITION;
}

const findReferences = ls.findReferences.bind(ls);
ls.findReferences = (fileName, position) => {
const symbols = findReferences(fileName, position);
if (!symbols) return;
return toLineColumnOffset(fileName, pos);
};

const result: ts.ReferencedSymbol[] = [];
for (const symbol of symbols) {
let definition: ts.ReferencedSymbolDefinitionInfo | undefined =
symbol.definition;
const defExtracted = extractCache.get(definition.fileName);
const findReferences = ls.findReferences.bind(ls);
ls.findReferences = (fileName, position) => {
const symbols = findReferences(fileName, position);
if (!symbols) return;

if (defExtracted) {
definition = mapTextSpans(defExtracted, definition);
if (!definition) continue;
}
const result: ts.ReferencedSymbol[] = [];
for (const symbol of symbols) {
let definition: ts.ReferencedSymbolDefinitionInfo | undefined =
symbol.definition;
const defExtracted = extractCache.get(definition.fileName);

const references: ts.ReferencedSymbolEntry[] = [];
for (const reference of symbol.references) {
const refExtracted = extractCache.get(reference.fileName);
if (refExtracted) {
const updated = mapTextSpans(refExtracted, reference);
if (updated) references.push(updated);
} else {
references.push(reference);
if (defExtracted) {
definition = mapTextSpans(defExtracted, definition);
if (!definition) continue;
}

const references: ts.ReferencedSymbolEntry[] = [];
for (const reference of symbol.references) {
const refExtracted = extractCache.get(reference.fileName);
if (refExtracted) {
const updated = mapTextSpans(refExtracted, reference);
if (updated) references.push(updated);
} else {
references.push(reference);
}
}

result.push({
definition,
references,
});
}

result.push({
definition,
references,
});
}
return result;
};

return result;
};

const findRenameLocations = ls.findRenameLocations.bind(ls);
ls.findRenameLocations = (
fileName,
position,
findInStrings,
findInComments,
providePrefixAndSuffixTextForRename
) => {
const renames = findRenameLocations(
const findRenameLocations = ls.findRenameLocations.bind(ls);
ls.findRenameLocations = (
fileName,
position,
findInStrings,
findInComments,
providePrefixAndSuffixTextForRename
);
if (!renames) return;

const result: ts.RenameLocation[] = [];
for (const rename of renames) {
const extracted = extractCache.get(rename.fileName);
if (extracted) {
const updated = mapTextSpans(extracted, rename);
if (updated) result.push(updated);
} else {
result.push(rename);
) => {
const renames = findRenameLocations(
fileName,
position,
findInStrings,
findInComments,
providePrefixAndSuffixTextForRename
);
if (!renames) return;

const result: ts.RenameLocation[] = [];
for (const rename of renames) {
const extracted = extractCache.get(rename.fileName);
if (extracted) {
const updated = mapTextSpans(extracted, rename);
if (updated) result.push(updated);
} else {
result.push(rename);
}
}
}

return result;
};
return result;
};
} catch (err) {
console.error(err);
}

return ls;
},
Expand Down
10 changes: 9 additions & 1 deletion packages/language-server/src/utils/get-component-filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function getComponentFilename(from: string) {
const componentBrowserFull = `${nameNoExt}.component-browser.`;
const componentPartial = isEntry ? "component." : undefined;
const componentBrowserPartial = isEntry ? "component-browser." : undefined;
for (const entry of fs.readdirSync(dir)) {
for (const entry of tryReaddirSync(dir)) {
// Prefers `component-browser` over `component`.
if (
(entry !== from &&
Expand All @@ -22,3 +22,11 @@ export default function getComponentFilename(from: string) {
}
}
}

function tryReaddirSync(dir: string) {
try {
return fs.readdirSync(dir);
} catch {
return [];
}
}

0 comments on commit 89ad8bb

Please sign in to comment.