11import { dirname , join } from 'node:path'
22
33import { Arkstack } from '@arkstack/contract'
4+ import { env } from '@arkstack/common'
45import { Command } from '@h3ravel/musket'
56import { createRequire } from 'node:module'
67import { readFileSync } from 'node:fs'
78import { spawn } from 'node:child_process'
9+ import ngrok , { type Listener } from '@ngrok/ngrok'
810
911export interface DevServerOptions {
1012 /**
@@ -35,6 +37,22 @@ export class DevCommand extends Command {
3537 const vars = DevCommand . devServerEnv ( this . options ( ) )
3638 const rootDir = Arkstack . rootDir ( )
3739 const bin = DevCommand . resolveTsdownBin ( rootDir )
40+ let tunnel : Listener | undefined
41+
42+ if ( this . option ?.( 'tunnel' ) ) {
43+ tunnel = await ngrok . forward ( {
44+ addr : Number ( env ( 'APP_PORT' , env ( 'PORT' , 3000 ) ) ) ,
45+ authtoken : env ( 'NGROK_AUTHTOKEN' ) ,
46+ domain : env ( 'NGROK_DOMAIN' ) ,
47+ } )
48+
49+ const url = tunnel . url ( )
50+
51+ if ( url ) {
52+ vars . TUNNEL_URL = url
53+ console . log ( `Traffic has been tunnelled to ${ url } ` )
54+ }
55+ }
3856
3957 // Run tsdown directly with node when we can resolve its bin — this avoids
4058 // the extra `pnpm exec` wrapper process. Fall back to `pnpm exec tsdown`
@@ -46,27 +64,31 @@ export class DevCommand extends Command {
4664 [ 'exec' , 'tsdown' , '--log-level' , 'silent' ] ,
4765 ]
4866
49- await new Promise < void > ( ( resolve , reject ) => {
50- const child = spawn ( command , args , {
51- cwd : rootDir ,
52- stdio : 'inherit' ,
53- env : Object . assign ( process . env , vars ) ,
54- } )
55-
56- child . on ( 'error' , ( error ) => {
57- reject ( error )
58- } )
59-
60- child . on ( 'exit' , ( code ) => {
61- if ( code === 0 || code === null ) {
62- resolve ( )
63-
64- return
65- }
66-
67- reject ( new Error ( `tsdown exited with code ${ code } ` ) )
67+ try {
68+ await new Promise < void > ( ( resolve , reject ) => {
69+ const child = spawn ( command , args , {
70+ cwd : rootDir ,
71+ stdio : 'inherit' ,
72+ env : Object . assign ( { } , process . env , vars ) ,
73+ } )
74+
75+ child . on ( 'error' , ( error ) => {
76+ reject ( error )
77+ } )
78+
79+ child . on ( 'exit' , ( code ) => {
80+ if ( code === 0 || code === null ) {
81+ resolve ( )
82+
83+ return
84+ }
85+
86+ reject ( new Error ( `tsdown exited with code ${ code } ` ) )
87+ } )
6888 } )
69- } )
89+ } finally {
90+ await tunnel ?. close ( )
91+ }
7092 }
7193
7294 /**
@@ -97,7 +119,8 @@ export class DevCommand extends Command {
97119 *
98120 * The dev server binds `127.0.0.1` by default so it is local-only; `--host`
99121 * switches it to `0.0.0.0` to expose it on the local network. `--secure` flags
100- * the driver to serve HTTPS, and `--tunnel` enables the Ngrok tunnel.
122+ * the driver to serve HTTPS. The command itself owns the Ngrok tunnel so
123+ * watcher-driven application restarts cannot replace its public URL.
101124 *
102125 * @param options
103126 * @returns
@@ -108,10 +131,7 @@ export class DevCommand extends Command {
108131 const vars : Record < string , string > = {
109132 NODE_ENV : 'development' ,
110133 APP_HOST : options . host ? '0.0.0.0' : '127.0.0.1' ,
111- }
112-
113- if ( options . tunnel ) {
114- vars . TUNNEL = 'true'
134+ ARKSTACK_ENV_RELOAD : 'true' ,
115135 }
116136
117137 if ( options . secure ) {
0 commit comments