UNPKG

@vue.ts/language

Version:
271 lines (268 loc) 8.19 kB
import { createRequire } from 'node:module'; import * as path from 'node:path'; import { createLanguage as createLanguage$1 } from '@volar/typescript'; import * as vue from '@vue/language-core'; import { normalizePath } from '@vue.ts/common'; const require = createRequire(import.meta.url); function createLanguage(tsconfigPath, ts = require("typescript")) { const tsconfig = normalizePath(tsconfigPath); return createLanguageWorker( () => vue.createParsedCommandLine(ts, ts.sys, tsconfigPath), path.dirname(tsconfig), tsconfig, ts ); } function createLanguageWorker(loadParsedCommandLine, rootPath, configFileName, ts) { let parsedCommandLine = loadParsedCommandLine(); let fileNames = parsedCommandLine.fileNames.map(normalizePath); let projectVersion = 0; const scriptSnapshots = /* @__PURE__ */ new Map(); const _host = { // Commented to wait for volar 2.2.0-alpha.0 // ...ts.sys, // configFileName, getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => parsedCommandLine.options, getScriptFileNames: () => fileNames, getProjectReferences: () => parsedCommandLine.projectReferences, getScriptSnapshot: (fileName) => { if (!scriptSnapshots.has(fileName)) { const fileText = ts.sys.readFile(fileName); if (fileText !== void 0) { scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(fileText)); } } return scriptSnapshots.get(fileName); }, getLanguageId: (fileName) => { if (parsedCommandLine.vueOptions.extensions.some( (ext) => fileName.endsWith(ext) )) { return "vue"; } return vue.resolveCommonLanguageId(fileName); } // scriptIdToFileName: (id) => id, // fileNameToScriptId: (id) => id, }; return { ...baseCreateLanguageWorker( ts, configFileName, _host, parsedCommandLine.vueOptions ), updateFile(fileName, text) { fileName = normalizePath(fileName); scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text)); projectVersion++; }, deleteFile(fileName) { fileName = normalizePath(fileName); fileNames = fileNames.filter((f) => f !== fileName); projectVersion++; }, reload() { parsedCommandLine = loadParsedCommandLine(); fileNames = parsedCommandLine.fileNames.map(normalizePath); this.clearCache(); }, clearCache() { scriptSnapshots.clear(); projectVersion++; } }; } function baseCreateLanguageWorker(ts, configFileName, host, vueCompilerOptions) { const vueLanguagePlugin = vue.createVueLanguagePlugin( ts, (id) => id, (fileName) => { if (ts.sys.useCaseSensitiveFileNames) { return host.getScriptFileNames().includes(fileName) ?? false; } else { const lowerFileName = fileName.toLowerCase(); for (const rootFile of host.getScriptFileNames()) { if (rootFile.toLowerCase() === lowerFileName) { return true; } } return false; } }, host.getCompilationSettings(), vueCompilerOptions ); const language = createLanguage$1( ts, ts.sys, [vueLanguagePlugin], configFileName, host, { fileIdToFileName: (id) => id, fileNameToFileId: (id) => id } ); const { languageServiceHost } = language.typescript; 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(); function getScriptSetupBlock(normalizedFilepath) { const sourceFile = language.files.get(normalizedFilepath)?.generated?.code; if (!(sourceFile instanceof vue.VueGeneratedCode)) { return; } if (!sourceFile.sfc.scriptSetup) { return; } const { lang } = sourceFile.sfc.scriptSetup; if (lang !== "ts" && lang !== "tsx") { return; } return sourceFile.sfc.scriptSetup; } function findNodeByRange(filepath, filter) { filepath = normalizePath(filepath); const scriptSetupBlock = getScriptSetupBlock(filepath); if (!scriptSetupBlock) { return; } const { startTagEnd: offset } = scriptSetupBlock; const scriptSetupAst = getScriptSetupAst(filepath); if (!scriptSetupAst) { return; } const scriptSetupRanges = vue.parseScriptSetupRanges( ts, scriptSetupAst, vueCompilerOptions ); const virtualFileAst = getVirtualFileOrTsAst(filepath); if (!virtualFileAst) { return; } const virtualTsFileScriptSetupRanges = vue.parseScriptSetupRanges( ts, virtualFileAst, vueCompilerOptions ); const sourceRange = filter(scriptSetupRanges); const virtualTsRange = filter(virtualTsFileScriptSetupRanges); if (!sourceRange || !virtualTsRange) { return; } let foundVirtualFileNode; virtualFileAst.forEachChild(function traverse(node) { if (node.getStart(virtualFileAst) === virtualTsRange.start && node.getEnd() === virtualTsRange.end) { foundVirtualFileNode = node; } else { node.forEachChild(traverse); } }); let foundSetupNode; scriptSetupAst.forEachChild(function traverse(node) { if (node.getStart(scriptSetupAst) === sourceRange.start && node.getEnd() === sourceRange.end) { foundSetupNode = node; } else { node.forEachChild(traverse); } }); const setupRange = { start: offset + sourceRange.start, end: offset + sourceRange.end }; return { virtualFileNode: foundVirtualFileNode, scriptNode: foundSetupNode, setupRange, offset }; } function getScriptSetupAst(filepath) { filepath = normalizePath(filepath); const scriptSetupBlock = getScriptSetupBlock(filepath); if (!scriptSetupBlock) { return; } return scriptSetupBlock.ast; } function getVirtualFileOrTsAst(filepath) { filepath = normalizePath(filepath); let virtualFileOrTsAst = program.getSourceFile(filepath); if (virtualFileOrTsAst) { return virtualFileOrTsAst; } const scriptSetupBlock = getScriptSetupBlock(filepath); if (!scriptSetupBlock) { return; } const { lang } = scriptSetupBlock; virtualFileOrTsAst = program.getSourceFile(`${filepath}.${lang}`); return virtualFileOrTsAst; } function traverseAst(filepath, cb) { filepath = normalizePath(filepath); const scriptSetupAst = getScriptSetupAst(filepath); const virtualFileOrTsAst = getVirtualFileOrTsAst(filepath); if (!virtualFileOrTsAst) { return; } if (scriptSetupAst) { scriptSetupAst.forEachChild(function traverse(node) { cb(node, { scriptSetupAst, virtualFileOrTsAst, isVirtualOrTsFile: false }); node.forEachChild(traverse); }); } if (virtualFileOrTsAst) { virtualFileOrTsAst.forEachChild(function traverse(node) { cb(node, { scriptSetupAst, virtualFileOrTsAst, isVirtualOrTsFile: true }); node.forEachChild(traverse); }); } } return { findNodeByRange, getScriptSetupAst, getVirtualFileAst: getVirtualFileOrTsAst, traverseAst, __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]; } export { ensureLanguage, getLanguage };