axelerator.de

Episode 1: Projektsetup für den Elm Tetris Clon

1 Minute
#elm
30 June 2021

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();