@vue.ts/language
Version:
Vue 3 Language Helper
150 lines (147 loc) • 5.75 kB
JavaScript
import { createRequire } from "node:module";
import * as path from "node:path";
import { createLanguageServiceHost, resolveFileLanguageId } from "@volar/typescript";
import * as vue from "@vue/language-core";
import { normalizePath } from "@vue.ts/shared";
//#region src/helpers.ts
function createHelpers(language, tsLs, vueCompilerOptions, ts) {
const parseScriptSetupRanges = (ast) => vue.parseScriptSetupRanges(ts, ast, vueCompilerOptions);
function getScriptSetupBlock(normalizedFilepath) {
const sourceFile = language.scripts.get(normalizedFilepath)?.generated?.root;
if (!(sourceFile instanceof vue.VueVirtualCode)) return;
if (!sourceFile.sfc.scriptSetup) return;
const { lang } = sourceFile.sfc.scriptSetup;
if (lang !== "ts" && lang !== "tsx") return;
return sourceFile.sfc.scriptSetup;
}
function getScriptSetupAst(normalizedFilepath) {
const scriptSetupBlock = getScriptSetupBlock(normalizedFilepath);
if (!scriptSetupBlock) return;
return scriptSetupBlock.ast;
}
function getVirtualFileOrTsAst(normalizedFilepath) {
const program = tsLs.getProgram();
let virtualFileOrTsAst = program.getSourceFile(normalizedFilepath);
if (virtualFileOrTsAst) return virtualFileOrTsAst;
const scriptSetupBlock = getScriptSetupBlock(normalizedFilepath);
if (!scriptSetupBlock) return;
const { lang } = scriptSetupBlock;
virtualFileOrTsAst = program.getSourceFile(`${normalizedFilepath}.${lang}`);
return virtualFileOrTsAst;
}
function traverseAst(ast, cb) {
ts.forEachChild(ast, function traverse(node) {
cb(node);
ts.forEachChild(node, traverse);
});
}
function findNodeByRange(ast, filter) {
const range = filter(parseScriptSetupRanges(ast));
if (!range) return;
let node;
traverseAst(ast, (n) => {
if (n.getStart(ast) === range.start && n.getEnd() === range.end) node = n;
});
return node;
}
function findNodeByName(ast, name) {
let node;
traverseAst(ast, (n) => {
if (ts.isIdentifier(n) && n.getText(ast) === name) node = n;
});
return node;
}
return {
parseScriptSetupRanges,
getScriptSetupBlock,
getScriptSetupAst,
getVirtualFileOrTsAst,
traverseAst,
findNodeByRange,
findNodeByName
};
}
//#endregion
//#region src/index.ts
const require = createRequire(import.meta.url);
function createLanguage(tsconfigPath, ts = require("typescript")) {
const tsconfig = normalizePath(tsconfigPath);
return createLanguageWorker(ts, () => {
const commandLine = vue.createParsedCommandLine(ts, ts.sys, tsconfig);
const { fileNames } = ts.parseJsonSourceFileConfigFileContent(ts.readJsonConfigFile(tsconfig, ts.sys.readFile), ts.sys, path.dirname(tsconfig), {}, tsconfig, void 0, vue.getAllExtensions(commandLine.vueOptions).map((extension) => ({
extension: extension.slice(1),
isMixedContent: true,
scriptKind: ts.ScriptKind.Deferred
})));
return [commandLine, fileNames];
}, path.dirname(tsconfig));
}
function createLanguageWorker(ts, getConfigAndFiles, rootPath) {
let [{ vueOptions, options, projectReferences }, fileNames] = getConfigAndFiles();
let fileNamesSet = new Set(fileNames.map((fileName) => normalizePath(fileName)));
let projectVersion = 0;
const projectHost = {
getCurrentDirectory: () => rootPath,
getProjectVersion: () => projectVersion.toString(),
getCompilationSettings: () => options,
getScriptFileNames: () => [...fileNamesSet],
getProjectReferences: () => projectReferences
};
const scriptSnapshots = /* @__PURE__ */ new Map();
const vueLanguagePlugin = vue.createVueLanguagePlugin(ts, projectHost.getCompilationSettings(), vueOptions, (id) => id);
const language = vue.createLanguage([vueLanguagePlugin, { getLanguageId(fileName) {
return resolveFileLanguageId(fileName);
} }], new vue.FileMap(ts.sys.useCaseSensitiveFileNames), (fileName) => {
let snapshot = scriptSnapshots.get(fileName);
if (!scriptSnapshots.has(fileName)) {
const fileText = ts.sys.readFile(fileName);
if (fileText) scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(fileText));
else scriptSnapshots.set(fileName, void 0);
}
snapshot = scriptSnapshots.get(fileName);
if (snapshot) language.scripts.set(fileName, snapshot);
else language.scripts.delete(fileName);
});
const { languageServiceHost } = createLanguageServiceHost(ts, ts.sys, language, (s) => s, projectHost);
const tsLs = ts.createLanguageService(languageServiceHost);
const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost);
languageServiceHost.getScriptKind = (fileName) => {
if (fileName.endsWith(".vue.js")) return ts.ScriptKind.TS;
if (fileName.endsWith(".vue.jsx")) return ts.ScriptKind.TSX;
return getScriptKind(fileName);
};
return {
...createHelpers(language, tsLs, vueOptions, ts),
tsLs,
updateFile(fileName, text) {
fileName = normalizePath(fileName);
scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text));
fileNamesSet.add(fileName);
projectVersion++;
},
deleteFile(fileName) {
fileName = normalizePath(fileName);
fileNamesSet.delete(fileName);
projectVersion++;
},
reload() {
[{vueOptions, options, projectReferences}, fileNames] = getConfigAndFiles();
fileNamesSet = new Set(fileNames.map(normalizePath));
this.clearCache();
},
clearCache() {
scriptSnapshots.clear();
projectVersion++;
}
};
}
const LANGUAGE_GLOBAL_KEY = "__VUETS_LANGUAGE__";
function ensureLanguage(tsconfigPath) {
if (!globalThis[LANGUAGE_GLOBAL_KEY]) globalThis[LANGUAGE_GLOBAL_KEY] = createLanguage(tsconfigPath);
}
function getLanguage() {
if (!globalThis[LANGUAGE_GLOBAL_KEY]) throw new Error("[@vue.ts/language] Language not created!");
return globalThis[LANGUAGE_GLOBAL_KEY];
}
//#endregion
export { ensureLanguage, getLanguage };