177 lines
6.5 KiB
JavaScript
Raw Permalink Normal View History

2026-05-31 13:21:13 +02:00
import { defaultMapperFactory, resolveVueCompilerOptions, setupGlobalTypes, createVueLanguagePlugin, createLanguage, FileMap } from '@vue/language-core';
import { createTwoslasher as createTwoslasher$1, defaultCompilerOptions, defaultHandbookOptions, findQueryMarkers, findFlagNotations, getObjectHash } from 'twoslash';
import { createPositionConverter, removeCodeRanges, resolveNodePositions } from 'twoslash-protocol';
import ts from 'typescript';
function createTwoslasher(createOptions = {}) {
const twoslasherBase = createTwoslasher$1(createOptions);
const cache = twoslasherBase.getCacheMap();
const tsOptionDeclarations = ts.optionDeclarations;
function getVueLanguage(compilerOptions, vueCompilerOptions) {
if (!cache)
return getLanguage();
const key = `vue:${getObjectHash([compilerOptions, vueCompilerOptions])}`;
if (!cache.has(key)) {
const env = getLanguage();
cache.set(key, env);
return env;
}
return cache.get(key);
function getLanguage() {
const resolvedVueOptions = resolveVueCompilerOptions(vueCompilerOptions);
resolvedVueOptions.__setupedGlobalTypes = setupGlobalTypes(ts.sys.getCurrentDirectory(), resolvedVueOptions, ts.sys);
const vueLanguagePlugin = createVueLanguagePlugin(ts, defaultCompilerOptions, resolvedVueOptions, (id) => id);
return createLanguage(
[vueLanguagePlugin],
new FileMap(ts.sys.useCaseSensitiveFileNames),
() => {
}
);
}
}
function twoslasher(code, extension, options = {}) {
if (extension !== "vue")
return twoslasherBase(code, extension, options);
const vueCompilerOptions = {
...createOptions.vueCompilerOptions,
...options.vueCompilerOptions
};
const compilerOptions = {
...defaultCompilerOptions,
...options.compilerOptions
};
const handbookOptions = {
...defaultHandbookOptions,
noErrorsCutted: true,
...options.handbookOptions
};
const sourceMeta = {
removals: [],
positionCompletions: [],
positionQueries: [],
positionHighlights: [],
flagNotations: []
};
const {
customTags = createOptions.customTags || []
} = options;
const pc = createPositionConverter(code);
findQueryMarkers(code, sourceMeta, pc);
const flagNotations = findFlagNotations(code, customTags, tsOptionDeclarations);
for (const flag of flagNotations) {
switch (flag.type) {
case "unknown":
continue;
case "compilerOptions":
compilerOptions[flag.name] = flag.value;
break;
case "handbookOptions":
handbookOptions[flag.name] = flag.value;
break;
}
sourceMeta.removals.push([flag.start, flag.end]);
}
let strippedCode = code;
for (const [start, end] of sourceMeta.removals) {
strippedCode = strippedCode.slice(0, start) + strippedCode.slice(start, end).replace(/\S/g, " ") + strippedCode.slice(end);
}
const lang = getVueLanguage(compilerOptions, vueCompilerOptions);
const sourceScript = lang.scripts.set("index.vue", ts.ScriptSnapshot.fromString(strippedCode));
const fileCompiled = get(sourceScript.generated.embeddedCodes.values(), 2);
const compiled = fileCompiled.snapshot.getText(0, fileCompiled.snapshot.getLength());
const map = defaultMapperFactory(fileCompiled.mappings);
function getLastGeneratedOffset(pos) {
const offsets = [...map.toGeneratedLocation(pos)];
if (!offsets.length)
return void 0;
return offsets[offsets.length - 1]?.[0];
}
const result = twoslasherBase(compiled, "tsx", {
...options,
compilerOptions: {
jsx: 1,
jsxImportSource: "vue",
noImplicitAny: false,
...compilerOptions
},
handbookOptions: {
...handbookOptions,
keepNotations: true
},
shouldGetHoverInfo(id) {
return !id.startsWith("__VLS");
},
positionCompletions: sourceMeta.positionCompletions.map((p) => getLastGeneratedOffset(p)),
positionQueries: sourceMeta.positionQueries.map((p) => get(map.toGeneratedLocation(p), 0)?.[0]).filter(isNotNull),
positionHighlights: sourceMeta.positionHighlights.map(([start, end]) => [
get(map.toGeneratedLocation(start), 0)?.[0],
get(map.toGeneratedLocation(end), 0)?.[0]
]).filter((x) => x[0] != null && x[1] != null)
});
if (createOptions.debugShowGeneratedCode)
return result;
const mappedNodes = result.nodes.map((q) => {
if ("text" in q && q.text === "any")
return void 0;
const startMap = get(map.toSourceLocation(q.start), 0);
if (!startMap)
return void 0;
const start = startMap[0];
let end = get(map.toSourceLocation(q.start + q.length), 0)?.[0];
if (end == null && startMap[1].sourceOffsets[0] === startMap[0])
end = startMap[1].sourceOffsets[1];
if (end == null || start < 0 || end < 0 || start > end)
return void 0;
return Object.assign(q, {
...q,
target: code.slice(start, end),
start: startMap[0],
length: end - start
});
}).filter(isNotNull);
const mappedRemovals = [
...sourceMeta.removals,
...result.meta.removals.map((r) => {
const start = get(map.toSourceLocation(r[0]), 0)?.[0] ?? code.match(/(?<=<script[\s\S]*>\s)/)?.index;
const end = get(map.toSourceLocation(r[1]), 0)?.[0];
if (start == null || end == null || start < 0 || end < 0 || start >= end)
return void 0;
return [start, end];
}).filter(isNotNull)
];
if (!options.handbookOptions?.keepNotations) {
const removed = removeCodeRanges(code, mappedRemovals, mappedNodes);
result.code = removed.code;
result.meta.removals = removed.removals;
result.nodes = resolveNodePositions(removed.nodes, result.code);
} else {
result.meta.removals = mappedRemovals;
}
result.nodes = result.nodes.filter((n, idx) => {
const next = result.nodes[idx + 1];
if (!next)
return true;
if (next.type === n.type && next.start === n.start)
return false;
return true;
});
result.meta.extension = "vue";
return result;
}
twoslasher.getCacheMap = twoslasherBase.getCacheMap;
return twoslasher;
}
const createTwoslasherVue = createTwoslasher;
function isNotNull(x) {
return x != null;
}
function get(iterator, index) {
for (const item of iterator) {
if (index-- === 0)
return item;
}
return void 0;
}
export { createTwoslasher, createTwoslasherVue };