@@ -9,6 +9,31 @@ import type { EnvironmentConfig } from './environment.js';
99
1010export type Application = ReturnType < typeof application > ;
1111
12+ /**
13+ * Resolves the server URL for a dev/serve process, ensuring the runtime port
14+ * is always reflected in the URL. Uses the URL constructor to detect whether
15+ * an explicit port is present (avoiding false positives from the scheme colon).
16+ */
17+ export const resolveServerUrl = (
18+ optsServerUrl : string | undefined ,
19+ fallbackServerUrl : string | undefined ,
20+ port : number ,
21+ ) : string => {
22+ if ( optsServerUrl ) {
23+ try {
24+ const parsed = new URL ( optsServerUrl ) ;
25+ if ( ! parsed . port ) {
26+ parsed . port = String ( port ) ;
27+ }
28+ return parsed . origin ;
29+ } catch {
30+ // Bare host (e.g. "localhost"), not a full URL
31+ return `${ optsServerUrl } :${ port } ` ;
32+ }
33+ }
34+ return fallbackServerUrl || `http://localhost:${ port } ` ;
35+ } ;
36+
1237export const application = (
1338 config : ApplicationConfig ,
1439 appDirPath : string ,
@@ -64,13 +89,7 @@ export const application = (
6489 dev : async ( opts : { port ?: number ; manualStart ?: boolean ; detached ?: boolean ; serverUrl ?: string } = { } ) => {
6590 const log = logger . child ( { prefix : 'dev' } ) . info ;
6691 const port = opts . port || ( await getPort ( ) ) ;
67- const getServerUrl = ( ) => {
68- if ( opts . serverUrl ) {
69- return opts . serverUrl . includes ( ':' ) ? opts . serverUrl : `${ opts . serverUrl } :${ port } ` ;
70- }
71- return serverUrl || `http://localhost:${ port } ` ;
72- } ;
73- const runtimeServerUrl = getServerUrl ( ) ;
92+ const runtimeServerUrl = resolveServerUrl ( opts . serverUrl , serverUrl , port ) ;
7493 log ( `Will try to serve app at ${ runtimeServerUrl } ` ) ;
7594 if ( opts . manualStart ) {
7695 // for debugging, you can start the dev server manually by cd'ing into the temp dir
@@ -150,25 +169,59 @@ export const application = (
150169 get serveOutput ( ) {
151170 return serveOutput ;
152171 } ,
153- serve : async ( opts : { port ?: number ; manualStart ?: boolean } = { } ) => {
172+ serve : async ( opts : { port ?: number ; manualStart ?: boolean ; detached ?: boolean ; serverUrl ?: string } = { } ) => {
154173 const log = logger . child ( { prefix : 'serve' } ) . info ;
155174 const port = opts . port || ( await getPort ( ) ) ;
156- // TODO: get serverUrl as in dev()
157- const serverUrl = `http://localhost:${ port } ` ;
158- // If this is ever used as a background process, we need to make sure
159- // it's not using the log function. See the dev() method above
175+ const runtimeServerUrl = resolveServerUrl ( opts . serverUrl , serverUrl , port ) ;
176+ log ( `Will try to serve app at ${ runtimeServerUrl } ` ) ;
177+
178+ if ( opts . manualStart ) {
179+ state . serverUrl = runtimeServerUrl ;
180+ return { port, serverUrl : runtimeServerUrl } ;
181+ }
182+
183+ // Read .env file and pass as process env vars since production servers
184+ // may not auto-load .env files (e.g., react-router-serve)
185+ const envFromFile : Record < string , string > = { } ;
186+ const envFilePath = path . resolve ( appDirPath , '.env' ) ;
187+ if ( fs . existsSync ( envFilePath ) ) {
188+ const envContent = fs . readFileSync ( envFilePath , 'utf-8' ) ;
189+ for ( const line of envContent . split ( '\n' ) ) {
190+ const trimmed = line . trim ( ) ;
191+ if ( trimmed && ! trimmed . startsWith ( '#' ) ) {
192+ const eqIdx = trimmed . indexOf ( '=' ) ;
193+ if ( eqIdx > 0 ) {
194+ envFromFile [ trimmed . slice ( 0 , eqIdx ) ] = trimmed . slice ( eqIdx + 1 ) ;
195+ }
196+ }
197+ }
198+ }
199+
160200 const proc = run ( scripts . serve , {
161201 cwd : appDirPath ,
162- env : { PORT : port . toString ( ) } ,
163- log : ( msg : string ) => {
164- serveOutput += `\n${ msg } ` ;
165- log ( msg ) ;
166- } ,
202+ env : { ...envFromFile , PORT : port . toString ( ) } ,
203+ detached : opts . detached ,
204+ stdout : opts . detached ? fs . openSync ( stdoutFilePath , 'a' ) : undefined ,
205+ stderr : opts . detached ? fs . openSync ( stderrFilePath , 'a' ) : undefined ,
206+ log : opts . detached
207+ ? undefined
208+ : ( msg : string ) => {
209+ serveOutput += `\n${ msg } ` ;
210+ log ( msg ) ;
211+ } ,
167212 } ) ;
213+
214+ if ( opts . detached ) {
215+ const shouldExit = ( ) => ! ! proc . exitCode && proc . exitCode !== 0 ;
216+ await waitForServer ( runtimeServerUrl , { log, maxAttempts : Infinity , shouldExit } ) ;
217+ } else {
218+ await waitForIdleProcess ( proc ) ;
219+ }
220+
221+ log ( `Server started at ${ runtimeServerUrl } , pid: ${ proc . pid } ` ) ;
168222 cleanupFns . push ( ( ) => awaitableTreekill ( proc . pid , 'SIGKILL' ) ) ;
169- await waitForIdleProcess ( proc ) ;
170- state . serverUrl = serverUrl ;
171- return { port, serverUrl, pid : proc } ;
223+ state . serverUrl = runtimeServerUrl ;
224+ return { port, serverUrl : runtimeServerUrl , pid : proc . pid } ;
172225 } ,
173226 stop : async ( ) => {
174227 logger . info ( 'Stopping...' ) ;
0 commit comments