@ts-macro/twoslash
Version:
Extended Twoslash for ts-macro support
179 lines (176 loc) • 6.45 kB
JavaScript
import { getLanguagePlugins } from '@ts-macro/language-plugin';
import { defaultMapperFactory, createLanguage, FileMap } from '@volar/language-core';
import { createPlugin } from 'ts-macro';
import { createTwoslasher as createTwoslasher$1, defaultCompilerOptions, defaultHandbookOptions, findQueryMarkers, findFlagNotations, getObjectHash } from 'twoslash';
import { createPositionConverter, removeCodeRanges, resolveNodePositions } from 'twoslash-protocol';
import ts from 'typescript';
const placeholderPlugin = createPlugin(() => {
return {
name: "ts-macro-placeholder",
resolveVirtualCode() {
}
};
});
function createTwoslasher(createOptions = {}) {
const twoslasherBase = createTwoslasher$1(createOptions);
const cache = twoslasherBase.getCacheMap();
const tsOptionDeclarations = ts.optionDeclarations;
function getTsmLanguage(compilerOptions, tsmCompilerOptions) {
if (!cache)
return getLanguage();
const key = `tsm:${getObjectHash([compilerOptions, tsmCompilerOptions])}`;
if (!cache.has(key)) {
const env = getLanguage();
cache.set(key, env);
return env;
}
return cache.get(key);
function getLanguage() {
return createLanguage(
getLanguagePlugins(ts, defaultCompilerOptions, {
plugins: tsmCompilerOptions.plugins ?? [placeholderPlugin()]
}),
new FileMap(ts.sys.useCaseSensitiveFileNames),
() => {
}
);
}
}
function twoslasher(code, extension, options = {}) {
const tsmCompilerOptions = createOptions.tsmCompilerOptions ?? { plugins: [] };
const compilerOptions = {
...defaultCompilerOptions,
noImplicitAny: false,
...options.compilerOptions,
...createOptions.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 = getTsmLanguage(compilerOptions, tsmCompilerOptions);
const sourceScript = lang.scripts.set("index.tsx", ts.ScriptSnapshot.fromString(strippedCode), "typescriptreact");
if (!sourceScript?.generated) {
return twoslasherBase(code, extension, options);
}
const fileCompiled = get(sourceScript.generated.embeddedCodes.values(), 0);
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,
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];
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 = "tsx";
return result;
}
twoslasher.getCacheMap = twoslasherBase.getCacheMap;
return twoslasher;
}
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 };