import { promises as fs } from "fs" ; import rmfr from "rmfr" ; import path from "upath" ; import * as cli from "./cli" ; import * as gen from "./gen" ; import * as logger from "./logger" ; import { setMetadata } from "./metadata" ; import { captureException , initSentry } from "./sentry" ; import * as substitutions from "./substitutions" ; import type { PlasmicLoaderConfig , PlasmicOpts } from "./types" ; import { spawn } from "./utils" ; import { PlasmicOptsSchema } from "./validation" ; const ABOUT_PAGE_URL = "https://docs.plasmic.app/learn/loader-config" ; type onRegisterPages = ( pages : { name : string ; projectId : string ; path : string ; url : string ; } [ ] , config : any ) => Promise < void > ; export async function watchForChanges ( opts , onRegisterPages ? ) { const { plasmicDir , pageDir } = opts ; let currentConfig = await cli . readConfig ( plasmicDir ) ; await initSentry ( opts ) ; setMetadata ( { source : "loader" , scheme : "loader" , } ) ; const userCli = require ( path . join ( plasmicDir , "node_modules" , "@plasmicapp" , "cli" , "dist" , "lib.js" ) ) ; spawn ( userCli . watchProjects ( { yes : true , projects : opts . projects . map ( ( { projectId } ) => projectId ) , baseDir : plasmicDir , } , { } , async function ( ) { await gen . generateAll ( { dir : plasmicDir , pageDir } ) ; currentConfig = await cli . readConfig ( plasmicDir ) ; if ( onRegisterPages ) { await onRegisterPages ( cli . getPagesFromConfig ( plasmicDir , currentConfig ) , currentConfig ) . catch ( ( e ) => logger . crash ( e . message , e ) ) ; } } ) ) ; } export async function ensurePlasmicIsNotStale ( plasmicDir , opts ) { const savedConfigPath = path . join ( plasmicDir , ".plasmic-loader-config.json" ) ; const currentConfig = JSON . stringify ( { ... opts , watch : false } ) ; const savedConfig = await fs . readFile ( savedConfigPath ) . then ( ( file ) => file . toString ( ) ) . catch ( ( error ) => { if ( error . code === "ENOENT" ) { return undefined ; } throw error ; } ) ; if ( savedConfig === currentConfig ) { return ; } if ( savedConfig !== undefined ) { logger . info ( "Detected a change in the previous config. Deleting .plasmic directory..." ) ; } await rmfr ( plasmicDir ) . catch ( ( error ) => { if ( error . code !== "ENOTEMPTY" && error . code !== "EBUSY" ) { throw error ; } logger . warn ( `Unable to clear ${ opts . plasmicDir }. Error: ${ error . message }\n` + "If you made changes to your plasmic configuration, please delete this directory." ) ; } ) ; await fs . mkdir ( plasmicDir , { recursive : true } ) ; await fs . writeFile ( savedConfigPath , currentConfig ) ; } function readLoaderConfig ( configPath ) { return fs . readFile ( configPath ) . then ( ( file ) => JSON . parse ( file . toString ( ) ) ) . catch ( ( error ) => { if ( error . code === "ENOENT" ) { return { } ; } throw error ; } ) ; } export async function convertOptsToLoaderConfig ( userOpts , defaultOpts ) { await PlasmicOptsSchema . validateAsync ( { ... defaultOpts , ... userOpts } ) . catch ( ( error ) => logger . crash ( error . message ) ) ; const configPath = path . join ( userOpts . dir || defaultOpts . dir , "plasmic-loader.json" ) ; const currentConfig = await readLoaderConfig ( configPath ) ; const currentProjects = Object . fromEntries ( ( currentConfig . projects || [ ] ) . map ( ( project ) => [ project . projectId , project , ] ) ) ; Object . assign ( currentConfig , { ... userOpts , projects : [ ] } ) ; userOpts . projects ?. filter ( ( projectId ) => ! currentProjects [ projectId ] ) . forEach ( ( projectId ) => ( currentProjects [ projectId ] = { projectId } ) ) ; currentConfig . projects = Object . values ( currentProjects ) ; delete currentConfig . aboutThisFile ; await fs . writeFile ( configPath , JSON . stringify ( { aboutThisFile : ABOUT_PAGE_URL , ... currentConfig , $schema : "./node_modules/@plasmicapp/loader/plasmic-loader.schema.json" , } , undefined , 2 ) ) ; const config = { aboutThisFile : currentConfig . aboutThisFile , projects : currentConfig . projects , watch : currentConfig . watch || defaultOpts . watch , dir : userOpts . dir || currentConfig . dir || defaultOpts . dir , plasmicDir : userOpts . plasmicDir || currentConfig . plasmicDir || defaultOpts . plasmicDir , pageDir : userOpts . pageDir || currentConfig . pageDir || defaultOpts . pageDir , initArgs : { ... ( defaultOpts . initArgs || { } ) , ... ( currentConfig . initArgs || { } ) , ... ( userOpts . initArgs || { } ) , } , substitutions : userOpts . substitutions || currentConfig . substitutions || defaultOpts . substitutions , } ; await ensurePlasmicIsNotStale ( config . plasmicDir , userOpts ) ; return config ; } async function checkNoTopLevelPlasmicJson ( config ) { try { await fs . access ( path . join ( config . dir , "plasmic.json" ) ) ; logger . crash ( 'Top-level "plasmic.json" detected. It looks like you may be using Plasmic Codegen. Both codegen and loader are two ways in which you can consume your Plasmic designs. To prevent confusion, you cannot use both approaches.' + "\n\nRead more: https://docs.plasmic.app/learn/loader-vs-codegen/." ) ; } catch ( error ) { if ( error . code === "ENOENT" ) { return ; } throw error ; } } export async function initLoader ( config ) { await initSentry ( config ) ; const { dir , pageDir , plasmicDir , initArgs = { } } = config ; await checkNoTopLevelPlasmicJson ( config ) ; logger . info ( "Checking that your loader version is up to date." ) ; await cli . ensureRequiredLoaderVersion ( ) ; const projectIds = config . projects . map ( ( { projectId } ) => projectId ) ; logger . info ( `Syncing plasmic projects: ${ projectIds . join ( ", " ) }` ) ; await cli . tryInitializePlasmicDir ( plasmicDir , initArgs ) ; await cli . syncProject ( plasmicDir , dir , projectIds ) ; if ( config . substitutions ) { logger . info ( "Registering substitutions..." ) ; const cliConfig = await cli . readConfig ( plasmicDir ) ; substitutions . registerSubstitutions ( plasmicDir , cliConfig , config . substitutions ) ; await cli . saveConfig ( plasmicDir , cliConfig ) ; await cli . fixImports ( plasmicDir ) ; } logger . info ( "Generating loader..." ) ; return Promise . all ( [ gen . genStaticNodeModuleExports ( plasmicDir ) , gen . generateAll ( { dir : plasmicDir , pageDir } ) , ] ) ; } export async function onPostInit ( config , watch , onRegisterPages ? ) { if ( onRegisterPages ) { const userConfig = await cli . readConfig ( config . plasmicDir ) ; await onRegisterPages ( cli . getPagesFromConfig ( config . plasmicDir , userConfig ) , config ) . catch ( ( e ) => logger . crash ( e . message , e ) ) ; } if ( watch ) { await watchForChanges ( config , onRegisterPages ) ; } } export async function maybeAddToGitIgnore ( gitIgnorePath , name ) { const file = await fs . readFile ( gitIgnorePath ) . then ( ( content ) => content . toString ( ) ) . catch ( async ( error ) => { if ( error . code === "ENOENT" ) { logger . warn ( ".gitignore not found. Plasmic loader creates multiple files that are not meant to be checked into your repository. To silence this warning, please add a .gitignore." ) ; return ; } await captureException ( error ) ; logger . warn ( `Found error while trying to read .gitignore:\n\n ${ error . message }\n\nPlease add "${ name }" to your .gitignore.` ) ; return "" ; } ) ; if ( ! file || file . includes ( name ) ) return ; await fs . writeFile ( gitIgnorePath , `${ file }\n# Plasmic loader code\n${ name }\n` ) ; logger . info ( `Added "${ name }" to your .gitignore.` ) ; }