Episode 1: Projektsetup für den Elm Tetris Clon
In dieser Episode stelle ich mein hot code replacement setup für lokale Elm Entwicklung vor. Nachdem ich etwas mit den Einschränkungen von elm reactor
gekämpft habe, habe ich mir ein kleines boiler plate setup gebastelt mit dem ich kurze Feedbackzyklen erhalte.
Hier ist der Link zu der “coming from JavaScript” Site die die Unterschiede zwischen Elm und JavaScript Syntax erklärt.
Falls du mein boilerplate verwenden möchtest kannst du das hot_elm
Verzeichnis aus meinem “Elm nano examples” Repository verwenden.
Dann einfach die ./bin/build.sh
(or ./bin/build_linux
) im Prjektverzeichnis ausführen. Das Script lauscht auf Änderungen bis du es mit CTRL+C
abbrichst
In der Shell sollte es ungefähr so aussehen:
--> ./bin/build.sh
Compiling ⚔️
Compiled without errors
^C
`-->
Danach kannst Du die index.html
direkt im Browser öffnen. Die Elm App wird automatisch neu geladen und Buildfehler werden angezeigt falls welche aufgetreten sind.
Der Hauptakteur des Setups ist das besagte Shellskript. Es beobachtet das src
Verzeichnis und löst ein Build as falls sich eine Datei ändert.
build.sh
Visit build.sh
in context with the other files for this nano example on Github
#!/bin/sh
trap ctrl_c INT
function ctrl_c() {
exit 0
}
function append() {
ASSETS="$ASSETS\n$1"
}
function report() {
touch tmp/build.log
ERRORS=`cat tmp/build.log`
if [ -n "$ERRORS" ]; then
echo "Comiled with errors"
# to also print errors in console we just compile a second time
elm make src/Main.elm
VALUE=`date -r tmp/build.log`
printf "refresh('" > tmp/timestamp.js
printf "$VALUE" >> tmp/timestamp.js
printf "', " >> tmp/timestamp.js
cat tmp/build.log >> tmp/timestamp.js
printf ");" >> tmp/timestamp.js
else
echo "Compiled without errors"
VALUE=`date -r elm.js`
TIMESTAMP_JS_TEMPLATE="refresh('${VALUE}')"
INTERPOLATED=`echo "${TIMESTAMP_JS_TEMPLATE}" | sed "s/VALUE/${VALUE}/" | sed "s/ERROR//" `
echo "$INTERPOLATED" > tmp/timestamp.js
fi
}
function buildCode() {
echo "Compiling ⚔️"
elm make src/Main.elm --output=elm.js --report=json 2> tmp/build.log
report
}
while true; do
buildCode
fswatch --event PlatformSpecific src/ assets/ -1
done
Den anderen Teil spielt eine JavaScript Funktion die eine Datei pollt die bei jedem Build neu geschrieben wird. Sie enthält einen Zeitstempel und eventuelle Buildfehler.
loader.js
Visit loader.js
in context with the other files for this nano example on Github
function loadElm() {
window.Elm = undefined;
const scriptTag = mkScriptTag('elm-include', 'elm.js');
scriptTag.addEventListener('load', function() {
initElm();
});
}
function initElm() {
const main = document.querySelector('main')
while (main.firstChild) {
main.removeChild(main.lastChild);
}
var main_content = document.createElement("div");
main_content.setAttribute('id', main_content);
main.appendChild(main_content);
const app = Elm.Main.init({
node: main_content
});
}
const hotReloadInterval = window.setInterval(checkTimestamp, 1000)
window.lastTimestamp = '';
const parsedUrl = new URL(window.location.href);
const pathPrefix = parsedUrl.pathname.replace('/index.html', '');
function buildError(error) {
const outerDiv = document.createElement("div");
const path = error.path.replace(pathPrefix, '');
const newContent = document.createTextNode(path);
outerDiv.appendChild(newContent);
const problems = document.createElement("ul");
error.problems.forEach(function(p){
const li = document.createElement('li');
const pre = document.createElement('pre');
li.appendChild(pre);
p.message.forEach(function(message) {
if ((typeof message) == 'string') {
pre.appendChild(document.createTextNode(message));
} else {
const span = document.createElement('span');
span.setAttribute("style", "color:" + message.color);
span.appendChild(document.createTextNode(message.string));
pre.appendChild(span);
}
});
problems.appendChild(li);
});
outerDiv.appendChild(problems);
return outerDiv;
}
function showErrors(errors) {
const oldScript = document.getElementById('build-errors');
if (oldScript) {
oldScript.remove();
}
if (!errors) return;
const newScript = document.createElement("div");
newScript.setAttribute("id", "build-errors");
errors.errors.forEach(function(e){
newScript.appendChild(buildError(e));
});
// add the newly created element and its content into the DOM
document.getElementById('build-info').appendChild(newScript);
}
function refresh(timestamp, errors) {
if (window.lastTimestamp == '') {
if (errors) {
showErrors(errors);
}
} else if (window.lastTimestamp != timestamp) {
showErrors(errors);
if (!errors) {
loadElm();
}
}
window.lastTimestamp = timestamp;
}
function mkScriptTag(id, filename) {
const oldScript = document.getElementById(id);
if (oldScript) {
oldScript.remove();
}
const newScript = document.createElement("script");
newScript.setAttribute("id", id);
newScript.setAttribute("type", "text/javascript");
newScript.setAttribute("src", filename );
// add the newly created element and its content into the DOM
document.body.appendChild(newScript);
return newScript;
}
function checkTimestamp () {
mkScriptTag('timestampjs',"tmp/timestamp.js");
}
loadElm();