@ts-macro/twoslash
Version:
Extended Twoslash for ts-macro support
185 lines (179 loc) • 6.65 kB
JavaScript
;
const languagePlugin = require('@ts-macro/language-plugin');
const languageCore = require('@volar/language-core');
const tsMacro = require('ts-macro');
const twoslash = require('twoslash');
const twoslashProtocol = require('twoslash-protocol');
const ts = require('typescript');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const ts__default = /*#__PURE__*/_interopDefaultCompat(ts);
const placeholderPlugin = tsMacro.createPlugin(() => {
return {
name: "ts-macro-placeholder",
resolveVirtualCode() {
}
};
});
function createTwoslasher(createOptions = {}) {
const twoslasherBase = twoslash.createTwoslasher(createOptions);
const cache = twoslasherBase.getCacheMap();
const tsOptionDeclarations = ts__default.optionDeclarations;
function getTsmLanguage(compilerOptions, tsmCompilerOptions) {
if (!cache)
return getLanguage();
const key = `tsm:${twoslash.getObjectHash([compilerOptions, tsmCompilerOptions])}`;
if (!cache.has(key)) {
const env = getLanguage();
cache.set(key, env);
return env;
}
return cache.get(key);
function getLanguage() {
return languageCore.createLanguage(
languagePlugin.getLanguagePlugins(ts__default, twoslash.defaultCompilerOptions, {
plugins: tsmCompilerOptions.plugins ?? [placeholderPlugin()]
}),
new languageCore.FileMap(ts__default.sys.useCaseSensitiveFileNames),
() => {
}
);
}
}
function twoslasher(code, extension, options = {}) {
const tsmCompilerOptions = createOptions.tsmCompilerOptions ?? { plugins: [] };
const compilerOptions = {
...twoslash.defaultCompilerOptions,
noImplicitAny: false,
...options.compilerOptions,
...createOptions.compilerOptions
};
const handbookOptions = {
...twoslash.defaultHandbookOptions,
noErrorsCutted: true,
...options.handbookOptions
};
const sourceMeta = {
removals: [],
positionCompletions: [],
positionQueries: [],
positionHighlights: [],
flagNotations: []
};
const {
customTags = createOptions.customTags || []
} = options;
const pc = twoslashProtocol.createPositionConverter(code);
twoslash.findQueryMarkers(code, sourceMeta, pc);
const flagNotations = twoslash.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__default.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 = languageCore.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 = twoslashProtocol.removeCodeRanges(code, mappedRemovals, mappedNodes);
result.code = removed.code;
result.meta.removals = removed.removals;
result.nodes = twoslashProtocol.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;
}
exports.createTwoslasher = createTwoslasher;