6
6
import { isIPv4 } from "net" ;
7
7
import * as clc from "colorette" ;
8
8
import { checkListenable } from "../portUtils" ;
9
- import { detectPackageManager , detectStartCommand } from "./developmentServer" ;
9
+ import { detectPackageManager , detectPackageManagerStartCommand } from "./developmentServer" ;
10
10
import { DEFAULT_HOST , DEFAULT_PORTS } from "../constants" ;
11
11
import { spawnWithCommandString } from "../../init/spawn" ;
12
12
import { logger } from "./developmentServer" ;
@@ -33,32 +33,6 @@ interface StartOptions {
33
33
rootDirectory ?: string ;
34
34
}
35
35
36
- /**
37
- * Spins up a project locally by running the project's dev command.
38
- *
39
- * Assumptions:
40
- * - Dev server runs on "localhost" when the package manager's dev command is
41
- * run
42
- * - Dev server will respect the PORT environment variable
43
- */
44
- export async function start ( options ?: StartOptions ) : Promise < { hostname : string ; port : number } > {
45
- const hostname = DEFAULT_HOST ;
46
- let port = options ?. port ?? DEFAULT_PORTS . apphosting ;
47
- while ( ! ( await availablePort ( hostname , port ) ) ) {
48
- port += 1 ;
49
- }
50
-
51
- await serve (
52
- options ?. projectId ,
53
- options ?. backendId ,
54
- port ,
55
- options ?. startCommand ,
56
- options ?. rootDirectory ,
57
- ) ;
58
-
59
- return { hostname, port } ;
60
- }
61
-
62
36
// Matches a fully qualified secret or version name, e.g.
63
37
// projects/my-project/secrets/my-secret/versions/1
64
38
// projects/my-project/secrets/my-secret/versions/latest
@@ -111,22 +85,58 @@ async function loadSecret(project: string | undefined, name: string): Promise<st
111
85
}
112
86
113
87
/**
114
- * Runs the development server in a child process.
88
+ * Spins up a project locally by running the project's dev command.
89
+ *
90
+ * Assumptions:
91
+ * - Dev server runs on "localhost" when the package manager's dev command is
92
+ * run
93
+ * - Dev server will respect the PORT environment variable
94
+ * - This is not the case for Angular. When an `ng serve`
95
+ * custom command is detected, we add --port <PORT> instead.
115
96
*/
116
- async function serve (
117
- projectId : string | undefined ,
118
- backendId : string | undefined ,
119
- port : number ,
120
- startCommand ?: string ,
121
- backendRelativeDir ?: string ,
122
- ) : Promise < void > {
123
- backendRelativeDir = backendRelativeDir ?? "./" ;
97
+ export async function start ( options ?: StartOptions ) : Promise < { hostname : string ; port : number } > {
98
+ const hostname = DEFAULT_HOST ;
99
+ let port = options ?. port ?? DEFAULT_PORTS . apphosting ;
100
+ while ( ! ( await availablePort ( hostname , port ) ) ) {
101
+ port += 1 ;
102
+ }
103
+
104
+ const backendRoot = resolveProjectPath ( { } , options ?. rootDirectory ?? "./" ) ;
105
+
106
+ let startCommand ;
107
+ if ( options ?. startCommand ) {
108
+ startCommand = options ?. startCommand ;
109
+ // Angular and nextjs CLIs allow for specifying port options but the emulator is setting and
110
+ // specifying specific ports rather than use framework defaults or w/e the user has set, so we
111
+ // need to reject such custom commands.
112
+ // NOTE: this is not robust, a command could be a wrapper around another command and we cannot
113
+ // detect --port there.
114
+ if ( startCommand . includes ( "--port" ) || startCommand . includes ( " -p " ) ) {
115
+ throw new FirebaseError (
116
+ "Specifying a port in the start command is not supported by the apphosting emulator" ,
117
+ ) ;
118
+ }
119
+ // Angular does not respect the NodeJS.ProcessEnv.PORT set below. Port needs to be
120
+ // set directly in the CLI.
121
+ if ( startCommand . includes ( "ng serve" ) ) {
122
+ startCommand += ` --port ${ port } ` ;
123
+ }
124
+ logger . logLabeled (
125
+ "BULLET" ,
126
+ Emulators . APPHOSTING ,
127
+ `running custom start command: '${ startCommand } '` ,
128
+ ) ;
129
+ } else {
130
+ // TODO: port may be specified in an underlying command. But we will need to parse the package.json
131
+ // file to be sure.
132
+ startCommand = await detectPackageManagerStartCommand ( backendRoot ) ;
133
+ logger . logLabeled ( "BULLET" , Emulators . APPHOSTING , `starting app with: '${ startCommand } '` ) ;
134
+ }
124
135
125
- const backendRoot = resolveProjectPath ( { } , backendRelativeDir ) ;
126
136
const apphostingLocalConfig = await getLocalAppHostingConfiguration ( backendRoot ) ;
127
137
const resolveEnv = Object . entries ( apphostingLocalConfig . env ) . map ( async ( [ key , value ] ) => [
128
138
key ,
129
- value . value ? value . value : await loadSecret ( projectId , value . secret ! ) ,
139
+ value . value ? value . value : await loadSecret ( options ?. projectId , value . secret ! ) ,
130
140
] ) ;
131
141
132
142
const environmentVariablesToInject : NodeJS . ProcessEnv = {
@@ -135,8 +145,8 @@ async function serve(
135
145
...Object . fromEntries ( await Promise . all ( resolveEnv ) ) ,
136
146
FIREBASE_APP_HOSTING : "1" ,
137
147
X_GOOGLE_TARGET_PLATFORM : "fah" ,
138
- GCLOUD_PROJECT : projectId ,
139
- PROJECT_ID : projectId ,
148
+ GCLOUD_PROJECT : options ?. projectId ,
149
+ PROJECT_ID : options ?. projectId ,
140
150
PORT : port . toString ( ) ,
141
151
} ;
142
152
@@ -145,7 +155,7 @@ async function serve(
145
155
// TODO(jamesdaniels) look into pnpm support for autoinit
146
156
logLabeledWarning ( "apphosting" , `Firebase JS SDK autoinit does not currently support PNPM.` ) ;
147
157
} else {
148
- const webappConfig = await getBackendAppConfig ( projectId , backendId ) ;
158
+ const webappConfig = await getBackendAppConfig ( options ?. projectId , options ?. backendId ) ;
149
159
if ( webappConfig ) {
150
160
environmentVariablesToInject [ "FIREBASE_WEBAPP_CONFIG" ] ||= JSON . stringify ( webappConfig ) ;
151
161
environmentVariablesToInject [ "FIREBASE_CONFIG" ] ||= JSON . stringify ( {
@@ -157,31 +167,14 @@ async function serve(
157
167
await tripFirebasePostinstall ( backendRoot , environmentVariablesToInject ) ;
158
168
}
159
169
160
- if ( startCommand ) {
161
- logger . logLabeled (
162
- "BULLET" ,
163
- Emulators . APPHOSTING ,
164
- `running custom start command: '${ startCommand } '` ,
165
- ) ;
166
-
167
- // NOTE: Development server should not block main emulator process.
168
- spawnWithCommandString ( startCommand , backendRoot , environmentVariablesToInject )
169
- . catch ( ( err ) => {
170
- logger . logLabeled ( "ERROR" , Emulators . APPHOSTING , `failed to start Dev Server: ${ err } ` ) ;
171
- } )
172
- . then ( ( ) => logger . logLabeled ( "BULLET" , Emulators . APPHOSTING , `Dev Server stopped` ) ) ;
173
- return ;
174
- }
175
-
176
- const detectedStartCommand = await detectStartCommand ( backendRoot ) ;
177
- logger . logLabeled ( "BULLET" , Emulators . APPHOSTING , `starting app with: '${ detectedStartCommand } '` ) ;
178
-
179
170
// NOTE: Development server should not block main emulator process.
180
- spawnWithCommandString ( detectedStartCommand , backendRoot , environmentVariablesToInject )
171
+ spawnWithCommandString ( startCommand , backendRoot , environmentVariablesToInject )
181
172
. catch ( ( err ) => {
182
173
logger . logLabeled ( "ERROR" , Emulators . APPHOSTING , `failed to start Dev Server: ${ err } ` ) ;
183
174
} )
184
175
. then ( ( ) => logger . logLabeled ( "BULLET" , Emulators . APPHOSTING , `Dev Server stopped` ) ) ;
176
+
177
+ return { hostname, port } ;
185
178
}
186
179
187
180
function availablePort ( host : string , port : number ) : Promise < boolean > {
0 commit comments