import * as babel from "@babel/core" ; import generate from "@babel/generator" ; import * as parser from "@babel/parser" ; import traverse , { Node } from "@babel/traverse" ; import { ImportDeclaration } from "@babel/types" ; import L from "lodash" ; import * as Prettier from "prettier" ; import { Options , resolveConfig } from "prettier" ; import * as ts from "typescript" ; import path from "upath" ; import { fixComponentCssReferences , fixComponentImagesReferences , } from "../actions/sync-images" ; import { logger } from "../deps" ; import { HandledError } from "../utils/error" ; import { CodeComponentConfig , ComponentConfig , GlobalVariantGroupConfig , IconConfig , ImageConfig , PlasmicConfig , PlasmicContext , ProjectConfig , } from "./config-utils" ; import { existsBuffered , makeFilePath , readFileText , stripExtension , writeFileContent , } from "./file-utils" ; import { assert , flatMap } from "./lang-utils" ; export const formatAsLocal = ( content , filePath , baseDir , defaultOpts = { } ) => { const opts = resolveConfig . sync ( baseDir ) || defaultOpts ; opts . filepath = filePath ; const res = Prettier . format ( content , opts ) ; const res2 = Prettier . format ( res , opts ) ; return res2 ; } ; const nodeToFormattedCode = ( n , baseDir , unformatted ? , commentsToRemove ? ) => { const c = generate ( n , { retainLines : true , shouldPrintComment : ( c ) => ! commentsToRemove || ! commentsToRemove . has ( c ) , } ) . code ; return unformatted ? c : formatAsLocal ( c , "/tmp/x.tsx" , baseDir , { trailingComma : "none" , arrowParens : "avoid" , } ) ; } ; function findImportSpecifierWithAlias ( importDecl , local ) { for ( const spec of importDecl . specifiers ) { if ( spec . type === "ImportSpecifier" && spec . local . name === local ) { return spec ; } } return undefined ; } function findImportDefaultSpecifier ( importDecl ) { for ( const spec of importDecl . specifiers ) { if ( spec . type === "ImportDefaultSpecifier" ) { return spec ; } } return undefined ; } export function ensureImportSpecifierWithAlias ( decl , imported , alias ) { const existing = findImportSpecifierWithAlias ( decl , alias ) ; if ( existing ) { if ( existing . imported . type === "Identifier" ) { existing . imported . name = imported ; } else { existing . imported . value = imported ; } } else { decl . specifiers = decl . specifiers . filter ( ( specifier ) => { if ( specifier . type === "ImportDefaultSpecifier" && specifier . local . name === alias ) { return false ; } return true ; } ) ; decl . specifiers . push ( babel . types . importSpecifier ( babel . types . identifier ( alias ) , babel . types . identifier ( imported ) ) ) ; } } export function ensureImportDefaultSpecifier ( decl , defaultExport ) { const existing = findImportDefaultSpecifier ( decl ) ; if ( existing ) { existing . local . name = defaultExport ; } else { decl . specifiers . splice ( 0 , 0 , babel . types . importDefaultSpecifier ( babel . types . identifier ( defaultExport ) ) ) ; } } interface PlasmicImportSpec { id : string ; type : PlasmicImportType ; node : ImportDeclaration ; } type PlasmicImportType = "render" | "css" | "component" | "globalVariant" | "projectcss" | "defaultcss" | "icon" | "picture" | "jsBundle" | "codeComponent" | undefined ; function tryParsePlasmicImportSpec ( node ) { const c = node . trailingComments ?. [ 0 ] ; if ( ! c ) { return undefined ; } const m = c . value . match ( /plasmic-import:\s+([\w-]+)(?:\/(component|css|render|globalVariant|projectcss|defaultcss|icon|picture|jsBundle|codeComponent))?/ ) ; if ( m ) { return { id : m [ 1 ] , type : m [ 2 ] as PlasmicImportType } as PlasmicImportSpec ; } return undefined ; } function filterUnformattedMarker ( code , changed ) { const lines = code . split ( "\n" ) ; const isUnformattedMarker = ( line ) => line . trim ( ) === "// plasmic-unformatted" ; changed = changed || lines . some ( isUnformattedMarker ) ; code = lines . filter ( ( line ) => ! isUnformattedMarker ( line ) ) . join ( "\n" ) ; return [ code , changed ] as const ; } /**
 * Given the argument `code` string, for module at `fromPath`, replaces all Plasmic imports
 * for modules found in `compConfigsMap`.
 */ export function replaceImports ( context , code , fromPath , fixImportContext , removeImportDirective , baseDir , changed = false ) { [ code , changed ] = filterUnformattedMarker ( code , changed ) ; const file = parser . parse ( code , { strictMode : true , sourceType : "module" , plugins : [ "jsx" , "typescript" , "classProperties" , ] , } ) ; const commentsToRemove = new Set < string > ( ) ; file . program . body . forEach ( ( stmt ) => { if ( stmt . type !== "ImportDeclaration" ) { return ; } const importStmt = stmt ; const spec = tryParsePlasmicImportSpec ( importStmt ) ; if ( ! spec ) { return ; } changed = true ; if ( removeImportDirective ) { commentsToRemove . add ( stmt . trailingComments ?. [ 0 ] . value || "" ) ; } const type = spec . type ; const uuid = spec . id ; if ( type === "component" ) { const compConfig = fixImportContext . components [ uuid ] ; if ( ! compConfig ) { throwMissingReference ( context , "component" , uuid , fromPath ) ; } const { modulePath , exportName } = compConfig . importSpec ; if ( exportName ) { ensureImportSpecifierWithAlias ( stmt , exportName , compConfig . name ) ; } else { } const realPath = makeImportPath ( context , fromPath , modulePath , true ) ; stmt . source . value = realPath ; } else if ( type === "render" ) { const compConfig = fixImportContext . components [ uuid ] ; if ( ! compConfig ) { throwMissingReference ( context , "component" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , compConfig . renderModuleFilePath , true ) ; stmt . source . value = realPath ; } else if ( type === "css" ) { const compConfig = fixImportContext . components [ uuid ] ; if ( ! compConfig ) { throwMissingReference ( context , "component" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , compConfig . cssFilePath , false ) ; stmt . source . value = realPath ; } else if ( type === "globalVariant" ) { const variantConfig = fixImportContext . globalVariants [ uuid ] ; if ( ! variantConfig ) { throwMissingReference ( context , "global variant" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , variantConfig . contextFilePath , true ) ; stmt . source . value = realPath ; } else if ( type === "icon" ) { const iconConfig = fixImportContext . icons [ uuid ] ; if ( ! iconConfig ) { throwMissingReference ( context , "icon" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , iconConfig . moduleFilePath , true ) ; stmt . source . value = realPath ; } else if ( type === "picture" ) { const imageConfig = fixImportContext . images [ uuid ] ; if ( ! imageConfig ) { throwMissingReference ( context , "image" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , imageConfig . filePath , false ) ; stmt . source . value = realPath ; } else if ( type === "projectcss" ) { const projectConfig = fixImportContext . projects [ uuid ] ; if ( ! projectConfig ) { throwMissingReference ( context , "project" , uuid , fromPath ) ; } const realPath = makeImportPath ( context , fromPath , projectConfig . cssFilePath , false ) ; stmt . source . value = realPath ; } else if ( type === "defaultcss" ) { const realPath = makeImportPath ( context , fromPath , fixImportContext . config . style . defaultStyleCssFilePath , false ) ; stmt . source . value = realPath ; } else if ( type === "codeComponent" ) { const meta = fixImportContext . codeComponentMetas [ uuid ] ; if ( meta . componentImportPath [ 0 ] === "." ) { const toPath = path . join ( process . cwd ( ) , meta . componentImportPath ) ; assert ( path . isAbsolute ( toPath ) ) ; const realPath = makeImportPath ( context , fromPath , toPath , true , true ) ; stmt . source . value = realPath ; } else { stmt . source . value = meta . componentImportPath ; } } } ) ; if ( ! changed ) { return code ; } return nodeToFormattedCode ( file , baseDir , ! changed , commentsToRemove ) ; } function throwMissingReference ( context , itemType , uuid , fromPath ) { let recFix = `Please make sure projects that "${ uuid }" depends on have already been synced.` ; if ( context . cliArgs . nonRecursive ) { recFix = `Please run "plasmic sync" without the --non-recursive flag to sync dependencies.` ; } else if ( ! context . cliArgs . force ) { recFix = `Please run "plasmic sync" with --force flag to force-sync the dependencies.` ; } throw new HandledError ( `Encountered Plasmic ${ itemType } "${ uuid }" in ${ fromPath } that are being used but have not been synced. ${ recFix }` ) ; } function makeImportPath ( context , fromPath , toPath , stripExt , forceRelative = false ) { let result = toPath ; if ( forceRelative || isLocalModulePath ( toPath ) ) { result = path . relative ( makeFilePath ( context , path . dirname ( fromPath ) ) , makeFilePath ( context , toPath ) ) ; if ( ! result . startsWith ( "." ) ) { result = `./${ result }` ; } } if ( stripExt ) { result = stripExtension ( result ) ; } return result ; } /**
 * Returns true if the argument ComponentConfig.importSpec.modulePath is referencing
 * a local file
 */ export function isLocalModulePath ( modulePath ) { return ! ! path . extname ( modulePath ) ; } export interface ComponentUpdateSummary { skeletonModuleModified : boolean ; } export interface FixImportContext { config : PlasmicConfig ; components : Record < string , ComponentConfig > ; codeComponentMetas : Record < string , CodeComponentConfig > ; globalVariants : Record < string , GlobalVariantGroupConfig > ; icons : Record < string , IconConfig > ; images : Record < string , ImageConfig > ; projects : Record < string , ProjectConfig > ; } export const mkFixImportContext = ( config ) => { const allComponents = flatMap ( config . projects , ( p ) => p . components ) ; const components = L . keyBy ( allComponents , ( c ) => c . id ) ; const allCodeComponents = flatMap ( config . projects , ( p ) => p . codeComponents || [ ] ) ; const codeComponentMetas = L . keyBy ( allCodeComponents , ( c ) => c . id ) ; const globalVariants = L . keyBy ( config . globalVariants . variantGroups , ( c ) => c . id ) ; const icons = L . keyBy ( flatMap ( config . projects , ( p ) => p . icons ) , ( c ) => c . id ) ; const images = L . keyBy ( flatMap ( config . projects , ( p ) => p . images ) , ( c ) => c . id ) ; const projects = L . keyBy ( config . projects , ( p ) => p . projectId ) ; return { config , components , codeComponentMetas , globalVariants , icons , images , projects , } ; } ; /**
 * Assuming that all the files referenced in PlasmicConfig are correct, fixes import statements using PlasmicConfig
 * file locations as the source of truth.
 */ export async function fixAllImportStatements ( context , baseDir , summary ? ) { logger . info ( "Fixing import statements..." ) ; const config = context . config ; const fixImportContext = mkFixImportContext ( config ) ; for ( const project of config . projects ) { for ( const compConfig of project . components ) { const compSummary = summary ?. get ( compConfig . id ) ; if ( summary && ! compSummary ) { continue ; } const fixSkeletonModule = compSummary ? compSummary . skeletonModuleModified : true ; if ( ! summary || compSummary ) { await fixComponentImportStatements ( context , compConfig , fixImportContext , fixSkeletonModule , baseDir ) ; } } } } async function fixComponentImportStatements ( context , compConfig , fixImportContext , fixSkeletonModule , baseDir ) { if ( isLocalModulePath ( compConfig . importSpec . modulePath ) && fixSkeletonModule ) { await fixFileImportStatements ( context , compConfig . importSpec . modulePath , fixImportContext , true , baseDir ) ; } let renderModuleChanged = false ; if ( [ "files" , "public-files" ] . includes ( context . config . images . scheme ) ) { await fixComponentCssReferences ( context , fixImportContext , compConfig . cssFilePath ) ; if ( context . config . images . scheme === "public-files" ) { renderModuleChanged = await fixComponentImagesReferences ( context , fixImportContext , compConfig . renderModuleFilePath ) ; } } await fixFileImportStatements ( context , compConfig . renderModuleFilePath , fixImportContext , false , baseDir , renderModuleChanged ) ; } async function fixFileImportStatements ( context , srcDirFilePath , fixImportContext , removeImportDirective , baseDir , fileHasChanged = false ) { const filePath = makeFilePath ( context , srcDirFilePath ) ; if ( ! existsBuffered ( filePath ) ) { logger . warn ( `Cannot fix imports in non-existing file ${ srcDirFilePath }. Check your plasmic.json file for invalid entries.` ) ; return ; } const prevContent = readFileText ( filePath ) . toString ( ) ; const newContent = replaceImports ( context , prevContent , srcDirFilePath , fixImportContext , removeImportDirective , baseDir , fileHasChanged ) ; if ( prevContent !== newContent ) { await writeFileContent ( context , srcDirFilePath , newContent , { force : true , } ) ; } } class CompilerOptions { private static opts : ts . CompilerOptions | undefined = undefined ; static getOpts ( ) { if ( ! this . opts ) { let curDir = __dirname ; let configPath = "" ; do { curDir = path . join ( curDir , ".." ) ; configPath = path . join ( curDir , "tsconfig-transform.json" ) ; } while ( ! existsBuffered ( configPath ) ) ; const c = ts . readConfigFile ( configPath , ( path ) => readFileText ( path ) ) ; this . opts = ts . convertCompilerOptionsFromJson ( c . config . compilerOptions , "." ) . options ; } return this . opts ; } } export const tsxToJsx = ( code ) => { const usageMagic = "\ncreatePlasmicElementProxy();" ; const jsxPragmas = [ "jsx" , "jsxFrag" , "jsxRuntime" , "jsxImportSource" ] ; function prepForTranspile ( str ) { for ( const p of jsxPragmas ) { str = str . replace ( `/** @${ p } ` , `/** @ ${ p } ` ) ; } return str + usageMagic ; } function fixPostTranspile ( str ) { for ( const p of jsxPragmas ) { str = str . replace ( `/** @ ${ p } ` , `/** @${ p } ` ) ; } str = str . replace ( usageMagic , "" ) ; return str ; } let result = ts . transpileModule ( prepForTranspile ( code ) , { compilerOptions : CompilerOptions . getOpts ( ) , } ) ; return fixPostTranspile ( result . outputText ) ; } ; export function maybeConvertTsxToJsx ( fileName , content , baseDir ) { if ( fileName . endsWith ( "tsx" ) ) { const jsFileName = stripExtension ( fileName ) + ".jsx" ; const jsContent = formatScript ( tsxToJsx ( content ) , baseDir ) ; return [ jsFileName , jsContent ] ; } return [ fileName , content ] ; } export const formatScript = ( code , baseDir ) => { const file = parser . parse ( code , { strictMode : true , sourceType : "module" , plugins : [ "jsx" , "typescript" ] , } ) ; let newLineMarker = "THIS_SHALL_BE_NEW_LINE" ; while ( code . includes ( newLineMarker ) ) { newLineMarker = newLineMarker + "_REALLY" ; } traverse ( file , { Statement : function ( path ) { if ( file . program . body . includes ( path . node ) && path . node . type !== "ImportDeclaration" ) { path . insertBefore ( babel . types . stringLiteral ( newLineMarker ) ) ; path . skip ( ) ; } } , } ) ; const withmarkers = nodeToFormattedCode ( file , baseDir , true ) ; const withNewLines = withmarkers . replace ( new RegExp ( `"${ newLineMarker }"` , "g" ) , "\n" ) ; return formatAsLocal ( withNewLines , "/tmp/x.tsx" , baseDir , { printWidth : 80 , tabWidth : 2 , useTabs : false , } ) ; } ;