diff --git a/generator/src/compile-elm.js b/generator/src/compile-elm.js index ef40c6a9..9b2e7bc7 100644 --- a/generator/src/compile-elm.js +++ b/generator/src/compile-elm.js @@ -1,8 +1,9 @@ const spawnCallback = require("cross-spawn").spawn; const fs = require("fs"); +const fsHelpers = require("./dir-helpers.js"); +const fsPromises = require("fs").promises; const path = require("path"); const kleur = require("kleur"); -const debug = true; const { inject } = require("elm-hot"); const pathToClientElm = path.join( process.cwd(), @@ -10,21 +11,6 @@ const pathToClientElm = path.join( "browser-elm.js" ); -async function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) { - const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath; - await runElm(options, elmEntrypointPath, outputPath, cwd); - - await fs.promises.writeFile( - fullOutputPath, - (await fs.promises.readFile(fullOutputPath, "utf-8")) - .replace( - /return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g, - "return " + (debug ? "_Json_wrap(x)" : "x") - ) - .replace(`console.log('App dying')`, "") - ); -} - async function compileElmForBrowser(options) { await runElm(options, "./.elm-pages/Main.elm", pathToClientElm); return fs.promises.writeFile( @@ -32,13 +18,147 @@ async function compileElmForBrowser(options) { inject(await fs.promises.readFile(pathToClientElm, "utf-8")).replace( /return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_FORM_TO_STRING.\)/g, "let appendSubmitter = (myFormData, event) => { event.submitter && event.submitter.name && event.submitter.name.length > 0 ? myFormData.append(event.submitter.name, event.submitter.value) : myFormData; return myFormData }; return " + - (debug - ? "_Json_wrap([...(appendSubmitter(new FormData(_Json_unwrap(event).target), _Json_unwrap(event)))])" + (true + ? // TODO remove hardcoding + "_Json_wrap([...(appendSubmitter(new FormData(_Json_unwrap(event).target), _Json_unwrap(event)))])" : "[...(new FormData(event.target))") ) ); } +async function compileCliApp( + options, + elmEntrypointPath, + outputPath, + cwd, + readFrom +) { + await compileElm(options, elmEntrypointPath, outputPath, cwd); + + const elmFileContent = await fsPromises.readFile(readFrom, "utf-8"); + // Source: https://github.com/elm-explorations/test/blob/d5eb84809de0f8bbf50303efd26889092c800609/src/Elm/Kernel/HtmlAsJson.js + const forceThunksSource = ` _HtmlAsJson_toJson(x) +} + + var virtualDomKernelConstants = + { + nodeTypeTagger: 4, + nodeTypeThunk: 5, + kids: "e", + refs: "l", + thunk: "m", + node: "k", + value: "a" + } + +function forceThunks(vNode) { + if (typeof vNode !== "undefined" && vNode.$ === "#2") { + // This is a tuple (the kids : List (String, Html) field of a Keyed node); recurse into the right side of the tuple + vNode.b = forceThunks(vNode.b); + } + if (typeof vNode !== 'undefined' && vNode.$ === virtualDomKernelConstants.nodeTypeThunk && !vNode[virtualDomKernelConstants.node]) { + // This is a lazy node; evaluate it + var args = vNode[virtualDomKernelConstants.thunk]; + vNode[virtualDomKernelConstants.node] = vNode[virtualDomKernelConstants.thunk].apply(args); + // And then recurse into the evaluated node + vNode[virtualDomKernelConstants.node] = forceThunks(vNode[virtualDomKernelConstants.node]); + } + if (typeof vNode !== 'undefined' && vNode.$ === virtualDomKernelConstants.nodeTypeTagger) { + // This is an Html.map; recurse into the node it is wrapping + vNode[virtualDomKernelConstants.node] = forceThunks(vNode[virtualDomKernelConstants.node]); + } + if (typeof vNode !== 'undefined' && typeof vNode[virtualDomKernelConstants.kids] !== 'undefined') { + // This is something with children (either a node with kids : List Html, or keyed with kids : List (String, Html)); + // recurse into the children + vNode[virtualDomKernelConstants.kids] = vNode[virtualDomKernelConstants.kids].map(forceThunks); + } + return vNode; +} + +function _HtmlAsJson_toJson(html) { +`; + + await fsPromises.writeFile( + readFrom, + elmFileContent + .replace( + /return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g, + "return " + + // TODO should the logic for this be `if options.optimize`? Or does the first case not make sense at all? + (true + ? `${forceThunksSource} + return _Json_wrap(forceThunks(html)); +` + : `${forceThunksSource} +return forceThunks(html); +`) + ) + .replace(/console\.log..App dying../, "") + ); +} + +/** + * @param {string} elmEntrypointPath + * @param {string} outputPath + * @param {string | undefined} cwd + */ +async function compileElm(options, elmEntrypointPath, outputPath, cwd) { + await spawnElmMake(options, elmEntrypointPath, outputPath, cwd); + if (!options.debug) { + // TODO maybe pass in a boolean argument for whether it's build or dev server, and only do eol2 for build + // await elmOptimizeLevel2(outputPath, cwd); + } +} + +function spawnElmMake(options, elmEntrypointPath, outputPath, cwd) { + return new Promise(async (resolve, reject) => { + const subprocess = spawnCallback( + `lamdera`, + [ + "make", + elmEntrypointPath, + "--output", + outputPath, + // TODO use --optimize for prod build + ...(options.debug ? ["--debug"] : []), + "--report", + "json", + ], + { + // ignore stdout + // stdio: ["inherit", "ignore", "inherit"], + + cwd: cwd, + } + ); + if (await fsHelpers.fileExists(outputPath)) { + await fsPromises.unlink(outputPath, { + force: true /* ignore errors if file doesn't exist */, + }); + } + let commandOutput = ""; + + subprocess.stderr.on("data", function (data) { + commandOutput += data; + }); + subprocess.on("error", function () { + reject(commandOutput); + }); + + subprocess.on("close", async (code) => { + if ( + code == 0 && + (await fsHelpers.fileExists(outputPath)) && + commandOutput === "" + ) { + resolve(); + } else { + reject(commandOutput); + } + }); + }); +} + /** * @param {string} elmEntrypointPath * @param {string} outputPath @@ -56,6 +176,7 @@ async function runElm(options, elmEntrypointPath, outputPath, cwd) { "--output", outputPath, ...(options.debug ? ["--debug"] : []), + ...(options.optimize ? ["--optimize"] : []), "--report", "json", ], @@ -129,10 +250,44 @@ async function runElmReview(cwd) { }); } +function elmOptimizeLevel2(outputPath, cwd) { + return new Promise((resolve, reject) => { + const optimizedOutputPath = outputPath + ".opt"; + const subprocess = spawnCallback( + `elm-optimize-level-2`, + [outputPath, "--output", optimizedOutputPath], + { + // ignore stdout + // stdio: ["inherit", "ignore", "inherit"], + + cwd: cwd, + } + ); + let commandOutput = ""; + + subprocess.stderr.on("data", function (data) { + commandOutput += data; + }); + + subprocess.on("close", async (code) => { + if ( + code === 0 && + commandOutput === "" && + (await fsHelpers.fileExists(optimizedOutputPath)) + ) { + await fs.promises.copyFile(optimizedOutputPath, outputPath); + resolve(); + } else { + reject(commandOutput); + } + }); + }); +} + module.exports = { - spawnElmMake, compileElmForBrowser, runElmReview, + compileCliApp, }; /** diff --git a/generator/src/dev-server.js b/generator/src/dev-server.js index d45fa042..a9f73ba0 100644 --- a/generator/src/dev-server.js +++ b/generator/src/dev-server.js @@ -4,9 +4,9 @@ const which = require("which"); const chokidar = require("chokidar"); const { URL } = require("url"); const { - spawnElmMake, compileElmForBrowser, runElmReview, + compileCliApp, } = require("./compile-elm.js"); const http = require("http"); const https = require("https"); @@ -64,7 +64,18 @@ async function start(options) { process.exit(1); } let clientElmMakeProcess = compileElmForBrowser(options); - let pendingCliCompile = compileCliApp(options); + console.log({ options }); + let pendingCliCompile = compileCliApp( + options, + ".elm-pages/Main.elm", + + path.join(process.cwd(), "elm-stuff/elm-pages/", "elm.js"), + + // "elm.js", + "elm-stuff/elm-pages/", + path.join("elm-stuff/elm-pages/", "elm.js") + ); + watchElmSourceDirs(true); async function setup() { @@ -107,14 +118,6 @@ async function start(options) { watcher.add(sourceDirs); } - async function compileCliApp(options) { - await spawnElmMake( - options, - ".elm-pages/Main.elm", - "elm.js", - "elm-stuff/elm-pages/" - ); - } const viteConfig = await import( path.join(process.cwd(), "elm-pages.config.mjs") ) @@ -211,7 +214,13 @@ async function start(options) { try { await codegen.generate(options.base); clientElmMakeProcess = compileElmForBrowser(options); - pendingCliCompile = compileCliApp(options); + pendingCliCompile = compileCliApp( + options, + ".elm-pages/Main.elm", + "elm.js", + "elm-stuff/elm-pages/", + path.join("elm-stuff/elm-pages/.elm-pages/", "elm.js") + ); Promise.all([clientElmMakeProcess, pendingCliCompile]) .then(() => { @@ -237,7 +246,13 @@ async function start(options) { pendingCliCompile = Promise.reject(errorJson); } else { clientElmMakeProcess = compileElmForBrowser(options); - pendingCliCompile = compileCliApp(options); + pendingCliCompile = compileCliApp( + options, + ".elm-pages/Main.elm", + "elm.js", + "elm-stuff/elm-pages/", + path.join("elm-stuff/elm-pages/.elm-pages/", "elm.js") + ); } Promise.all([clientElmMakeProcess, pendingCliCompile])