11import type { Paths } from './Paths.js' ;
22import * as fs from 'node:fs' ;
33import { confirm , input } from '@inquirer/prompts' ;
4- import { parse as parseEnv } from 'dotenv' ;
54import * as path from 'node:path' ;
65
76let loadedEnvFile : EnvFile | undefined = undefined ;
87
8+ interface EnvFileState {
9+ filename : string ;
10+ values : Map < string , string > ;
11+ tpl : string ;
12+ }
13+
914export class EnvFile {
10- private readonly _values : Map < string , string > ;
15+ private readonly _state : EnvFileState ;
1116
12- public constructor ( values : Map < string , string > ) {
13- this . _values = values ;
17+ public constructor ( state : EnvFileState ) {
18+ this . _state = state ;
1419 }
1520
1621 public get ( key : string , fallback ?: string ) : string | undefined {
17- return this . _values . get ( key ) || fallback ;
22+ return this . _state . values . get ( key ) || fallback ;
1823 }
1924
2025 public has ( key : string ) : boolean {
21- return this . _values . has ( key ) ;
26+ return this . _state . values . has ( key ) ;
27+ }
28+
29+ public set ( key : string , value : string ) : this {
30+ this . _state . values . set ( key , value ) ;
31+ return this ;
32+ }
33+
34+ public write ( ) : void {
35+ writeStateToFile ( this . _state ) ;
2236 }
2337}
2438
@@ -37,54 +51,113 @@ export async function makeEnvFile(paths: Paths): Promise<EnvFile> {
3751 fs . copyFileSync ( paths . envFileTemplatePath , paths . envFilePath ) ;
3852 }
3953
40- return loadedEnvFile = await ensureEnvFileContainsProjectName ( loadEnvFile ( paths ) , paths ) ;
54+ const envFile = new EnvFile ( loadEnvFileState ( paths . envFilePath ) ) ;
55+
56+ if ( ! envFile . has ( 'PROJECT_NAME' ) || envFile . get ( 'PROJECT_NAME' ) === '' || envFile . get ( 'PROJECT_NAME' ) === 'replace-me' ) {
57+ const projectName = await input ( {
58+ message : 'You need to define a project name, which can be used for your docker containers and generated urls. Please enter a project name:' ,
59+ validate : ( input ) => {
60+ return input . length > 0 && input . match ( / ^ [ a - z 0 - 9 - ] + $ / ) ? true : 'The project name must only contain lowercase letters, numbers and dashes' ;
61+ } ,
62+ default : extractProjectNameFromPath ( paths ) ,
63+ required : true
64+ } ) ;
65+
66+ envFile . set ( 'PROJECT_NAME' , projectName ) ;
67+ envFile . write ( ) ;
68+ }
69+
70+ return loadedEnvFile = envFile ;
4171}
4272
4373export function getEnvValue ( key : string , fallback ?: string ) : string {
4474 if ( loadedEnvFile && loadedEnvFile . has ( key ) ) {
4575 return loadedEnvFile . get ( key ) ! ;
76+ } else if ( process . env [ key ] ) {
77+ return process . env [ key ] ! ;
4678 } else if ( fallback ) {
4779 return fallback ;
4880 } else {
81+ console . log ( loadedEnvFile , key ) ;
4982 throw new Error ( `Missing required env value: ${ key } ` ) ;
5083 }
5184}
5285
53- function loadEnvFileContent ( paths : Paths ) : string {
54- if ( ! fs . existsSync ( paths . envFilePath ) ) {
55- throw new Error ( `Env file does not exist: ${ paths . envFilePath } ` ) ;
56- }
57-
58- return fs . readFileSync ( paths . envFilePath ) . toString ( 'utf-8' ) ;
86+ function loadEnvFileState ( filename : string ) : EnvFileState {
87+ return {
88+ filename,
89+ ...parseFile ( fs . readFileSync ( filename ) . toString ( 'utf-8' ) )
90+ } ;
5991}
6092
61- function loadEnvFile ( paths : Paths ) : EnvFile {
62- return new EnvFile ( new Map ( Object . entries ( parseEnv ( loadEnvFileContent ( paths ) ) ) ) ) ;
63- }
93+ function parseFile ( content : string ) : {
94+ values : Map < string , string > ;
95+ tpl : string ;
96+ } {
97+ const lines = content . split ( / \r ? \n / ) ;
98+ const tpl : Array < string > = [ ] ;
99+ const values = new Map ( ) ;
100+
101+ // Iterate the lines
102+ lines . forEach ( line => {
103+ let _line = line . trim ( ) ;
104+
105+ // Skip comments and empty lines
106+ if ( _line . length === 0 || _line . charAt ( 0 ) === '#' || _line . indexOf ( '=' ) === - 1 ) {
107+ tpl . push ( line ) ;
108+ return ;
109+ }
64110
65- async function ensureEnvFileContainsProjectName ( envFile : EnvFile , paths : Paths ) {
66- if ( ! envFile . has ( 'PROJECT_NAME' ) || envFile . get ( 'PROJECT_NAME' ) === '' || envFile . get ( 'PROJECT_NAME' ) === 'replace-me' ) {
67- const projectName = await input ( {
68- message : 'You need to define a project name, which can be used for your docker containers and generated urls. Please enter a project name:' ,
69- validate : ( input ) => {
70- return input . length > 0 && input . match ( / ^ [ a - z 0 - 9 - ] + $ / ) ? true : 'The project name must only contain lowercase letters, numbers and dashes' ;
71- } ,
72- default : extractProjectNameFromPath ( paths ) ,
73- required : true
74- } ) ;
111+ // Extract key value and store the line in the template
112+ tpl . push ( _line . replace ( / ^ ( [ ^ = ] * ?) (?: \s + ) ? = (?: \s + ) ? ( .* ?) ( \s # | $ ) / , ( _ , key , value , comment ) => {
113+ // Prepare value
114+ value = value . trim ( ) ;
115+ if ( value . length === 0 ) {
116+ value = null ;
117+ }
118+
119+ // Handle comment only value
120+ if ( typeof value === 'string' && value . charAt ( 0 ) === '#' ) {
121+ comment = ' ' + value ;
122+ value = null ;
123+ }
124+
125+ key = key . trim ( ) ;
126+ if ( values . has ( key ) ) {
127+ throw new Error ( 'Invalid .env file! There was a duplicate key: ' + key ) ;
128+ }
129+ values . set ( key . trim ( ) , value ) ;
130+ return '{{pair}}' + ( ( comment + '' ) . trim ( ) . length > 0 ? comment : '' ) ;
131+ } ) ) ;
132+ } ) ;
133+
134+ return {
135+ values : values ,
136+ tpl : tpl . join ( '\n' )
137+ } ;
138+ }
75139
76- let envContent = loadEnvFileContent ( paths ) ;
77- if ( envContent . includes ( 'PROJECT_NAME=replace-me' ) ) {
78- envContent = envContent . replace ( 'PROJECT_NAME=replace-me' , `PROJECT_NAME=${ projectName } ` ) ;
79- } else {
80- envContent += ( envContent . length > 0 ? `\n` : '' ) + `PROJECT_NAME=${ projectName } ` ;
140+ function writeStateToFile ( state : EnvFileState ) : void {
141+ // Build the content based on the template and the current storage
142+ const keys : Array < string > = Array . from ( state . values . keys ( ) ) ;
143+ let contents = state . tpl . replace ( / { { pair} } / g, ( ) => {
144+ const key = keys . shift ( ) ;
145+ const value = state . values . get ( key + '' ) ;
146+ return key + '=' + value ;
147+ } ) ;
148+
149+ if ( keys . length > 0 ) {
150+ for ( const key of keys ) {
151+ const value = state . values . get ( key ) ;
152+ contents += '\n' + key + '=' + value ;
81153 }
82-
83- fs . writeFileSync ( paths . envFilePath , envContent ) ;
84- return loadEnvFile ( paths ) ;
85154 }
86155
87- return envFile ;
156+ // Remove all spacing at the top and bottom of the file
157+ contents = contents . replace ( / ^ \s + | \s + $ / g, '' ) ;
158+
159+ // Write the file
160+ fs . writeFileSync ( state . filename , contents ) ;
88161}
89162
90163function extractProjectNameFromPath ( paths : Paths ) : string {
0 commit comments