Skip to content

Commit

Permalink
Migrate esbuild web plugin serve script to parametrized version
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrdom committed Nov 1, 2023
1 parent 7902cea commit 45b6c3d
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 161 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ object EsbuildWebScripts {

private[scalajsesbuild] def esbuildLiveReload = {
// language=JS
s"""const esbuildLiveReload = (
"""const esbuildLiveReload = (
| htmlString
|) => {
| return htmlString
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("</head>", `
| <script type="text/javascript">
| // Based on https://esbuild.github.io/api/#live-reload
| new EventSource('/esbuild').addEventListener('change', e => {
| const { added, removed, updated } = JSON.parse(e.data)
|
| if (!added.length && !removed.length && updated.length === 1) {
| for (const link of document.getElementsByTagName("link")) {
| const url = new URL(link.href)
|
| if (url.host === location.host && url.pathname === updated[0]) {
| const next = link.cloneNode()
| next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
| next.onload = () => link.remove()
| link.parentNode.insertBefore(next, link.nextSibling)
| return
| }
| }
| }
|
| location.reload()
| })
| </script>
| </head>
| `);
|}
|
|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",
Expand Down

0 comments on commit 45b6c3d

Please sign in to comment.