import * as fs from "fs" ; import * as path from "path" ; import * as ejs from "ejs" ; import { indent , ComposeWithOptions , trailingComma , normalizeProjectInput , Project , RelationTypes , RuntimeLogLevel , RuntimeLogLevels , PluginMetadata , PluginRegistration , RuntimeProps , RuntimeAdapter , Runtime , ProjectBuild , RuntimeAdapterProps , GeneratorProps , Datatypes , PrettifyOptions , RuntimeLogBehaviors , FileOverwriteBehaviors , FileSystemAdapter , RuntimeLogBehavior , FileOverwriteBehavior , ConfigurationValue , } from "@codotype/core" ; import { RuntimeProxyAdapter } from "./utils/runtimeProxyAdapter" ; import { runGenerator } from "./utils/runGenerator" ; import { prettify } from "./utils/prettify" ; import { OUTPUT_DIRECTORY , TEMPLATES_DIRECTORY_NAME , PLUGIN_METADATA_FILENAME , PLUGIN_DISTRIBUTABLE_DIR , } from "./constants" ; import { getPluginPath } from "./utils/getPluginPath" ; import { prepareProjectBuildDestination } from "./utils/prepareProjectBuildDestination" ; import { logger } from "./utils/logger" ; import { getAllFiles } from "./utils/getAllFiles" ; import { handlePluginImportError , handleExecuteImportError , } from "./utils/handleErrors" ; import { getComposeWithModulePath } from "./utils/getComposeWithModulePath" ; interface TemplateData { plugin : PluginMetadata ; project : Project ; configuration : ConfigurationValue ; RelationTypes : typeof RelationTypes ; Datatypes : typeof Datatypes ; helpers : { indent : ( text : string , depth : number ) => string ; trailingComma : ( arr : any [ ] , index : number ) => string ; } ; [ key ] : any ; } /**
 * NodeRuntime
 * Runtime for running Codotype plugins through Node.js
 * NOTE - this is called `NodeRuntime` because we may support a non-Node.js runtime
 * in the future (i.e. DenoRuntime, BrowserRuntime, etc.)
 */ export class NodeRuntime implements Runtime { private plugins : PluginRegistration [ ] ; private options : { cwd : string ; fileSystemAdapter : FileSystemAdapter ; logBehavior : RuntimeLogBehavior ; fileOverwriteBehavior : FileOverwriteBehavior ; } ; param options constructor ( options ) { this . options = { ... options , fileOverwriteBehavior : options . fileOverwriteBehavior || FileOverwriteBehaviors . force , logBehavior : options . logBehavior || RuntimeLogBehaviors . verbose , } ; this . plugins = [ ] ; return this ; } param props registerPlugin ( props ) { const pluginPath = getPluginPath ( { ... props , cwd : this . options . cwd , } ) ; const pluginDynamicImportPath = path . join ( pluginPath , PLUGIN_DISTRIBUTABLE_DIR ) ; const pluginMetadataImportPath = path . join ( pluginPath , PLUGIN_DISTRIBUTABLE_DIR , PLUGIN_METADATA_FILENAME ) ; this . log ( `registerPlugin - pluginPath: ${ pluginPath }` , { level : RuntimeLogLevels . verbose , } ) ; this . log ( `registerPlugin - pluginDynamicImportPath: ${ pluginDynamicImportPath }` , { level : RuntimeLogLevels . verbose } ) ; this . log ( `registerPlugin - pluginMetadataImportPath: ${ pluginMetadataImportPath }` , { level : RuntimeLogLevels . verbose } ) ; try { const pluginDynamicImport = require ( pluginDynamicImportPath ) ; const pluginMetadataImport = require ( pluginMetadataImportPath ) ; const pluginMetadata = { ... pluginMetadataImport . default , } ; if ( pluginDynamicImport && pluginMetadata ) { const newPluginRegistration = { id : pluginMetadata . identifier , pluginPath , pluginMetadata , pluginDynamicImportPath , } ; this . plugins . push ( newPluginRegistration ) ; this . log ( `Registered ${ pluginMetadata . content . label } Codotype Plugin` , { level : RuntimeLogLevels . verbose , } ) ; return Promise . resolve ( newPluginRegistration ) ; } } catch ( err ) { return handlePluginImportError ( err , this . options . logBehavior ) ; } return Promise . resolve ( null ) ; } param dir ensureDir ( dir ) { return this . options . fileSystemAdapter . ensureDir ( dir ) ; } /**
     * getPlugins
     * Returns an array of PluginRegistration instances currently to this runtime instance
     */ getPlugins ( ) { return Promise . resolve ( this . plugins ) ; } param pluginID async findPlugin ( pluginID ) { const plugins = await this . getPlugins ( ) ; const pluginRegistration = plugins . find ( ( g ) => g . id === pluginID ) ; return Promise . resolve ( pluginRegistration ) ; } param props . build async execute ( { build } ) { this . log ( "NodeRuntime - start execute({ build })" , { level : RuntimeLogLevels . verbose , } ) ; let { id , projectInput } = build ; const project = normalizeProjectInput ( { projectInput , } ) ; await prepareProjectBuildDestination ( { build , runtime : this , cwd : this . options . cwd , } ) ; const pluginRegistration = await this . findPlugin ( project . pluginID ) ; if ( pluginRegistration === undefined ) { this . log ( "NodeRuntime.execute - Codotype Plugin not found. Please ensure that Codotype Plugin has been correctly registered with Runtime.registerPlugin" , { level : RuntimeLogLevels . verbose } ) ; return Promise . resolve ( ) ; } const { pluginDynamicImportPath } = pluginRegistration ; const buildOutputDirectory = id || "" ; const destinationPath = path . join ( this . options . cwd , OUTPUT_DIRECTORY , buildOutputDirectory , project . identifiers . snake ) ; try { const generator = require ( pluginDynamicImportPath ) ; const generatorResolvedPath = require . resolve ( pluginDynamicImportPath ) ; const runtimeAdapterProps = { project , destinationPath , generatorResolvedPath , plugin : pluginRegistration . pluginMetadata , runtime : this , } ; this . log ( `Running Plugin Generator: ${ generator . name }` , { level : RuntimeLogLevels . verbose , } ) ; const runtimeProxyAdapter = new RuntimeProxyAdapter ( generator , runtimeAdapterProps ) ; await runGenerator ( { project , runtimeAdapter : runtimeProxyAdapter , } ) ; } catch ( err ) { return handleExecuteImportError ( err , this . options . logBehavior ) ; } this . log ( "\nBuild complete\nThank you for using Codotype :)\nFollow us on github.com/codotype\n" , { level : RuntimeLogLevels . verbose } ) ; return Promise . resolve ( ) ; } param { string } generatorResolved param { string } template_path getTemplatePath ( generatorResolvedPath , templateRelativePath ) { return path . join ( generatorResolvedPath , TEMPLATES_DIRECTORY_NAME , templateRelativePath ) ; } /**
     * getDestinationPath
     * Takes the destination name for a template and Generates
     */ getDestinationPath ( outputDirAbsolutePath , destinationRelativePath ) { return path . join ( outputDirAbsolutePath , destinationRelativePath ) ; } param generatorInstance param src param options renderTemplate ( generatorInstance , src , data ? , options ? ) { return new Promise ( ( resolve , reject ) => { let renderOptions = { } ; const dataObj = data || { } ; const optionsObj = options || { } ; const { plugin , project } = generatorInstance . props ; const templateData = { plugin , project , configuration : generatorInstance . props . project . configuration , helpers : { indent , trailingComma , } , RelationTypes , Datatypes , ... dataObj , } ; return ejs . renderFile ( src , templateData , renderOptions , ( err , str ) => { if ( err ) return reject ( err ) ; if ( optionsObj . prettify !== undefined ) { str = prettify ( { source : str , parser : optionsObj . prettify . parser , semi : optionsObj . prettify . semi , } ) ; } return resolve ( str ) ; } ) ; } ) ; } param destinationFilepath param compiledTemplate async compareFile ( destinationFilepath , compiledTemplate ) { const existingFile = await this . options . fileSystemAdapter . readFile ( destinationFilepath ) ; if ( existingFile === null ) { return Promise . resolve ( false ) ; } return Promise . resolve ( compiledTemplate === existingFile ) ; } param dest param compiledTemplate writeFile ( dest , compiledTemplate , options ? ) { let fileContents = compiledTemplate ; if ( options && options . prettify ) { fileContents = prettify ( { source : compiledTemplate , semi : options . prettify . semi , parser : options . prettify . parser , } ) ; } return this . options . fileSystemAdapter . writeFile ( dest , fileContents ) ; } param src param dest async copyDir ( params ) { const { src , dest } = params ; const allFiles = getAllFiles ( src , [ ] ) ; for ( const i in allFiles ) { const sourcePath = allFiles [ i ] ; let destPath = sourcePath . split ( `/${ TEMPLATES_DIRECTORY_NAME }/` ) . pop ( ) ; if ( destPath === undefined ) { this . log ( "Runtime Error - destination path not found!" , { level : RuntimeLogLevels . error , } ) ; continue ; } destPath = path . resolve ( dest , destPath ) ; const destinationFragments = destPath . split ( "/" ) ; destinationFragments . pop ( ) ; const destDirectoryPath = destinationFragments . join ( "/" ) ; try { const contents = fs . readFileSync ( sourcePath , "utf8" ) ; await this . options . fileSystemAdapter . ensureDir ( destDirectoryPath ) ; await this . options . fileSystemAdapter . writeFile ( destPath , contents ) ; } catch ( e ) { this . log ( `Runtime Error: copyDir file not found: ${ sourcePath }` , { level : RuntimeLogLevels . error , } ) ; continue ; } } return Promise . resolve ( true ) ; } copyFile ( params ) { const { src , dest } = params ; const contents = fs . readFileSync ( src , "utf8" ) ; return this . options . fileSystemAdapter . writeFile ( dest , contents ) ; } param args log ( args , options ) { logger ( args , options , this . options . logBehavior ) ; } param parentRuntimeAdapter param generatorModulePath param options see ComposeWithOptions async composeWith ( parentRuntimeAdapter , generatorModulePath , options = { } ) { this . log ( `Composing Generator: ${ generatorModulePath }` , { level : RuntimeLogLevels . verbose , } ) ; const activePlugin = await this . findPlugin ( parentRuntimeAdapter . props . plugin . identifier ) ; if ( activePlugin === undefined ) { console . log ( "MODULE NOT FOUND" ) ; throw new Error ( "NodeRuntime - composeWith - plugin not found" ) ; } const modulePath = getComposeWithModulePath ( generatorModulePath , parentRuntimeAdapter , activePlugin ) ; try { const generator = require ( modulePath ) ; const resolved = require . resolve ( modulePath ) ; let resolvedGeneratorPath = resolved ; let resolvedGeneratorPathParts = resolvedGeneratorPath . split ( "/" ) ; resolvedGeneratorPathParts . pop ( ) ; resolvedGeneratorPath = resolvedGeneratorPathParts . join ( "/" ) ; let resolvedDestination = parentRuntimeAdapter . props . destinationPath ; if ( options . outputDirectoryScope ) { resolvedDestination = path . resolve ( parentRuntimeAdapter . props . destinationPath , options . outputDirectoryScope ) ; } this . log ( `Runtime.composeWith - resolvedDestination: ${ resolvedDestination }` , { level : RuntimeLogLevels . verbose } ) ; this . log ( `Runtime.composeWith - resolvedGeneratorPath: ${ resolvedGeneratorPath }` , { level : RuntimeLogLevels . verbose } ) ; const project = parentRuntimeAdapter . props . project ; const runtimeProxyAdapter = new RuntimeProxyAdapter ( generator , { ... parentRuntimeAdapter . props , destinationPath : resolvedDestination , generatorResolvedPath : resolvedGeneratorPath , } ) ; await runGenerator ( { project , runtimeAdapter : runtimeProxyAdapter , } ) ; this . log ( `Generated ${ generator . name }.\n` , { level : RuntimeLogLevels . info , } ) ; } catch ( err ) { return handleExecuteImportError ( err , this . options . logBehavior ) ; } } param runtimeAdapter param src param dest param options writeTemplateToFile ( runtimeAdapter , src , dest , data = { } , options = { } ) { return new Promise ( async ( resolve , reject ) => { const compiledTemplate = await this . renderTemplate ( runtimeAdapter , src , data , options ) ; return this . writeFile ( dest , compiledTemplate ) . then ( ( ) => { return resolve ( ) ; } ) ; } ) ; } }