UNPKG

@ts-macro/twoslash

Version:

Extended Twoslash for ts-macro support

179 lines (176 loc) 6.45 kB
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 };