From 8489f44a3d8b695e91ae891662f98e0ae8b365fd Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 2 Mar 2022 10:14:55 -0800 Subject: [PATCH] Use esbuild to transpile port-data-source file, and watch for changes. --- examples/end-to-end/port-data-source.ts | 24 +++++++++++++++ generator/src/dev-server.js | 37 ++++++++++++++++++++++ generator/src/hello.ts | 5 +++ generator/src/render-worker.js | 3 +- generator/src/render.js | 20 ++++++++++-- generator/src/request-cache.js | 41 +++++++++++++++---------- package-lock.json | 1 + package.json | 3 +- src/DataSource/Port.elm | 22 ++++++++----- 9 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 examples/end-to-end/port-data-source.ts create mode 100644 generator/src/hello.ts diff --git a/examples/end-to-end/port-data-source.ts b/examples/end-to-end/port-data-source.ts new file mode 100644 index 00000000..263dbb28 --- /dev/null +++ b/examples/end-to-end/port-data-source.ts @@ -0,0 +1,24 @@ +import kleur from "kleur"; +kleur.enabled = true; + +export async function environmentVariable(name) { + const result = process.env[name]; + if (result) { + return result; + } else { + throw `No environment variable called ${kleur + .yellow() + .underline(name)}\n\nAvailable:\n\n${Object.keys(process.env) + .slice(0, 5) + .join("\n")}`; + } +} + +export async function hello(name) { + await waitFor(1000); + return `147 ${name}!!`; +} + +function waitFor(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/generator/src/dev-server.js b/generator/src/dev-server.js index 2af40a1c..3a98c0eb 100644 --- a/generator/src/dev-server.js +++ b/generator/src/dev-server.js @@ -24,6 +24,7 @@ const cookie = require("cookie"); const busboy = require("busboy"); const { createServer: createViteServer } = require("vite"); const cliVersion = require("../../package.json").version; +const esbuild = require("esbuild"); /** * @param {{ port: string; base: string; https: boolean; debug: boolean; }} options @@ -137,6 +138,40 @@ async function start(options) { base: options.base, ...viteConfig, }); + esbuild + .build({ + entryPoints: ["./port-data-source"], + entryNames: "[dir]/[name]-[hash]", + + outdir: ".elm-pages/compiled-ports", + assetNames: "[name]-[hash]", + chunkNames: "chunks/[name]-[hash]", + outExtension: { ".js": ".mjs" }, + + metafile: true, + bundle: false, + watch: true, + plugins: [ + { + name: "example", + setup(build) { + build.onEnd((result) => { + global.portsFilePath = Object.keys(result.metafile.outputs)[0]; + + clients.forEach((client) => { + client.response.write(`data: content.dat\n\n`); + }); + }); + }, + }, + ], + }) + .then((result) => { + console.log("Watching port-data-source..."); + }) + .catch((error) => { + console.error("Failed to start port-data-source watcher", error); + }); const app = connect() .use(timeMiddleware()) @@ -288,6 +323,7 @@ async function start(options) { mode: "dev-server", pathname, serverRequest, + portsFilePath: global.portsFilePath, }); readyThread.worker.on("message", (message) => { if (message.tag === "done") { @@ -379,6 +415,7 @@ async function start(options) { }); req.on("end", async function () { + // TODO run render directly instead of in worker thread await runRenderThread( await reqToJson(req, body, requestTime), pathname, diff --git a/generator/src/hello.ts b/generator/src/hello.ts new file mode 100644 index 00000000..85f1f691 --- /dev/null +++ b/generator/src/hello.ts @@ -0,0 +1,5 @@ +export function hello(): string { + console.log("HELLLO!!!!"); + + return "Hello World!"; +} diff --git a/generator/src/render-worker.js b/generator/src/render-worker.js index 5dd7613c..20edd57d 100644 --- a/generator/src/render-worker.js +++ b/generator/src/render-worker.js @@ -7,10 +7,11 @@ let Elm; global.staticHttpCache = {}; -async function run({ mode, pathname, serverRequest }) { +async function run({ mode, pathname, serverRequest, portsFilePath }) { console.time(`${threadId} ${pathname}`); try { const renderResult = await renderer( + portsFilePath, workerData.basePath, requireElm(mode), mode, diff --git a/generator/src/render.js b/generator/src/render.js index 3fa8e12c..43dc2941 100755 --- a/generator/src/render.js +++ b/generator/src/render.js @@ -30,6 +30,7 @@ module.exports = * @returns */ async function run( + portsFile, basePath, elmModule, mode, @@ -49,6 +50,7 @@ module.exports = // we can provide a fake HTTP instead of xhr2 (which is otherwise needed for Elm HTTP requests from Node) XMLHttpRequest = {}; const result = await runElmApp( + portsFile, basePath, elmModule, mode, @@ -71,6 +73,7 @@ module.exports = * @returns {Promise<({is404: boolean} & ( { kind: 'json'; contentJson: string} | { kind: 'html'; htmlString: string } | { kind: 'api-response'; body: string; }) )>} */ function runElmApp( + portsFile, basePath, elmModule, mode, @@ -161,7 +164,10 @@ function runElmApp( } } else if (fromElm.tag === "DoHttp") { const requestToPerform = fromElm.args[0]; - if (requestToPerform.url.startsWith("elm-pages-internal://")) { + if ( + requestToPerform.url !== "elm-pages-internal://port" && + requestToPerform.url.startsWith("elm-pages-internal://") + ) { runInternalJob( app, mode, @@ -171,7 +177,7 @@ function runElmApp( patternsToWatch ); } else { - runHttpJob(app, mode, requestToPerform, fs, hasFsAccess); + runHttpJob(portsFile, app, mode, requestToPerform, fs, hasFsAccess); } } else if (fromElm.tag === "Errors") { foundErrors = true; @@ -229,10 +235,18 @@ async function outputString( /** @typedef { { head: any[]; errors: any[]; contentJson: any[]; html: string; route: string; title: string; } } Arg */ -async function runHttpJob(app, mode, requestToPerform, fs, hasFsAccess) { +async function runHttpJob( + portsFile, + app, + mode, + requestToPerform, + fs, + hasFsAccess +) { pendingDataSourceCount += 1; try { const responseFilePath = await lookupOrPerform( + portsFile, mode, requestToPerform, hasFsAccess diff --git a/generator/src/request-cache.js b/generator/src/request-cache.js index 9888059b..743b15da 100644 --- a/generator/src/request-cache.js +++ b/generator/src/request-cache.js @@ -16,29 +16,36 @@ function requestToString(request) { /** * @param {Object} request */ -function fullPath(request, hasFsAccess) { +function fullPath(portsHash, request, hasFsAccess) { + const requestWithPortHash = + request.url === "elm-pages-internal://port" + ? { portsHash, ...request } + : request; if (hasFsAccess) { return path.join( process.cwd(), ".elm-pages", "http-response-cache", - requestToString(request) + requestToString(requestWithPortHash) ); } else { - return path.join("/", requestToString(request)); + return path.join("/", requestToString(requestWithPortHash)); } } /** * @param {string} mode - * @param {{url: string; headers: {[x: string]: string}; method: string; body: Body } } rawRequest + * @param {{url: string;headers: {[x: string]: string;};method: string;body: Body;}} rawRequest * @returns {Promise} + * @param {string} portsFile + * @param {boolean} hasFsAccess */ -function lookupOrPerform(mode, rawRequest, hasFsAccess) { +function lookupOrPerform(portsFile, mode, rawRequest, hasFsAccess) { const { fs } = require("./request-cache-fs.js")(hasFsAccess); return new Promise(async (resolve, reject) => { const request = toRequest(rawRequest); - const responsePath = fullPath(request, hasFsAccess); + const portsHash = portsFile.match(/-([^-]+)\.mjs$/)[1]; + const responsePath = fullPath(portsHash, request, hasFsAccess); // TODO check cache expiration time and delete and go to else if expired if (await checkFileExists(fs, responsePath)) { @@ -48,17 +55,14 @@ function lookupOrPerform(mode, rawRequest, hasFsAccess) { let portDataSource = {}; let portDataSourceFound = false; try { - portDataSource = requireUncached( - mode, - path.join(process.cwd(), "port-data-source.js") - ); + portDataSource = await import(path.join(process.cwd(), portsFile)); portDataSourceFound = true; } catch (e) {} - if (request.url.startsWith("port://")) { + if (request.url === "elm-pages-internal://port") { try { - const portName = request.url.replace(/^port:\/\//, ""); - // console.time(JSON.stringify(request.url)); + const { input, portName } = rawRequest.body.args[0]; + if (!portDataSource[portName]) { if (portDataSourceFound) { throw `DataSource.Port.send "${portName}" is not defined. Be sure to export a function with that name from port-data-source.js`; @@ -70,9 +74,7 @@ function lookupOrPerform(mode, rawRequest, hasFsAccess) { } await fs.promises.writeFile( responsePath, - JSON.stringify( - await portDataSource[portName](rawRequest.body.args[0]) - ) + JSON.stringify(jsonResponse(await portDataSource[portName](input))) ); resolve(responsePath); } catch (error) { @@ -228,4 +230,11 @@ function requireUncached(mode, filePath) { return require(filePath); } +/** + * @param {unknown} json + */ +function jsonResponse(json) { + return { bodyKind: "json", body: json }; +} + module.exports = { lookupOrPerform }; diff --git a/package-lock.json b/package-lock.json index 22663083..b13de128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "devcert": "^1.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", + "esbuild": "^0.14.23", "fs-extra": "^10.0.0", "globby": "11.0.4", "gray-matter": "^4.0.3", diff --git a/package.json b/package.json index 67e16241..98090a2b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "devcert": "^1.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", + "esbuild": "^0.14.23", "fs-extra": "^10.0.0", "globby": "11.0.4", "gray-matter": "^4.0.3", @@ -73,4 +74,4 @@ "bin": { "elm-pages": "generator/src/cli.js" } -} \ No newline at end of file +} diff --git a/src/DataSource/Port.elm b/src/DataSource/Port.elm index 5b991100..52c45f83 100644 --- a/src/DataSource/Port.elm +++ b/src/DataSource/Port.elm @@ -8,8 +8,9 @@ module DataSource.Port exposing (get) import DataSource import DataSource.Http +import DataSource.Internal.Request import Json.Decode exposing (Decoder) -import Json.Encode +import Json.Encode as Encode {-| In a vanilla Elm application, ports let you either send or receive JSON data between your Elm application and the JavaScript context in the user's browser at runtime. @@ -73,12 +74,17 @@ prefer to add ANSI color codes within the error string in an exception and it wi As with any JavaScript or NodeJS code, avoid doing blocking IO operations. For example, avoid using `fs.readFileSync`, because blocking IO can slow down your elm-pages builds and dev server. -} -get : String -> Json.Encode.Value -> Decoder b -> DataSource.DataSource b +get : String -> Encode.Value -> Decoder b -> DataSource.DataSource b get portName input decoder = - DataSource.Http.request - { url = "port://" ++ portName - , method = "GET" - , headers = [] - , body = DataSource.Http.jsonBody input + DataSource.Internal.Request.request + { name = "port" + , body = + Encode.object + [ ( "input", input ) + , ( "portName", Encode.string portName ) + ] + |> DataSource.Http.jsonBody + , expect = + decoder + |> DataSource.Http.expectJson } - (DataSource.Http.expectJson decoder)