import fs from "fs" ; import glob from "glob" ; import L from "lodash" ; import path from "upath" ; import { ProjectMetaBundle } from "../api" ; import { logger } from "../deps" ; import { HandledError } from "../utils/error" ; import { isLocalModulePath } from "./code-utils" ; import { ComponentConfig , CONFIG_FILE_NAME , PlasmicContext , ProjectConfig , updateConfig , } from "./config-utils" ; import { ensureString } from "./lang-utils" ; import { confirmWithUser } from "./user-utils" ; export function stripExtension ( filename , removeComposedPath = false ) { const ext = removeComposedPath ? filename . substring ( filename . indexOf ( "." ) ) : path . extname ( filename ) ; if ( ! ext || filename === ext ) { return filename ; } return filename . substring ( 0 , filename . lastIndexOf ( ext ) ) ; } export async function writeFileContentRaw ( filePath , content , opts ? ) { opts = opts || { } ; if ( existsBuffered ( filePath ) && ! opts . force ) { const overwrite = await confirmWithUser ( `File ${ filePath } already exists. Do you want to overwrite?` , opts . yes ) ; if ( ! overwrite ) { throw new HandledError ( `Cannot write to ${ filePath }; file already exists.` ) ; } } fs . mkdirSync ( path . dirname ( filePath ) , { recursive : true } ) ; writeFileText ( filePath , content ) ; } export function defaultResourcePath ( context , project , ... subpaths ) { const projectName = L . isString ( project ) ? project : project . projectName ; return path . join ( context . config . defaultPlasmicDir , L . snakeCase ( projectName ) , ... subpaths ) ; } export function defaultPublicResourcePath ( context , project , ... subpaths ) { return path . join ( context . config . images . publicDir , "plasmic" , L . snakeCase ( project . projectName ) , ... subpaths ) ; } export function defaultPagePath ( context , fileName ) { if ( context . config . platform === "nextjs" ) { return path . join ( context . config . nextjsConfig ?. pagesDir || "" , fileName ) ; } if ( context . config . platform === "gatsby" ) { return path . join ( context . config . gatsbyConfig ?. pagesDir || "" , fileName ) ; } return fileName ; } export async function writeFileContent ( context , srcDirFilePath , content , opts = { } ) { const path = makeFilePath ( context , srcDirFilePath ) ; await writeFileContentRaw ( path , content , { yes : context . cliArgs . yes , ... opts , } ) ; } export function readFileContent ( context , srcDirFilePath ) { const path = makeFilePath ( context , srcDirFilePath ) ; return readFileText ( path ) ; } export function deleteFile ( context , srcDirFilePath ) { const path = makeFilePath ( context , srcDirFilePath ) ; deleteFileBuffered ( path ) ; } export function fileExists ( context , srcDirFilePath ) { return existsBuffered ( makeFilePath ( context , srcDirFilePath ) ) ; } export function makeFilePath ( context , filePath ) { return path . isAbsolute ( filePath ) ? filePath : path . join ( context . absoluteSrcDir , filePath ) ; } export function renameFile ( context , oldPath , newPath ) { renameFileBuffered ( makeFilePath ( context , oldPath ) , makeFilePath ( context , newPath ) ) ; } param { PlasmicContext } context returns { Record < string , string [ ] > } export function buildBaseNameToFiles ( context ) { const srcDir = context . absoluteSrcDir ; const scriptFileExt = context . config . code . lang === "ts" ? "tsx" : "jsx" ; const allFiles = glob . sync ( `${ srcDir }/**/*.+(ts|css|${ scriptFileExt }|json)` , { ignore : [ `${ srcDir }/**/node_modules/**/*` ] , } ) ; return L . groupBy ( allFiles , ( f ) => path . basename ( f ) ) ; } /**
 * Tries to find the file at `srcDir/expectedPath`.  If it's not there, tries to detect if it has
 * been moved to a different location.  Returns the found location relative to the `srcDir`.
 *
 * If `expectedPath` doesn't exist, but there's more than one file of that name in `baseNameToFiles`, then
 * error and quit.  If no file of that name can be found, `expectedPath` is returned.
 */ export function findSrcDirPath ( absoluteSrcDir , expectedPath , baseNameToFiles ) { if ( ! path . isAbsolute ( absoluteSrcDir ) ) { throw new HandledError ( "Cannot find srcDir. Please check plasmic.json." ) ; } const fileName = path . basename ( expectedPath ) ; if ( existsBuffered ( path . join ( absoluteSrcDir , expectedPath ) ) ) { return expectedPath ; } else if ( ! ( fileName in baseNameToFiles ) ) { return expectedPath ; } else if ( baseNameToFiles [ fileName ] . length === 1 ) { const newPath = path . relative ( absoluteSrcDir , baseNameToFiles [ fileName ] [ 0 ] ) ; logger . info ( `\tDetected file moved from ${ expectedPath } to ${ newPath }` ) ; return newPath ; } else { throw new HandledError ( `Cannot find expected file at ${ expectedPath }, and found multiple possible matching files ${ baseNameToFiles [ fileName ] }.  Please update plasmic.config with the real location for ${ fileName }.` ) ; } } /**
 * Finds the full path to the first file satisfying `pred` in `dir`.  If
 * `opts.traverseParents` is set to true, then will also look in ancestor
 * directories until the plasmic.json file is found.  If none is found,
 * returns undefined.
 */ export function findFile ( dir , pred , opts ) { const files = fs . readdirSync ( dir ) ; const found = files . find ( ( f ) => pred ( f ) ) ; if ( found ) { return path . join ( dir , found ) ; } if ( ! opts . traverseParents ) { return undefined ; } const parent = path . dirname ( dir ) ; if ( parent === dir ) { return undefined ; } return findFile ( path . dirname ( dir ) , pred , opts ) ; } type BundleKeyPair = { bundle : Record < string , any > ; key : string ; } ; /**
 * Parses a configuration and returns file/dir paths in it (in the format
 * BundleKeyPair to allow the caller to change these paths).
 */ function getAllPaths ( context ) { const config = context . config ; const pairs = [ ] ; const pushPath = ( bundle , key ) => { pairs . push ( { bundle , key } ) ; } ; const pushComponent = ( comp ) => { pushPath ( comp , "renderModuleFilePath" ) ; pushPath ( comp , "cssFilePath" ) ; if ( isLocalModulePath ( comp . importSpec . modulePath ) ) { pushPath ( comp . importSpec , "modulePath" ) ; } } ; const pushProject = ( proj ) => { pushPath ( proj , "cssFilePath" ) ; for ( const component of proj . components ) { pushComponent ( component ) ; } for ( const icon of proj . icons ) { pushPath ( icon , "moduleFilePath" ) ; } for ( const image of proj . images ) { pushPath ( image , "filePath" ) ; } } ; for ( const project of config . projects ) { pushProject ( project ) ; } for ( const bundle of config . globalVariants . variantGroups ) { pushPath ( bundle , "contextFilePath" ) ; } pushPath ( config . tokens , "tokensFilePath" ) ; pushPath ( config . style , "defaultStyleCssFilePath" ) ; pushPath ( config , "defaultPlasmicDir" ) ; if ( config . images . publicDir ) { pushPath ( config . images , "publicDir" ) ; } if ( config . gatsbyConfig ) { pushPath ( config . gatsbyConfig , "pagesDir" ) ; } if ( config . nextjsConfig ) { pushPath ( config . nextjsConfig , "pagesDir" ) ; } return pairs ; } /**
 * Fixes all src-relative file paths in PlasmicConfig by detecting file
 * movement on disk.
 */ export async function fixAllFilePaths ( context , baseDir ) { const baseNameToFiles = buildBaseNameToFiles ( context ) ; let changed = false ; const paths = getAllPaths ( context ) ; for ( const { bundle , key } of paths ) { const known = bundle [ key ] ; if ( known == null ) { throw new HandledError ( `"${ key } is required, but missing in ${ CONFIG_FILE_NAME }. Please restore the file or delete from ${ CONFIG_FILE_NAME } and run plasmic sync: ${ bundle }"` ) ; } if ( ! known ) { continue ; } const found = findSrcDirPath ( context . absoluteSrcDir , known , baseNameToFiles ) ; if ( known !== found ) { bundle [ key ] = found ; changed = true ; } } if ( changed ) { await updateConfig ( context , context . config , baseDir ) ; } } /**
 * Throws an error if some file in PlasmicConfig is not inside the root
 * directory (i.e., the directory containing plasmic.json).
 */ export function assertAllPathsInRootDir ( context ) { if ( process . env . PLASMIC_LOADER ) { return ; } if ( ! context . absoluteSrcDir . startsWith ( context . rootDir ) ) { throw new HandledError ( `"srcDir" in ${ CONFIG_FILE_NAME } is outside of ${ context . rootDir }` ) ; } const paths = getAllPaths ( context ) ; for ( const { bundle , key } of paths ) { const relPath = bundle [ key ] ; if ( ! relPath ) { continue ; } const absPath = path . resolve ( context . absoluteSrcDir , relPath ) ; if ( ! absPath . startsWith ( context . rootDir ) ) { throw new HandledError ( `The path "${ relPath }" in ${ CONFIG_FILE_NAME } is outside of ${ context . rootDir }` ) ; } } } /** Whether we're currently recording to the buffer. */ let buffering = false ; interface BufferCreateFile { type : "create" ; content : string | Buffer ; } interface BufferRenameFile { type : "rename" ; newPath : string ; } interface BufferDeleteFile { type : "delete" ; } /** List of buffer actions. */ const buffer = new Map < string , BufferCreateFile | BufferRenameFile | BufferDeleteFile > ( ) ; const renamedFiles = new Map < string , string > ( ) ; /**
 * This turns on buffering of file writes/reads.
 *
 * This is useful for reducing the extent to which our file updates are scattered over time, which can cause webpack
 * dev server to trip up.
 *
 * This also has the side benefit of making our CLI commands more atomic, in case of failure partway through a sync.
 */ export async function withBufferedFs ( f ) { buffering = true ; buffer . clear ( ) ; renamedFiles . clear ( ) ; try { await f ( ) ; for ( const [ filePath , action ] of buffer . entries ( ) ) { switch ( action . type ) { case "create" : fs . mkdirSync ( path . dirname ( filePath ) , { recursive : true } ) ; fs . writeFileSync ( filePath , action . content ) ; break ; case "rename" : fs . mkdirSync ( path . dirname ( action . newPath ) , { recursive : true } ) ; fs . renameSync ( filePath , action . newPath ) ; break ; case "delete" : fs . unlinkSync ( filePath ) ; break ; } } } finally { buffering = false ; } } export function writeFileText ( path , content ) { if ( buffering ) { buffer . set ( path , { type : "create" , content } ) ; } else { fs . writeFileSync ( path , content , "utf8" ) ; } } export function readFileText ( path ) { if ( buffering ) { const action = buffer . get ( path ) ; if ( action ) { switch ( action . type ) { case "create" : return ensureString ( action . content ) ; case "rename" : return readFileText ( action . newPath ) ; case "delete" : throw new HandledError ( "File does not exists" ) ; } } } return fs . readFileSync ( path , "utf8" ) ; } export function renameFileBuffered ( oldPath , newPath ) { if ( oldPath === newPath ) { return ; } if ( buffering ) { if ( ! existsBuffered ( oldPath ) ) { throw new HandledError ( "File does not exists" ) ; } const action = buffer . get ( oldPath ) ; if ( action ) { switch ( action . type ) { case "create" : buffer . set ( newPath , action ) ; buffer . delete ( oldPath ) ; break ; case "rename" : throw new HandledError ( "File does not exists" ) ; case "delete" : throw new HandledError ( "File does not exists" ) ; } } const renamedFile = renamedFiles . get ( oldPath ) ; if ( renamedFile !== undefined ) { oldPath = renamedFile ; } buffer . set ( oldPath , { type : "rename" , newPath } ) ; renamedFiles . set ( newPath , oldPath ) ; } else { fs . mkdirSync ( path . dirname ( newPath ) , { recursive : true } ) ; fs . renameSync ( oldPath , newPath ) ; } } export function deleteFileBuffered ( path ) { if ( buffering ) { if ( ! existsBuffered ( path ) ) { throw new HandledError ( "File does not exists" ) ; } const action = buffer . get ( path ) ; if ( action ) { switch ( action . type ) { case "create" : buffer . delete ( path ) ; break ; case "rename" : throw new HandledError ( "File does not exists" ) ; case "delete" : throw new HandledError ( "File does not exists" ) ; } } else { buffer . set ( path , { type : "delete" } ) ; } renamedFiles . delete ( path ) ; } else { fs . unlinkSync ( path ) ; } } export function existsBuffered ( path ) { if ( buffering ) { if ( renamedFiles . has ( path ) ) { return true ; } const action = buffer . get ( path ) ; if ( action ) { switch ( action . type ) { case "create" : return true ; case "rename" : return false ; case "delete" : return false ; } } } return fs . existsSync ( path ) ; }