UNPKG

@vue.ts/language

Version:
149 lines (146 loc) 5.58 kB
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 };