1
1
import type { Paths } from './Paths.js' ;
2
2
import * as fs from 'node:fs' ;
3
3
import { confirm , input } from '@inquirer/prompts' ;
4
- import { parse as parseEnv } from 'dotenv' ;
5
4
import * as path from 'node:path' ;
6
5
7
6
let loadedEnvFile : EnvFile | undefined = undefined ;
8
7
8
+ interface EnvFileState {
9
+ filename : string ;
10
+ values : Map < string , string > ;
11
+ tpl : string ;
12
+ }
13
+
9
14
export class EnvFile {
10
- private readonly _values : Map < string , string > ;
15
+ private readonly _state : EnvFileState ;
11
16
12
- public constructor ( values : Map < string , string > ) {
13
- this . _values = values ;
17
+ public constructor ( state : EnvFileState ) {
18
+ this . _state = state ;
14
19
}
15
20
16
21
public get ( key : string , fallback ?: string ) : string | undefined {
17
- return this . _values . get ( key ) || fallback ;
22
+ return this . _state . values . get ( key ) || fallback ;
18
23
}
19
24
20
25
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 ) ;
22
36
}
23
37
}
24
38
@@ -37,54 +51,113 @@ export async function makeEnvFile(paths: Paths): Promise<EnvFile> {
37
51
fs . copyFileSync ( paths . envFileTemplatePath , paths . envFilePath ) ;
38
52
}
39
53
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 ;
41
71
}
42
72
43
73
export function getEnvValue ( key : string , fallback ?: string ) : string {
44
74
if ( loadedEnvFile && loadedEnvFile . has ( key ) ) {
45
75
return loadedEnvFile . get ( key ) ! ;
76
+ } else if ( process . env [ key ] ) {
77
+ return process . env [ key ] ! ;
46
78
} else if ( fallback ) {
47
79
return fallback ;
48
80
} else {
81
+ console . log ( loadedEnvFile , key ) ;
49
82
throw new Error ( `Missing required env value: ${ key } ` ) ;
50
83
}
51
84
}
52
85
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
+ } ;
59
91
}
60
92
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
+ }
64
110
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
+ }
75
139
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 ;
81
153
}
82
-
83
- fs . writeFileSync ( paths . envFilePath , envContent ) ;
84
- return loadEnvFile ( paths ) ;
85
154
}
86
155
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 ) ;
88
161
}
89
162
90
163
function extractProjectNameFromPath ( paths : Paths ) : string {
0 commit comments