@@ -5,6 +5,7 @@ import org.http4s.*
55import org .http4s .HttpApp
66import org .http4s .ember .client .EmberClientBuilder
77import org .http4s .ember .server .EmberServerBuilder
8+ import org .http4s .server .Server
89
910import com .comcast .ip4s .Port
1011import com .comcast .ip4s .host
@@ -156,6 +157,13 @@ object LiveServer extends IOApp:
156157 )
157158 .orNone
158159
160+ val preventBrowserOpenOpt = Opts
161+ .flag(
162+ " prevent-browser-open" ,
163+ " prevent the browser from opening on server start"
164+ )
165+ .orFalse
166+
159167 val millModuleNameOpt : Opts [Option [String ]] = Opts
160168 .option[String ](
161169 " mill-module-name" ,
@@ -167,128 +175,144 @@ object LiveServer extends IOApp:
167175 }
168176 .orNone
169177
170- def main : Opts [IO [ExitCode ]] =
171- (
172- baseDirOpt,
173- outDirOpt,
174- portOpt,
175- proxyPortTargetOpt,
176- proxyPathMatchPrefixOpt,
177- clientRoutingPrefixOpt,
178- logLevelOpt,
179- buildToolOpt,
180- openBrowserAtOpt,
181- extraBuildArgsOpt,
182- millModuleNameOpt,
183- stylesDirOpt,
184- indexHtmlTemplateOpt
185- ).mapN {
186- (
187- baseDir,
188- outDirOpt,
189- port,
190- proxyTarget,
191- pathPrefix,
192- clientRoutingPrefix,
193- lvl,
194- buildTool,
195- openBrowserAt,
196- extraBuildArgs,
197- millModuleName,
198- stylesDir,
199- indexHtmlTemplate
200- ) =>
201-
202- scribe
203- .Logger
204- .root
205- .clearHandlers()
206- .clearModifiers()
207- .withHandler(minimumLevel = Some (Level .get(lvl).get))
208- .replace()
209-
210- val server = for
211- _ <- logger
212- .debug(
213- s " baseDir: $baseDir \n outDir: $outDirOpt \n stylesDir: $stylesDir \n indexHtmlTemplate: $indexHtmlTemplate \n port: $port \n proxyTarget: $proxyTarget \n pathPrefix: $pathPrefix \n extraBuildArgs: $extraBuildArgs"
214- )
215- .toResource
216-
217- fileToHashRef <- Ref [IO ].of(Map .empty[String , String ]).toResource
218- refreshTopic <- Topic [IO , Unit ].toResource
219- linkingTopic <- Topic [IO , Unit ].toResource
220- client <- EmberClientBuilder .default[IO ].build
221- baseDirPath <- baseDir.fold(Files [IO ].currentWorkingDirectory.toResource)(toDirectoryPath)
222- outDirPath <- outDirOpt.fold(Files [IO ].tempDirectory)(toDirectoryPath)
223- outDirString = outDirPath.show
224- indexHtmlTemplatePath <- indexHtmlTemplate.traverse(toDirectoryPath)
225- stylesDirPath <- stylesDir.traverse(toDirectoryPath)
226-
227- indexOpts <- (indexHtmlTemplatePath, stylesDirPath) match
228- case (Some (html), None ) =>
229- val indexHtmlFile = html / " index.html"
230- println(indexHtmlFile)
231- (for
232- indexHtmlExists <- Files [IO ].exists(indexHtmlFile)
233- _ <- IO .raiseUnless(indexHtmlExists)(CliValidationError (s " index.html doesn't exist in $html" ))
234- indexHtmlIsAFile <- Files [IO ].isRegularFile(indexHtmlFile)
235- _ <- IO .raiseUnless(indexHtmlIsAFile)(CliValidationError (s " $indexHtmlFile is not a file " ))
236- yield IndexHtmlConfig .IndexHtmlPath (html).some).toResource
237- case (None , Some (styles)) =>
238- val indexLessFile = styles / " index.less"
239- (for
240- indexLessExists <- Files [IO ].exists(indexLessFile)
241- _ <- IO .raiseUnless(indexLessExists)(CliValidationError (s " index.html doesn't exist in $styles" ))
242- indexLessIsAFile <- Files [IO ].isRegularFile(indexLessFile)
243- _ <- IO .raiseUnless(indexLessIsAFile)(CliValidationError (s " $indexLessFile is not a file " ))
244- yield IndexHtmlConfig .StylesOnly (styles).some).toResource
245- case (None , None ) =>
246- Resource .pure(Option .empty[IndexHtmlConfig ])
247- case (Some (_), Some (_)) =>
248- Resource .raiseError[IO , Nothing , Throwable ](
249- CliValidationError (" path-to-index-html and styles-dir can't be defined at the same time" )
250- )
251-
252- proxyConf2 <- proxyConf(proxyTarget, pathPrefix)
253- proxyRoutes : HttpRoutes [IO ] = makeProxyRoutes(client, proxyConf2)(logger)
254-
255- _ <- buildRunner(
256- buildTool,
257- linkingTopic,
258- baseDirPath,
259- outDirPath,
260- extraBuildArgs,
261- millModuleName
262- )(logger)
263-
264- app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef, clientRoutingPrefix)(logger)
265-
266- _ <- updateMapRef(outDirPath, fileToHashRef)(logger).toResource
267- // _ <- stylesDir.fold(Resource.unit)(sd => seedMapOnStart(sd, mr))
268- _ <- fileWatcher(outDirPath, fileToHashRef, linkingTopic, refreshTopic)(logger)
269- _ <- indexOpts.match
270- case Some (IndexHtmlConfig .IndexHtmlPath (indexHtmlatPath)) =>
271- staticWatcher(refreshTopic, fs2.io.file.Path (indexHtmlatPath.toString))(logger)
272- case _ => Resource .unit[IO ]
273-
274- // _ <- stylesDir.fold(Resource.unit[IO])(sd => fileWatcher(fs2.io.file.Path(sd), mr))
275- _ <- logger.info(s " Start dev server on http://localhost: $port" ).toResource
276- server <- buildServer(app.orNotFound, port)
277-
278- - <- openBrowser(Some (openBrowserAt), port)(logger).toResource
279- yield server
280-
281- server
282- .useForever
283- .as(ExitCode .Success )
284- .handleErrorWith {
285- case CliValidationError (message) =>
286- IO .println(s " $message\n ${command.showHelp}" ).as(ExitCode .Error )
287- case error => IO .raiseError(error)
288- }
289- }
178+ case class LiveServerConfig (
179+ baseDir : Option [String ],
180+ outDir : Option [String ] = None ,
181+ port : Port ,
182+ proxyPortTarget : Option [Port ] = None ,
183+ proxyPathMatchPrefix : Option [String ] = None ,
184+ clientRoutingPrefix : Option [String ] = None ,
185+ logLevel : String = " info" ,
186+ buildTool : BuildTool = ScalaCli (),
187+ openBrowserAt : String ,
188+ preventBrowserOpen : Boolean = false ,
189+ extraBuildArgs : List [String ] = List .empty,
190+ millModuleName : Option [String ] = None ,
191+ stylesDir : Option [String ] = None ,
192+ indexHtmlTemplate : Option [String ] = None
193+ )
194+
195+ def parseOpts = (
196+ baseDirOpt,
197+ outDirOpt,
198+ portOpt,
199+ proxyPortTargetOpt,
200+ proxyPathMatchPrefixOpt,
201+ clientRoutingPrefixOpt,
202+ logLevelOpt,
203+ buildToolOpt,
204+ openBrowserAtOpt,
205+ preventBrowserOpenOpt,
206+ extraBuildArgsOpt,
207+ millModuleNameOpt,
208+ stylesDirOpt,
209+ indexHtmlTemplateOpt
210+ ).mapN(LiveServerConfig .apply)
211+
212+ def main (lsc : LiveServerConfig ): Resource [IO , Server ] =
213+
214+ scribe
215+ .Logger
216+ .root
217+ .clearHandlers()
218+ .clearModifiers()
219+ .withHandler(minimumLevel = Some (Level .get(lsc.logLevel).get))
220+ .replace()
221+
222+ val server = for
223+ _ <- logger
224+ .debug(
225+ lsc.toString()
226+ )
227+ .toResource
228+
229+ fileToHashRef <- Ref [IO ].of(Map .empty[String , String ]).toResource
230+ refreshTopic <- Topic [IO , Unit ].toResource
231+ linkingTopic <- Topic [IO , Unit ].toResource
232+ client <- EmberClientBuilder .default[IO ].build
233+ baseDirPath <- lsc.baseDir.fold(Files [IO ].currentWorkingDirectory.toResource)(toDirectoryPath)
234+ outDirPath <- lsc.outDir.fold(Files [IO ].tempDirectory)(toDirectoryPath)
235+ outDirString = outDirPath.show
236+ indexHtmlTemplatePath <- lsc.indexHtmlTemplate.traverse(toDirectoryPath)
237+ stylesDirPath <- lsc.stylesDir.traverse(toDirectoryPath)
238+
239+ indexOpts <- (indexHtmlTemplatePath, stylesDirPath) match
240+ case (Some (html), None ) =>
241+ val indexHtmlFile = html / " index.html"
242+ println(indexHtmlFile)
243+ (for
244+ indexHtmlExists <- Files [IO ].exists(indexHtmlFile)
245+ _ <- IO .raiseUnless(indexHtmlExists)(CliValidationError (s " index.html doesn't exist in $html" ))
246+ indexHtmlIsAFile <- Files [IO ].isRegularFile(indexHtmlFile)
247+ _ <- IO .raiseUnless(indexHtmlIsAFile)(CliValidationError (s " $indexHtmlFile is not a file " ))
248+ yield IndexHtmlConfig .IndexHtmlPath (html).some).toResource
249+ case (None , Some (styles)) =>
250+ val indexLessFile = styles / " index.less"
251+ (for
252+ indexLessExists <- Files [IO ].exists(indexLessFile)
253+ _ <- IO .raiseUnless(indexLessExists)(CliValidationError (s " index.html doesn't exist in $styles" ))
254+ indexLessIsAFile <- Files [IO ].isRegularFile(indexLessFile)
255+ _ <- IO .raiseUnless(indexLessIsAFile)(CliValidationError (s " $indexLessFile is not a file " ))
256+ yield IndexHtmlConfig .StylesOnly (styles).some).toResource
257+ case (None , None ) =>
258+ Resource .pure(Option .empty[IndexHtmlConfig ])
259+ case (Some (_), Some (_)) =>
260+ Resource .raiseError[IO , Nothing , Throwable ](
261+ CliValidationError (" path-to-index-html and styles-dir can't be defined at the same time" )
262+ )
263+
264+ proxyConf2 <- proxyConf(lsc.proxyPortTarget, lsc.proxyPathMatchPrefix)
265+ proxyRoutes : HttpRoutes [IO ] = makeProxyRoutes(client, proxyConf2)(logger)
266+
267+ _ <- buildRunner(
268+ lsc.buildTool,
269+ linkingTopic,
270+ baseDirPath,
271+ outDirPath,
272+ lsc.extraBuildArgs,
273+ lsc.millModuleName
274+ )(logger)
275+
276+ app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef, lsc.clientRoutingPrefix)(logger)
277+
278+ _ <- updateMapRef(outDirPath, fileToHashRef)(logger).toResource
279+ // _ <- stylesDir.fold(Resource.unit)(sd => seedMapOnStart(sd, mr))
280+ _ <- fileWatcher(outDirPath, fileToHashRef, linkingTopic, refreshTopic)(logger)
281+ _ <- indexOpts.match
282+ case Some (IndexHtmlConfig .IndexHtmlPath (indexHtmlatPath)) =>
283+ staticWatcher(refreshTopic, fs2.io.file.Path (indexHtmlatPath.toString))(logger)
284+ case _ => Resource .unit[IO ]
285+
286+ // _ <- stylesDir.fold(Resource.unit[IO])(sd => fileWatcher(fs2.io.file.Path(sd), mr))
287+ _ <- logger.info(s " Start dev server on http://localhost: ${lsc.port}" ).toResource
288+ server <- buildServer(app.orNotFound, lsc.port)
289+
290+ // - <- openBrowser(Some(openBrowserAt), port)(logger).toResource
291+ yield server
292+
293+ server
294+ // .useForever
295+ // .as(ExitCode.Success)
296+ // .handleErrorWith {
297+ // case CliValidationError(message) =>
298+ // IO.println(s"$message\n${command.showHelp}").as(ExitCode.Error)
299+ // case error => IO.raiseError(error)
300+ // }
301+
290302 end main
291303
304+ def runServerHandleErrors : Opts [IO [ExitCode ]] = parseOpts.map(
305+ ops =>
306+ main(ops)
307+ .useForever
308+ .as(ExitCode .Success )
309+ .handleErrorWith {
310+ case CliValidationError (message) =>
311+ IO .println(s " ${command.showHelp} \n $message \n see help above " ).as(ExitCode .Error )
312+ case error => IO .raiseError(error)
313+ }
314+ )
315+
292316 val command =
293317 val versionFlag = Opts .flag(
294318 long = " version" ,
@@ -298,7 +322,11 @@ object LiveServer extends IOApp:
298322 )
299323
300324 val version = " 0.0.1"
301- val finalOpts = versionFlag.as(IO .println(version).as(ExitCode .Success )).orElse(main)
325+ val finalOpts = versionFlag
326+ .as(IO .println(version).as(ExitCode .Success ))
327+ .orElse(
328+ runServerHandleErrors
329+ )
302330 Command (name = " LiveServer" , header = " Scala JS live server" , helpFlag = true )(finalOpts)
303331 end command
304332
0 commit comments