@vue.ts/language
Version:
Vue 3 Language Helper
149 lines (146 loc) • 5.58 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, program, 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) {
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 scriptSetupRanges = parseScriptSetupRanges(ast);
const range = filter(scriptSetupRanges);
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(() => vue.createParsedCommandLine(ts, ts.sys, tsconfigPath, true), path.dirname(tsconfig), ts);
}
function createLanguageWorker(loadParsedCommandLine, rootPath, ts) {
let parsedCommandLine = loadParsedCommandLine();
let fileNames = new Set(parsedCommandLine.fileNames.map((fileName) => normalizePath(fileName)));
let projectVersion = 0;
const projectHost = {
getCurrentDirectory: () => rootPath,
getProjectVersion: () => projectVersion.toString(),
getCompilationSettings: () => parsedCommandLine.options,
getScriptFileNames: () => [...fileNames],
getProjectReferences: () => parsedCommandLine.projectReferences
};
const scriptSnapshots = /* @__PURE__ */ new Map();
const vueLanguagePlugin = vue.createVueLanguagePlugin(ts, projectHost.getCompilationSettings(), parsedCommandLine.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);
};
const program = tsLs.getProgram();
const typeChecker = program.getTypeChecker();
const helpers = createHelpers(language, program, parsedCommandLine.vueOptions, ts);
return {
...helpers,
updateFile(fileName, text) {
fileName = normalizePath(fileName);
scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text));
fileNames.add(fileName);
projectVersion++;
},
deleteFile(fileName) {
fileName = normalizePath(fileName);
fileNames.delete(fileName);
projectVersion++;
},
reload() {
parsedCommandLine = loadParsedCommandLine();
fileNames = new Set(parsedCommandLine.fileNames.map((fileName) => normalizePath(fileName)));
this.clearCache();
},
clearCache() {
scriptSnapshots.clear();
projectVersion++;
},
__internal__: {
tsLs,
program,
typeChecker
}
};
}
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 };