@@ -23,6 +23,8 @@ import dtFormat from "date-fns/format"
2323import dtParseISO from "date-fns/parseISO"
2424import { glob , hasMagic } from "glob"
2525import { OptionsBase } from "massarg/types"
26+ import { spawn } from "node:child_process"
27+ import os from "node:os"
2628
2729const dateFns = {
2830 add : dtAdd ,
@@ -99,17 +101,22 @@ export function handleErr(err: NodeJS.ErrnoException | null): void {
99101 if ( err ) throw err
100102}
101103
102- export function log ( config : ScaffoldConfig , level : LogLevel , ...obj : any [ ] ) : void {
104+ /** @internal */
105+ export type LogConfig = Pick < ScaffoldConfig , "quiet" | "verbose" >
106+
107+ export function log ( config : LogConfig , level : LogLevel , ...obj : any [ ] ) : void {
103108 if ( config . quiet || config . verbose === LogLevel . None || level < ( config . verbose ?? LogLevel . Info ) ) {
104109 return
105110 }
111+
106112 const levelColor : Record < LogLevel , keyof typeof chalk > = {
107113 [ LogLevel . None ] : "reset" ,
108114 [ LogLevel . Debug ] : "blue" ,
109115 [ LogLevel . Info ] : "dim" ,
110116 [ LogLevel . Warning ] : "yellow" ,
111117 [ LogLevel . Error ] : "red" ,
112118 }
119+
113120 const chalkFn : any = chalk [ levelColor [ level ] ]
114121 const key : "log" | "warn" | "error" = level === LogLevel . Error ? "error" : level === LogLevel . Warning ? "warn" : "log"
115122 const logFn : any = console [ key ]
@@ -118,8 +125,8 @@ export function log(config: ScaffoldConfig, level: LogLevel, ...obj: any[]): voi
118125 i instanceof Error
119126 ? chalkFn ( i , JSON . stringify ( i , undefined , 1 ) , i . stack )
120127 : typeof i === "object"
121- ? chalkFn ( JSON . stringify ( i , undefined , 1 ) )
122- : chalkFn ( i ) ,
128+ ? chalkFn ( JSON . stringify ( i , undefined , 1 ) )
129+ : chalkFn ( i ) ,
123130 ) ,
124131 )
125132}
@@ -370,16 +377,18 @@ export function logInitStep(config: ScaffoldConfig): void {
370377 name : config . name ,
371378 templates : config . templates ,
372379 output : config . output ,
373- createSubfolder : config . createSubFolder ,
380+ createSubFolder : config . createSubFolder ,
374381 data : config . data ,
375382 overwrite : config . overwrite ,
376383 quiet : config . quiet ,
377- subFolderTransformHelper : config . subFolderNameHelper ,
384+ subFolderNameHelper : config . subFolderNameHelper ,
378385 helpers : Object . keys ( config . helpers ?? { } ) ,
379386 verbose : `${ config . verbose } (${ Object . keys ( LogLevel ) . find (
380387 ( k ) => ( LogLevel [ k as any ] as unknown as number ) === config . verbose ! ,
381388 ) } )`,
382- } )
389+ dryRun : config . dryRun ,
390+ beforeWrite : config . beforeWrite ,
391+ } as Record < keyof ScaffoldConfig , unknown > )
383392 log ( config , LogLevel . Info , "Data:" , config . data )
384393}
385394
@@ -398,21 +407,27 @@ function isWrappedWithQuotes(string: string): boolean {
398407}
399408
400409/** @internal */
401- export function parseConfig ( config : ScaffoldCmdConfig & OptionsBase ) : ScaffoldConfig {
410+ export async function parseConfig ( config : ScaffoldCmdConfig & OptionsBase ) : Promise < ScaffoldConfig > {
402411 let c : ScaffoldConfig = config
403412
404413 if ( config . config ) {
405- const [ configFile , colonTemplate = "default" ] = config . config . split ( ":" )
406- const template = config . key ?? colonTemplate
407- const configImport : ScaffoldConfigFile = require ( path . resolve ( process . cwd ( ) , configFile ) )
408- if ( ! configImport [ template ] ) {
409- throw new Error ( `Template "${ template } " not found in ${ configFile } ` )
414+ const isUrl = config . config . includes ( "://" )
415+
416+ const hasColonToken = ( ! isUrl && config . config . includes ( ":" ) ) || ( isUrl && count ( config . config , ":" ) > 1 )
417+ const colonIndex = config . config . lastIndexOf ( ":" )
418+ const [ configFile , templateKey = "default" ] = hasColonToken
419+ ? [ config . config . substring ( 0 , colonIndex ) , config . config . substring ( colonIndex + 1 ) ]
420+ : [ config . config , undefined ]
421+ const key = ( config . key ?? templateKey ) || "default"
422+ const configImport = await getConfig ( { config : configFile , quiet : config . quiet , verbose : config . verbose } )
423+ if ( ! configImport [ key ] ) {
424+ throw new Error ( `Template "${ key } " not found in ${ configFile } ` )
410425 }
411426 c = {
412427 ...config ,
413- ...configImport [ template ] ,
428+ ...configImport [ key ] ,
414429 data : {
415- ...configImport [ template ] . data ,
430+ ...configImport [ key ] . data ,
416431 ...config . data ,
417432 } ,
418433 }
@@ -422,3 +437,62 @@ export function parseConfig(config: ScaffoldCmdConfig & OptionsBase): ScaffoldCo
422437 delete config . appendData
423438 return c
424439}
440+
441+ /** @internal */
442+ export async function getConfig (
443+ config : Pick < ScaffoldCmdConfig , "quiet" | "verbose" | "config" > ,
444+ ) : Promise < ScaffoldConfigFile > {
445+ const { config : configFile , ...logConfig } = config as Required < typeof config >
446+ const url = new URL ( configFile )
447+
448+ if ( url . protocol === "file:" ) {
449+ log ( logConfig , LogLevel . Info , `Loading config from file ${ configFile } ` )
450+ const absolutePath = path . resolve ( process . cwd ( ) , configFile )
451+ return import ( absolutePath )
452+ }
453+
454+ const isHttp = url . protocol === "http:" || url . protocol === "https:"
455+ const isGit = url . protocol === "git:" || ( isHttp && url . pathname . endsWith ( ".git" ) )
456+
457+ if ( isHttp || isGit ) {
458+ if ( isGit ) {
459+ const repoUrl = `${ url . protocol } //${ url . host } ${ url . pathname } `
460+ log ( logConfig , LogLevel . Info , `Cloning git repo ${ repoUrl } ` )
461+ const tmpPath = path . resolve ( os . tmpdir ( ) , `scaffold-config-${ Date . now ( ) } ` )
462+
463+ return new Promise ( ( resolve , reject ) => {
464+ const clone = spawn ( "git" , [ "clone" , repoUrl , tmpPath ] )
465+
466+ clone . on ( "error" , reject )
467+ clone . on ( "close" , async ( code ) => {
468+ if ( code === 0 ) {
469+ log ( logConfig , LogLevel . Info , `Loading config from git repo: ${ configFile } ` )
470+ const absolutePath = path . resolve ( tmpPath , url . hash . replace ( "#" , "" ) )
471+ const loadedConfig = ( await import ( absolutePath ) ) . default as ScaffoldConfigFile
472+ log ( logConfig , LogLevel . Info , `Loaded config from git repo` )
473+ log ( logConfig , LogLevel . Debug , `Raw config:` , loadedConfig )
474+ const fixedConfig : ScaffoldConfigFile = Object . fromEntries (
475+ Object . entries ( loadedConfig ) . map ( ( [ k , v ] ) => [
476+ k ,
477+ // use absolute paths for template as config is necessarily in another directory
478+ { ...v , templates : v . templates . map ( ( t ) => path . resolve ( tmpPath , t ) ) } ,
479+ ] ) ,
480+ )
481+
482+ resolve ( fixedConfig )
483+ } else {
484+ reject ( new Error ( `Git clone failed with code ${ code } ` ) )
485+ }
486+ } )
487+ } )
488+ }
489+
490+ throw new Error ( `Unsupported protocol ${ url . protocol } ` )
491+ }
492+
493+ return import ( path . resolve ( process . cwd ( ) , configFile ) )
494+ }
495+
496+ function count ( string : string , substring : string ) : number {
497+ return string . split ( substring ) . length - 1
498+ }
0 commit comments