From 45b6c3d9e4f63e280ec8a5d103cada81a4699e3b Mon Sep 17 00:00:00 2001 From: Domantas Petrauskas Date: Wed, 1 Nov 2023 15:14:29 +0200 Subject: [PATCH] Migrate esbuild web plugin serve script to parametrized version --- .../scalajsesbuild/EsbuildWebScripts.scala | 145 ++++++++++++- .../ScalaJSEsbuildWebPlugin.scala | 194 +++--------------- 2 files changed, 178 insertions(+), 161 deletions(-) diff --git a/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/EsbuildWebScripts.scala b/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/EsbuildWebScripts.scala index 8689ed8..73d1841 100644 --- a/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/EsbuildWebScripts.scala +++ b/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/EsbuildWebScripts.scala @@ -69,7 +69,7 @@ object EsbuildWebScripts { private[scalajsesbuild] def esbuildLiveReload = { // language=JS - s"""const esbuildLiveReload = ( + """const esbuildLiveReload = ( | htmlString |) => { | return htmlString @@ -102,4 +102,147 @@ object EsbuildWebScripts { |} |""".stripMargin } + + private[scalajsesbuild] def serve = { + // language=JS + """ + |const serve = async ( + | entryPoints, + | outDirectory, + | outputFilesDirectory, + | metaFileName, + | serverPort, + | serverProxyPort, + | htmlEntryPoints + |) => { + | const http = require('http'); + | const esbuild = require('esbuild'); + | const fs = require('fs'); + | const path = require('path'); + | const EventEmitter = require('events'); + | + | const reloadEventEmitter = new EventEmitter(); + | + | const plugins = [{ + | name: 'metafile-plugin', + | setup(build) { + | build.onEnd(result => { + | if (!result.metafile) { + | console.warn("Metafile missing in build result") + | fs.writeFileSync(metaFileName, '{}'); + | } else { + | fs.writeFileSync(metaFileName, JSON.stringify(result.metafile)); + | } + | }); + | } + | }]; + | + | const ctx = await esbuild.context({ + | ...esbuildOptions( + | entryPoints, + | outDirectory, + | outputFilesDirectory, + | false, + | false + | ), + | plugins: plugins + | }); + | + | await ctx.watch(); + | + | const { host, port } = await ctx.serve({ + | servedir: outDirectory, + | port: serverPort + | }); + | + | const proxy = http.createServer((req, res) => { + | const metaPath = path.join(__dirname, metaFileName); + | let meta; + | try { + | meta = JSON.parse(fs.readFileSync(metaPath)); + | } catch (error) { + | res.writeHead(500); + | res.end('META file ['+metaPath+'] not found'); + | } + | + | if (meta) { + | const forwardRequest = (path) => { + | const options = { + | hostname: host, + | port, + | path, + | method: req.method, + | headers: req.headers + | }; + | + | const multipleEntryPointsFound = htmlEntryPoints.length !== 1; + | + | if (multipleEntryPointsFound && path === "/") { + | res.writeHead(500); + | res.end('Multiple html entry points defined, unable to pick single root'); + | } else { + | if (path === '/' || path.endsWith('.html')) { + | let htmlFilePath; + | if (path === '/') { + | htmlFilePath = htmlEntryPoints[0]; + | } else { + | htmlFilePath = path; + | } + | if (htmlFilePath.startsWith('/')) { + | htmlFilePath = `.${htmlFilePath}`; + | } + | + | if (fs.existsSync(htmlFilePath)) { + | try { + | res.writeHead(200, {"Content-Type": "text/html"}); + | res.end(htmlTransform(esbuildLiveReload(fs.readFileSync(htmlFilePath)), outDirectory, meta)); + | } catch (error) { + | res.writeHead(500); + | res.end('Failed to transform html ['+error+']'); + | } + | } else { + | res.writeHead(404); + | res.end('HTML file ['+htmlFilePath+'] not found'); + | } + | } else { + | const proxyReq = http.request(options, (proxyRes) => { + | if (proxyRes.statusCode === 404 && !multipleEntryPointsFound) { + | // If esbuild 404s the request, assume it's a route needing to + | // be handled by the JS bundle, so forward a second attempt to `/`. + | return forwardRequest("/"); + | } + | + | // Otherwise esbuild handled it like a champ, so proxy the response back. + | res.writeHead(proxyRes.statusCode, proxyRes.headers); + | + | if (req.method === 'GET' && req.url === '/esbuild' && req.headers.accept === 'text/event-stream') { + | const reloadCallback = () => { + | res.write('event: reload\ndata: reload\n\n'); + | }; + | reloadEventEmitter.on('reload', reloadCallback); + | res.on('close', () => { + | reloadEventEmitter.removeListener('reload', reloadCallback); + | }); + | } + | proxyRes.pipe(res, { end: true }); + | }); + | + | req.pipe(proxyReq, { end: true }); + | } + | } + | }; + | // When we're called pass the request right through to esbuild. + | forwardRequest(req.url); + | } + | }); + | + | // Start our proxy server at the specified `listen` port. + | proxy.listen(serverProxyPort); + | + | console.log(`Started esbuild serve process [http://localhost:${serverProxyPort}]`); + | + | return reloadEventEmitter; + |}; + |""".stripMargin + } } diff --git a/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/ScalaJSEsbuildWebPlugin.scala b/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/ScalaJSEsbuildWebPlugin.scala index 7123a38..97c8833 100644 --- a/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/ScalaJSEsbuildWebPlugin.scala +++ b/sbt-scalajs-esbuild-web/src/main/scala/scalajsesbuild/ScalaJSEsbuildWebPlugin.scala @@ -157,176 +157,50 @@ object ScalaJSEsbuildWebPlugin extends AutoPlugin { Seq( stageTask / esbuildServeScript := { - val targetDir = (esbuildInstall / crossTarget).value - - val entryPoints = jsFileNames(stageTask.value.data) - .map(targetDir / _) - .toSeq - val outdir = + val stageTaskReport = stageTask.value.data + val entryPoints = jsFileNames(stageTaskReport).toSeq + val entryPointsJsArray = + entryPoints.map("'" + _ + "'").mkString("[", ",", "]") + val targetDirectory = (esbuildInstall / crossTarget).value + val outputDirectory = (stageTask / esbuildServeStart / crossTarget).value - + val relativeOutputDirectory = + targetDirectory + .relativize(outputDirectory) + .getOrElse( + sys.error( + s"Target directory [$targetDirectory] must be parent directory of output directory [$outputDirectory]" + ) + ) val htmlEntryPoints = esbuildBundleHtmlEntryPoints.value + require( + !htmlEntryPoints.forall(_.isAbsolute), + "HTML entry point paths must be relative" + ) + val htmlEntryPointsJsArray = + htmlEntryPoints.map("'" + _ + "'").mkString("[", ",", "]") // language=JS s""" - |const http = require('http'); - |const esbuild = require('esbuild'); - |const jsdom = require("jsdom") - |const { JSDOM } = jsdom; - |const fs = require('fs'); - |const path = require('path'); - | - |$htmlTransformScript - | - |const esbuildLiveReload = (htmlString) => { - | return htmlString - | .toString() - | .replace("", ` - | - | - | `); - |} - | - |const serve = async () => { - | // Start esbuild's local web server. Random port will be chosen by esbuild. - | - | const plugins = [{ - | name: 'metafile-plugin', - | setup(build) { - | build.onEnd(result => { - | const metafileName = 'sbt-scalajs-esbuild-serve-meta.json'; - | if (!result.metafile) { - | console.warn("Metafile missing in build result") - | fs.writeFileSync(metafileName, '{}'); - | } else { - | fs.writeFileSync(metafileName, JSON.stringify(result.metafile)); - | } - | }); - | }, - | }]; - | - | const ctx = await esbuild.context({ - |${esbuildOptions( - entryPoints = entryPoints, - outdir = outdir, - outputFilesDirectory = Some("assets"), - hashOutputFiles = false, - minify = false, - spaces = 6 - )} - | plugins: plugins, - | }); - | - | await ctx.watch() - | - | const { host, port } = await ctx.serve({ - | servedir: '${outdir.toPath.toStringEscaped}', - | port: 8001 - | }); - | - | // Create a second (proxy) server that will forward requests to esbuild. - | const proxy = http.createServer((req, res) => { - | const metaPath = path.join(__dirname, 'sbt-scalajs-esbuild-serve-meta.json'); - | let meta; - | try { - | meta = JSON.parse(fs.readFileSync(metaPath)); - | } catch (error) { - | res.writeHead(500); - | res.end('META file ['+metaPath+'] not found'); - | } - | - | if (meta) { - | // forwardRequest forwards an http request through to esbuid. - | const forwardRequest = (path) => { - | const options = { - | hostname: host, - | port, - | path, - | method: req.method, - | headers: req.headers, - | }; - | - | const multipleEntryPointsFound = ${htmlEntryPoints.size > 1}; - | - | if (multipleEntryPointsFound && path === "/") { - | res.writeHead(500); - | res.end('Multiple html entry points defined, unable to pick single root'); - | } else { - | if (path === "/" || path.endsWith(".html")) { - | let file; - | if (path === "/") { - | file = '/${htmlEntryPoints.head}'; - | } else { - | file = path; - | } - | - | const htmlFilePath = "."+file; - | - | if (fs.existsSync(htmlFilePath)) { - | try { - | res.writeHead(200, {"Content-Type": "text/html"}); - | res.end(htmlTransform(esbuildLiveReload(fs.readFileSync(htmlFilePath)), '${outdir.toPath.toStringEscaped}', meta)); - | } catch (error) { - | res.writeHead(500); - | res.end('Failed to transform html ['+error+']'); - | } - | } else { - | res.writeHead(404); - | res.end('HTML file ['+htmlFilePath+'] not found'); - | } - | } else { - | const proxyReq = http.request(options, (proxyRes) => { - | if (proxyRes.statusCode === 404 && !multipleEntryPointsFound) { - | // If esbuild 404s the request, assume it's a route needing to - | // be handled by the JS bundle, so forward a second attempt to `/`. - | return forwardRequest("/"); - | } + |${EsbuildScripts.esbuildOptions} | - | // Otherwise esbuild handled it like a champ, so proxy the response back. - | res.writeHead(proxyRes.statusCode, proxyRes.headers); - | proxyRes.pipe(res, { end: true }); - | }); + |${EsbuildScripts.bundle} | - | req.pipe(proxyReq, { end: true }); - | } - | } - | }; - | // When we're called pass the request right through to esbuild. - | forwardRequest(req.url); - | } - | }); + |${EsbuildWebScripts.htmlTransform} | - | // Start our proxy server at the specified `listen` port. - | proxy.listen(8000); + |${EsbuildWebScripts.esbuildLiveReload} | - | console.log("Started esbuild serve process [http://localhost:8000]"); - |}; + |${EsbuildWebScripts.serve} | - |// Serves all content from $outdir on :8000. - |// If esbuild 404s the request, the request is attempted again - |// from `/` assuming that it's an SPA route needing to be handled by the root bundle. - |serve(); + |serve( + | $entryPointsJsArray, + | ${s"'$relativeOutputDirectory'"}, + | 'assets', + | 'sbt-scalajs-esbuild-serve-meta.json', + | 8001, + | 8000, + | $htmlEntryPointsJsArray + |); |""".stripMargin }, stageTask / esbuildServeStart / crossTarget := (esbuildInstall / crossTarget).value / "www",