UNPKG

@vue.ts/language

Version:
150 lines (147 loc) 5.75 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, 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 };