66import { isIPv4 } from "net" ;
77import * as clc from "colorette" ;
88import { checkListenable } from "../portUtils" ;
9- import { detectPackageManager , detectStartCommand } from "./developmentServer" ;
9+ import { detectPackageManager , detectPackageManagerStartCommand } from "./developmentServer" ;
1010import { DEFAULT_HOST , DEFAULT_PORTS } from "../constants" ;
1111import { spawnWithCommandString } from "../../init/spawn" ;
1212import { logger } from "./developmentServer" ;
@@ -33,32 +33,6 @@ interface StartOptions {
3333 rootDirectory ?: string ;
3434}
3535
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-
6236// Matches a fully qualified secret or version name, e.g.
6337// projects/my-project/secrets/my-secret/versions/1
6438// projects/my-project/secrets/my-secret/versions/latest
@@ -111,22 +85,58 @@ async function loadSecret(project: string | undefined, name: string): Promise<st
11185}
11286
11387/**
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.
11596 */
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+ }
124135
125- const backendRoot = resolveProjectPath ( { } , backendRelativeDir ) ;
126136 const apphostingLocalConfig = await getLocalAppHostingConfiguration ( backendRoot ) ;
127137 const resolveEnv = Object . entries ( apphostingLocalConfig . env ) . map ( async ( [ key , value ] ) => [
128138 key ,
129- value . value ? value . value : await loadSecret ( projectId , value . secret ! ) ,
139+ value . value ? value . value : await loadSecret ( options ?. projectId , value . secret ! ) ,
130140 ] ) ;
131141
132142 const environmentVariablesToInject : NodeJS . ProcessEnv = {
@@ -135,8 +145,8 @@ async function serve(
135145 ...Object . fromEntries ( await Promise . all ( resolveEnv ) ) ,
136146 FIREBASE_APP_HOSTING : "1" ,
137147 X_GOOGLE_TARGET_PLATFORM : "fah" ,
138- GCLOUD_PROJECT : projectId ,
139- PROJECT_ID : projectId ,
148+ GCLOUD_PROJECT : options ?. projectId ,
149+ PROJECT_ID : options ?. projectId ,
140150 PORT : port . toString ( ) ,
141151 } ;
142152
@@ -145,7 +155,7 @@ async function serve(
145155 // TODO(jamesdaniels) look into pnpm support for autoinit
146156 logLabeledWarning ( "apphosting" , `Firebase JS SDK autoinit does not currently support PNPM.` ) ;
147157 } else {
148- const webappConfig = await getBackendAppConfig ( projectId , backendId ) ;
158+ const webappConfig = await getBackendAppConfig ( options ?. projectId , options ?. backendId ) ;
149159 if ( webappConfig ) {
150160 environmentVariablesToInject [ "FIREBASE_WEBAPP_CONFIG" ] ||= JSON . stringify ( webappConfig ) ;
151161 environmentVariablesToInject [ "FIREBASE_CONFIG" ] ||= JSON . stringify ( {
@@ -157,31 +167,14 @@ async function serve(
157167 await tripFirebasePostinstall ( backendRoot , environmentVariablesToInject ) ;
158168 }
159169
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-
179170 // NOTE: Development server should not block main emulator process.
180- spawnWithCommandString ( detectedStartCommand , backendRoot , environmentVariablesToInject )
171+ spawnWithCommandString ( startCommand , backendRoot , environmentVariablesToInject )
181172 . catch ( ( err ) => {
182173 logger . logLabeled ( "ERROR" , Emulators . APPHOSTING , `failed to start Dev Server: ${ err } ` ) ;
183174 } )
184175 . then ( ( ) => logger . logLabeled ( "BULLET" , Emulators . APPHOSTING , `Dev Server stopped` ) ) ;
176+
177+ return { hostname, port } ;
185178}
186179
187180function availablePort ( host : string , port : number ) : Promise < boolean > {
0 commit comments