Skip to content

Commit 51eb06b

Browse files
CopilotQuafadas
andauthored
Default CLI workspace root to cwd and generate a stable session UUID for DevTools integration OOTB (#74)
* Initial plan * Set good defaults for CLI devtools workspace (cwd and session UUID) Agent-Logs-Url: https://github.com/Quafadas/live-server-scala-cli-js/sessions/593d622f-ea31-4d85-9cf6-1eec429fb787 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> * Address feedback: merge main, none sentinel, scala file check, integration test, extract constant Agent-Logs-Url: https://github.com/Quafadas/live-server-scala-cli-js/sessions/d59529ce-98a4-4160-80a2-f341cefcb089 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> * . * . * . * . * . * . * . * . * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> Co-authored-by: Simon Parten <quafadas@gmail.com>
1 parent f7ff99a commit 51eb06b

11 files changed

Lines changed: 179 additions & 31 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"ghcr.io/devcontainers/features/java:1": {
2626
"version": 21
2727
},
28-
"ghcr.io/devcontainers-contrib/features/scalacli-sdkman:2":{},
28+
"ghcr.io/devcontainers-extra/features/scalacli-sdkman:2":{},
2929
"ghcr.io/guiyomh/features/just:0": {},
3030
"./features/mill": {}
3131
// //, "ghcr.io/devcontainers-contrib/features/scalacli-sdkman:2": {}

.github/copilot-instructions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
- Auto-open browser on startup
1010
- Support for Scala CLI, Mill, and no-build-tool modes
1111

12-
ToDo: Content hashing
13-
1412
## Build System: Mill
1513

1614
This project uses **Mill** (version 1.0.5) as its build tool. The Mill wrapper script (`./mill`) is checked in. JVM 21 is required.
@@ -86,10 +84,12 @@ live-server-scala-cli-js/
8684

8785
## Technology Stack
8886

89-
The typelevel stack -
87+
The typelevel stack.
9088

9189
## Testing
9290

91+
_AWLAYS_ add tests for each change.
92+
9393
Tests use **Munit** with **Cats Effect** and **Playwright** for browser integration tests.
9494

9595
Before running tests, you'll probably need to make sure that playwright is installed and ready. It should be installed as part of the `copilot-setup-steps` workflow.

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
fetch-depth: 0
4242

4343
- name: Install required libraries
44-
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libgraphene-1.0-dev libavif-dev
44+
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libgraphene-1.0-dev libavif-dev libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
4545

4646
- name: Install binaryen (wasm-opt)
4747
run: |
@@ -66,7 +66,7 @@ jobs:
6666
playwright-${{ runner.os }}-
6767
6868
- name: Install Playwright (browsers + system deps)
69-
run: ./mill PlaywrightSetup install --with-deps chromium-headless-shell
69+
run: ./mill PlaywrightSetup install chromium-headless-shell
7070

7171
- name: formatCheck
7272
run: ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources

.github/workflows/copilot-setup-steps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
playwright-${{ runner.os }}-
5555
5656
- name: Install Playwright
57-
run: cs launch com.microsoft.playwright:playwright:1.59.0 -M "com.microsoft.playwright.CLI" -- install --with-deps
57+
run: cs launch com.microsoft.playwright:playwright:1.59.0 -M "com.microsoft.playwright.CLI" -- install
5858

5959
- name: Install binaryen
6060
run: sudo apt-get update && sudo apt-get install -y binaryen

plugin/src/ScalaJsWebAppModule.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ trait ScalaJsWebAppModule extends InMemoryFastLinkHashScalaJSModule with ScalaJs
5555
logLevel = logLevel(),
5656
logFile = logFile().fold[Option[String]](None)(p => Some(p.path.toString)),
5757
customRefresh = Some(updateServer),
58-
devToolsWorkspace = Some((moduleDir.toString(), devToolsUuid())),
58+
devToolsWorkspaceRoot = Some(moduleDir.toString()),
59+
devToolsWorkspaceUuid = Some(devToolsUuid()),
5960
inMemoryFiles = Some(hashedOutputFiles)
6061
)
6162
}

plugin/src/refresh_plugin.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ trait ScalaJsRefreshModule extends ScalaJSConfigModule:
272272
logFile = logFile().fold[Option[String]](None)(p => Some(p.path.toString)),
273273
customRefresh = Some(updateServer),
274274
customAssetRefresh = Some(updateAsset),
275-
devToolsWorkspace = Some((moduleDir.toString(), devToolsUuid()))
275+
devToolsWorkspaceRoot = Some(moduleDir.toString()),
276+
devToolsWorkspaceUuid = Some(devToolsUuid())
276277
)
277278
}
278279

sjsls/package.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ object `package` extends Module:
4545
)
4646
def mvnDeps = super.mvnDeps() ++ Seq(
4747
mvn"com.microsoft.playwright:playwright:${V.pwV}",
48-
mvn"com.microsoft.playwright:driver-bundle:${V.pwV}"
48+
// mvn"com.microsoft.playwright:driver-bundle:${V.pwV}"
4949
)
5050
override def resources = super.resources()
5151

sjsls/src/CliOpts.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.comcast.ip4s.Port
44
import com.monovore.decline.Opts
55

66
private[sjsls] object CliOps:
7+
val defaultWorkspaceRoot: String = sys.props.getOrElse("user.dir", ".")
8+
79
val logLevelOpt: Opts[String] = Opts
810
.option[String]("log-level", help = "The log level. info, debug, error, trace)")
911
.withDefault("info")
@@ -146,14 +148,14 @@ private[sjsls] object CliOps:
146148
val workspaceRootOpt: Opts[Option[String]] = Opts
147149
.option[String](
148150
"workspace-root",
149-
"Absolute path to the project source root for Chrome DevTools automatic workspace folder integration. When provided, serves `/.well-known/appspecific/com.chrome.devtools.json`."
151+
"Absolute path to the project source root for Chrome DevTools automatic workspace folder integration. Defaults to the current working directory when it contains at least one `.scala` file. Pass `none` to disable."
150152
)
151153
.orNone
152154

153155
val workspaceUuidOpt: Opts[Option[String]] = Opts
154156
.option[String](
155157
"workspace-uuid",
156-
"UUID to use in the Chrome DevTools workspace descriptor. If omitted a random UUID is generated. Only used when --workspace-root is also supplied."
158+
"UUID to use in the Chrome DevTools workspace descriptor. If omitted a random UUID is generated at startup and reused for the lifetime of this CLI session."
157159
)
158160
.orNone
159161

sjsls/src/LiveServerConfig.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ case class LiveServerConfig(
2727
logFile: Option[String] = None,
2828
customRefresh: Option[Topic[IO, Unit]] = None,
2929
customAssetRefresh: Option[Topic[IO, String]] = None,
30-
devToolsWorkspace: Option[(String, String)] = None,
30+
devToolsWorkspaceRoot: Option[String] = None,
31+
devToolsWorkspaceUuid: Option[String] = None,
3132
inMemoryFiles: Option[java.util.concurrent.ConcurrentHashMap[String, Array[Byte]]] = None
3233
)

sjsls/src/liveServer.scala

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ object LiveServer extends IOApp:
2727

2828
import CliOps.*
2929

30+
private val sessionUuid: String = java.util.UUID.randomUUID().toString
31+
3032
private def buildServer(httpApp: HttpApp[IO], port: Port) = EmberServerBuilder
3133
.default[IO]
3234
.withHttp2
@@ -57,14 +59,58 @@ object LiveServer extends IOApp:
5759
logFileOpt,
5860
None.pure[Opts],
5961
None.pure[Opts],
60-
(workspaceRootOpt, workspaceUuidOpt).mapN {
61-
case (Some(root), uuidOpt) =>
62-
val uuid = uuidOpt.getOrElse(java.util.UUID.randomUUID().toString)
63-
Some((root, uuid))
64-
case _ => None
65-
},
66-
None.pure[Opts]
67-
).mapN(LiveServerConfig.apply)
62+
workspaceRootOpt,
63+
workspaceUuidOpt
64+
).mapN {
65+
(
66+
baseDir,
67+
outDir,
68+
port,
69+
proxyPortTarget,
70+
proxyPathMatchPrefix,
71+
clientRoutingPrefix,
72+
logLevel,
73+
buildTool,
74+
openBrowserAt,
75+
preventBrowserOpen,
76+
extraBuildArgs,
77+
millModuleName,
78+
stylesDir,
79+
indexHtmlTemplate,
80+
buildToolInvocation,
81+
injectPreloads,
82+
dezombify,
83+
logFile,
84+
customRefresh,
85+
customAssetRefresh,
86+
workspaceRoot,
87+
workspaceUuid
88+
) =>
89+
LiveServerConfig(
90+
baseDir,
91+
outDir,
92+
port,
93+
proxyPortTarget,
94+
proxyPathMatchPrefix,
95+
clientRoutingPrefix,
96+
logLevel,
97+
buildTool,
98+
openBrowserAt,
99+
preventBrowserOpen,
100+
extraBuildArgs,
101+
millModuleName,
102+
stylesDir,
103+
indexHtmlTemplate,
104+
buildToolInvocation,
105+
injectPreloads,
106+
dezombify,
107+
logFile,
108+
customRefresh,
109+
customAssetRefresh,
110+
workspaceRoot,
111+
workspaceUuid
112+
)
113+
}
68114

69115
def main(lsc: LiveServerConfig): Resource[IO, Server] =
70116

@@ -82,26 +128,22 @@ object LiveServer extends IOApp:
82128
.replace()
83129

84130
val server = for
85-
_ <- logger
86-
.debug(
87-
lsc.toString()
88-
)
89-
.toResource
131+
_ <- logger.debug("[liveServer] Starting with configuration: " + lsc.toString).toResource
90132

91133
_ <- Resource
92134
.pure[IO, Boolean](lsc.dezombify)
93135
.flatMap(
94136
if _ then
95-
Resource.eval(IO.println(s"Attempt to kill off process on port ${lsc.port}")) >>
137+
scribe.cats[IO].info(s"[dezombify] Attempt to kill off process on port ${lsc.port}").toResource >>
96138
dezombify(lsc.port)
97-
else scribe.cats[IO].debug(s"Assuming port ${lsc.port} is free").toResource
139+
else scribe.cats[IO].debug(s"[dezombify] Assuming port ${lsc.port} is free").toResource
98140
)
99141
fileToHashRef <- Ref[IO].of(Map.empty[String, String]).toResource
100142
refreshTopic <- lsc
101143
.customRefresh
102144
.fold(Topic[IO, Unit])(
103145
t =>
104-
scribe.cats[IO].debug("Custom refresh topic supplied — wrapping with debug tap") >>
146+
scribe.cats[IO].debug("[refreshTopic] Custom refresh topic supplied — wrapping with debug tap") >>
105147
IO(t)
106148
)
107149
.toResource
@@ -192,6 +234,27 @@ object LiveServer extends IOApp:
192234
// Plugin-provided inMemoryFiles take priority; CLI-created is the fallback.
193235
effectiveInMemoryFiles = lsc.inMemoryFiles.orElse(cliInMemoryFiles)
194236

237+
// Resolve the effective devtools workspace.
238+
// None → default to baseDir (the project root passed in config)
239+
// Some("none") → caller explicitly opted out
240+
// Some(root) → use the supplied path verbatim
241+
effectiveDevToolsWorkspace <- lsc.devToolsWorkspaceRoot match
242+
case Some("none") =>
243+
logger.debug("[devtools] workspace-root=none — workspace integration explicitly disabled").toResource >>
244+
Resource.pure[IO, Option[(String, String)]](Option.empty[(String, String)])
245+
246+
case Some(root) =>
247+
val uuid = lsc.devToolsWorkspaceUuid.getOrElse(sessionUuid)
248+
logger.debug(s"[devtools] Explicit workspace-root supplied. root=$root uuid=$uuid").toResource >>
249+
Resource.pure[IO, Option[(String, String)]](Some((root, uuid)))
250+
case None =>
251+
val root = baseDirPath.toString
252+
val uuid = lsc.devToolsWorkspaceUuid.getOrElse(sessionUuid)
253+
logger
254+
.debug(s"[devtools] No workspace-root supplied; defaulting to baseDir. root=$root uuid=$uuid")
255+
.toResource >>
256+
Resource.pure[IO, Option[(String, String)]](Some((root, uuid)))
257+
195258
app <- routes(
196259
outDirString,
197260
refreshTopic,
@@ -202,7 +265,7 @@ object LiveServer extends IOApp:
202265
lsc.clientRoutingPrefix,
203266
lsc.injectPreloads,
204267
lsc.buildTool,
205-
lsc.devToolsWorkspace,
268+
effectiveDevToolsWorkspace,
206269
effectiveInMemoryFiles
207270
)(logger)
208271

@@ -230,7 +293,7 @@ object LiveServer extends IOApp:
230293
case _ => Resource.unit[IO]
231294

232295
// _ <- stylesDir.fold(Resource.unit[IO])(sd => fileWatcher(fs2.io.file.Path(sd), mr))
233-
_ <- logger.info(s"Start dev server on http://localhost:${lsc.port}").toResource
296+
_ <- logger.info(s"[liveServer] Start dev server on http://localhost:${lsc.port}").toResource
234297
server <- buildServer(app.orNotFound, lsc.port)
235298

236299
_ <- IO.whenA(!lsc.preventBrowserOpen)(openBrowser(Some(lsc.openBrowserAt), lsc.port)(logger)).toResource

0 commit comments

Comments
 (0)