import { traverse } from '@babel/core' ; import { parse } from '@babel/parser' ; import { Comment , Decorator , File } from '@babel/types' ; import prettier , { Options as PrettierOptions } from 'prettier-reflow' ; import { ReflowOptions } from '../../plugin' ; import { getParserPlugins } from '../../plugin/util/options' ; import { FLOW_DIRECTIVE } from '../../plugin/visitors/program' ; export const BLANK_LINE = /^[ \t]*$/ ; export const LINE_BREAK = /\r?\n/ ; const BLOCK_COMMENT_AT_BEGINNING_OF_LINE = /^\s*\/\*/ ; const JSX_COMMENT = /\{\s*\/\*.*\*\/\s*\}/ ; const LINE_COMMENT_AT_BEGINNING_OF_LINE = /^\s*\/\/.*$/ ; function getPrettierConfig ( overrides ? ) { const defaults = { parser : 'typescript' , arrowParens : 'avoid' , singleQuote : true , trailingComma : 'all' , } ; return { ... defaults , ... overrides } ; } function parseAst ( code ) { return parse ( code , { plugins : getParserPlugins ( 'flow' ) , sourceType : 'module' , } ) ; } function getDecorators ( flowAst ) { const decorators = [ ] ; traverse ( flowAst , { enter ( path ) { if ( path . isClassDeclaration ( ) && path . node . decorators ) { decorators . push ( ... path . node . decorators ) ; } } , } ) ; return decorators ; } function copyComments ( outputLines , originalLines , originalAst , lineNumber ) { const outputLine = outputLines [ lineNumber ] ; const comment = ( originalAst . comments as Comment [ ] ) . find ( comment => comment . loc . start . line - 1 === lineNumber ) ; if ( comment ) { const originalLine = originalLines [ lineNumber ] ; if ( comment . type === 'CommentBlock' ) { if ( BLOCK_COMMENT_AT_BEGINNING_OF_LINE . test ( originalLine ) ) { `/*${ comment . value }*/` . split ( '\n' ) . reverse ( ) . forEach ( commentLine => { outputLines . splice ( lineNumber , 0 , commentLine ) ; } ) ; } else if ( JSX_COMMENT . test ( originalLine ) ) { outputLines [ lineNumber ] = `{/*${ comment . value }*/}` ; } else { outputLines [ lineNumber ] = `${ outputLine } /*${ comment . value }*/` ; } } if ( comment . type === 'CommentLine' ) { const lineComment = `//${ comment . value }` ; if ( LINE_COMMENT_AT_BEGINNING_OF_LINE . test ( originalLines [ lineNumber ] ) ) { outputLines . splice ( lineNumber , 0 , lineComment ) ; } else { outputLines [ lineNumber ] = `${ outputLine } ${ lineComment }` ; } } } return outputLines ; } /**
 * Since Babel uses an *abstract* synax tree, all information about whitespace
 * is lost after parsing. Babel has an option `retainLines`, but it is not
 * working as expected and will produce broken syntax in some cases. Neither
 * will Prettier help here, because it does not add additional blank lines:
 *
 * https://prettier.io/docs/en/rationale.html#empty-lines
 *
 * Therefore, the following function tries to synchronize the blank lines and
 * comments of two source code fragments. This means: insert blank lines in
 * given code wherever the original code has them and copy all comments from
 * the source to the output. Comments are also handled here, because Babel does
 * not reliably retain their position in the generated code. This approach
 * naively assumes that all code transformations will result in (roughly) the
 * same amount of lines. It is not perfect, but the best I came up with and it
 * seems to work reasonably well in practice.
 */ function syncBlankLinesAndComments ( outputCode , originalCode , pluginOptions ) { const originalLines = String ( originalCode ) . split ( LINE_BREAK ) . filter ( line => ! FLOW_DIRECTIVE . test ( line ) ) ; const flowAst = parseAst ( originalLines . join ( '\n' ) ) ; const decoratorList = getDecorators ( flowAst ) ; let outputLines = outputCode . split ( LINE_BREAK ) ; originalLines . forEach ( ( flowLine , lineNumber ) => { if ( outputLines [ lineNumber ] === undefined || flowLine === outputLines [ lineNumber ] ) { return ; } if ( BLANK_LINE . test ( flowLine ) ) { outputLines . splice ( lineNumber , 0 , '' ) ; } outputLines = copyComments ( outputLines , originalLines , flowAst , lineNumber ) ; if ( pluginOptions . replaceDecorators ) { const decorator = decoratorList . find ( decorator => decorator . loc ? decorator . loc . start . line - 1 === lineNumber : false ) ; if ( decorator ) { outputLines . splice ( lineNumber , 0 , '' ) ; } } } ) ; return outputLines . join ( '\n' ) ; } /**
 * Format the code generated by Babel by executing Prettier multiple times,
 * inserting blank lines and copying comments from the original source to the
 * output.
 */ export function formatOutputCode ( outputCode , originalCode , pluginOptions ) { try { const prettierOptions = { printWidth : Infinity , reflow : true , } ; originalCode = prettier . format ( originalCode , getPrettierConfig ( { parser : 'babel' , ... prettierOptions , } ) ) ; outputCode = prettier . format ( outputCode , getPrettierConfig ( { parser : 'typescript' , ... prettierOptions , } ) ) ; outputCode = syncBlankLinesAndComments ( outputCode , originalCode , pluginOptions ) ; outputCode = prettier . format ( outputCode , getPrettierConfig ( ) ) ; return outputCode ; } catch ( error ) { return error as Error ; } }