UNPKG

@linden.dev/vue-unclassify

Version:

Create Vue 3 script setup SFC from Vue2/3 class based TypeScript SFCs

195 lines (163 loc) 7.16 kB
import { AnyNode, Comment, Expression, MethodDefinition, Parser, PrivateIdentifier, Program, PropertyDefinition } from 'acorn'; import { tsPlugin } from 'acorn-typescript'; export interface ParsedCode { ast: Program; getSource: (node: AnyNode | null | undefined) => string | null; deconstructProperty: (node: MethodDefinition | PropertyDefinition) => DeconstructedProperty; asLambda: (node: MethodDefinition) => string | undefined; getCommentsFor: (node: AnyNode | null | undefined) => string; unIndent: (text: string) => string; readonly newLine: string; } export interface DeconstructedProperty { id: string; typeStr: any; node: MethodDefinition | PropertyDefinition; async?: boolean; } export function parseTS(code: string): ParsedCode { const newLine = getNewLine(code); const parser = Parser.extend(tsPlugin() as any); try { const comments: Comment[] = []; const ast = parser.parse(code, { ecmaVersion: 'latest', sourceType: 'module', onComment: comments, locations: true // Required for acorn-typescript }); fixBrokenSourceRanges(ast); const commentLines = mapComments(code, newLine, comments); return { ast, getSource: asSource.bind(null, code), deconstructProperty: deconstructProperty.bind(null, code), asLambda: asLambda.bind(null, code), getCommentsFor: getCommentsBefore.bind(null, code, commentLines), unIndent: unIndent.bind(null, code, newLine), newLine }; } catch (ex: any) { let msg = '// Transpilation failure - ' + (ex?.message ?? JSON.stringify(ex)); if (ex.loc?.line) msg += newLine + code?.split(newLine).slice(ex.loc.line - 1, ex.loc.line); throw new Error(msg); } } function fixBrokenSourceRanges(ast: Program) { // Repair buggy node source ranges (acorn-typescript bug?) applyRecursively(ast, n => { if (n.end < n.start) { const locStart = (n.loc?.start as any).index; const locEnd = (n.loc?.end as any).index; if (locStart === n.start && locEnd >= locStart) { // console.debug(`Adjusted broken range (${n.start}-${n.end}) to (${n.start}-${locEnd}) for ${n.type} node in line ${n.loc?.start.line}`); n.end = locEnd; } } }); } export function getNewLine(code: string) { return code.includes('\x0d\x0a') ? '\x0d\x0a' : '\x0a'; } function mapComments(code: string, newLine: string, comments: Comment[]) { comments.reverse(); const lines: { [line: number] : string } = {}; for (const c of comments.filter(x => x.value)) { // Check if comment is on its own line let idx = c.start - 1; while (--idx > 0 && (code[idx] == ' ' || code[idx] == '\t')); const onSeparateLine = ['\n', '\r', ' '].includes(code[idx]); let line = c.loc!.end.line; // If not on same line as code, move line # to next (code) line if (onSeparateLine) line++; // Comment was already found below; merge this one to its line if (lines[line + 1]) line++; const prefix = c.type === 'Block' ? '/*' : '//'; const suffix = c.type === 'Block' ? '*/' : ''; lines[line] = `${prefix}${c.value}${suffix}${newLine}${lines[line] ?? ''}`; } return lines; } function getCommentsBefore(code: string, commentLines: { [line: number] : string }, node: AnyNode | null | undefined) { const line = node?.loc?.start?.line; if (!line) return ''; return commentLines[line]; } function deconstructProperty(code: string, node: PropertyDefinition | MethodDefinition): DeconstructedProperty { const ta = (node as any)?.typeAnnotation?.typeAnnotation; const typeStr = ta?.types?.map((t: AnyNode) => asSource(code, t)).join(' | ') ?? asSource(code, ta?.elementType)?.concat('[]') ?? asSource(code, ta?.typeName ?? ta); return { id: identifier(code, node), typeStr, node, async: node.type === 'MethodDefinition' ? Boolean(node.value?.async) : undefined }; } const singleReturnFunc = /^\s*\{\s*return\s+([^;\{\}]+);?\s*\}/; function asLambda(code: string, node: MethodDefinition) { if (node.type === 'MethodDefinition' && node.value?.body) { const params = node.value.params?.map(p => asSource(code, p)).join(', ') ?? ''; const retType = asSource(code, (node.value as any).returnType) ?? ''; let body = asSource(code, node.value.body); if (body?.length) { const singleReturnBody = singleReturnFunc.exec(body); if (singleReturnBody?.length === 2) body = singleReturnBody[1]; } return `${node.value.async ? 'async ' : ''}(${params})${retType} => ${body}`; } throw new Error('Expecting a method definition'); } function asSource(code: string, node: AnyNode | null | undefined) { return node ? code.substring(node.start, node.end) : null; } function identifier(code: string, node: { key: Expression | PrivateIdentifier}) { return code.substring(node.key.start, node.key.end); } // Generic node methods export function isDecorated(node: AnyNode) { return (node as any).decorators?.length > 0; } export function isDecoratedWith(node: AnyNode, name: string) { const decorators = (node as any).decorators as any[]; return decorators?.length > 0 && decorators.some((d: any) => d.expression?.callee?.name === name); } export function decorators(node: AnyNode) { const decorators = (node as any).decorators as any[]; return decorators?.length > 0 ? decorators.map((d: any) => d.expression?.callee?.name as string).filter(x => x) : []; } export function applyRecursively(node: AnyNode, method: (node: AnyNode) => void) { if (typeof node?.type !== 'string') return; method(node); for (const [prop, value] of Object.entries(node)) { if (prop === 'type') continue; if (Array.isArray(value)) (value as AnyNode[]).forEach(el => applyRecursively(el, method)); else applyRecursively(value as AnyNode, method); } } const indentRegex = /^([ \t]+)(?:[^\s]|$)/; export function unIndent(code: string, newLine: string, bodyText: string) { let lines = bodyText.split(newLine); if (lines.length > 1) { let minIndent: string | null = null; for (const line of lines) { const lineIndent = indentRegex.exec(line)?.[1]; if (lineIndent?.length && (minIndent == null || lineIndent.length < minIndent.length)) minIndent = lineIndent; } if (minIndent?.length) bodyText = lines.map(l => l.replace(minIndent!, '')).join(newLine); } return bodyText; }