Include Html.Lazy thunk evaluation in server-side rendering in dev server.

This commit is contained in:
Dillon Kearns 2022-07-08 09:37:08 -07:00
parent 344341a93a
commit 4c9986702b
2 changed files with 201 additions and 31 deletions

View File

@ -1,8 +1,9 @@
const spawnCallback = require("cross-spawn").spawn; const spawnCallback = require("cross-spawn").spawn;
const fs = require("fs"); const fs = require("fs");
const fsHelpers = require("./dir-helpers.js");
const fsPromises = require("fs").promises;
const path = require("path"); const path = require("path");
const kleur = require("kleur"); const kleur = require("kleur");
const debug = true;
const { inject } = require("elm-hot"); const { inject } = require("elm-hot");
const pathToClientElm = path.join( const pathToClientElm = path.join(
process.cwd(), process.cwd(),
@ -10,21 +11,6 @@ const pathToClientElm = path.join(
"browser-elm.js" "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) { async function compileElmForBrowser(options) {
await runElm(options, "./.elm-pages/Main.elm", pathToClientElm); await runElm(options, "./.elm-pages/Main.elm", pathToClientElm);
return fs.promises.writeFile( return fs.promises.writeFile(
@ -32,13 +18,147 @@ async function compileElmForBrowser(options) {
inject(await fs.promises.readFile(pathToClientElm, "utf-8")).replace( inject(await fs.promises.readFile(pathToClientElm, "utf-8")).replace(
/return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_FORM_TO_STRING.\)/g, /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 " + "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 (true
? "_Json_wrap([...(appendSubmitter(new FormData(_Json_unwrap(event).target), _Json_unwrap(event)))])" ? // TODO remove hardcoding
"_Json_wrap([...(appendSubmitter(new FormData(_Json_unwrap(event).target), _Json_unwrap(event)))])"
: "[...(new FormData(event.target))") : "[...(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} elmEntrypointPath
* @param {string} outputPath * @param {string} outputPath
@ -56,6 +176,7 @@ async function runElm(options, elmEntrypointPath, outputPath, cwd) {
"--output", "--output",
outputPath, outputPath,
...(options.debug ? ["--debug"] : []), ...(options.debug ? ["--debug"] : []),
...(options.optimize ? ["--optimize"] : []),
"--report", "--report",
"json", "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 = { module.exports = {
spawnElmMake,
compileElmForBrowser, compileElmForBrowser,
runElmReview, runElmReview,
compileCliApp,
}; };
/** /**

View File

@ -4,9 +4,9 @@ const which = require("which");
const chokidar = require("chokidar"); const chokidar = require("chokidar");
const { URL } = require("url"); const { URL } = require("url");
const { const {
spawnElmMake,
compileElmForBrowser, compileElmForBrowser,
runElmReview, runElmReview,
compileCliApp,
} = require("./compile-elm.js"); } = require("./compile-elm.js");
const http = require("http"); const http = require("http");
const https = require("https"); const https = require("https");
@ -64,7 +64,18 @@ async function start(options) {
process.exit(1); process.exit(1);
} }
let clientElmMakeProcess = compileElmForBrowser(options); 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); watchElmSourceDirs(true);
async function setup() { async function setup() {
@ -107,14 +118,6 @@ async function start(options) {
watcher.add(sourceDirs); watcher.add(sourceDirs);
} }
async function compileCliApp(options) {
await spawnElmMake(
options,
".elm-pages/Main.elm",
"elm.js",
"elm-stuff/elm-pages/"
);
}
const viteConfig = await import( const viteConfig = await import(
path.join(process.cwd(), "elm-pages.config.mjs") path.join(process.cwd(), "elm-pages.config.mjs")
) )
@ -211,7 +214,13 @@ async function start(options) {
try { try {
await codegen.generate(options.base); await codegen.generate(options.base);
clientElmMakeProcess = compileElmForBrowser(options); 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]) Promise.all([clientElmMakeProcess, pendingCliCompile])
.then(() => { .then(() => {
@ -237,7 +246,13 @@ async function start(options) {
pendingCliCompile = Promise.reject(errorJson); pendingCliCompile = Promise.reject(errorJson);
} else { } else {
clientElmMakeProcess = compileElmForBrowser(options); 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]) Promise.all([clientElmMakeProcess, pendingCliCompile])