Skip to content

Instantly share code, notes, and snippets.

@ccapndave
Last active November 30, 2017 12:51
Show Gist options
  • Save ccapndave/36e91d922c2fd3ce4f4c51bd02601f36 to your computer and use it in GitHub Desktop.
Save ccapndave/36e91d922c2fd3ce4f4c51bd02601f36 to your computer and use it in GitHub Desktop.

I'm working on the plugin at https://github.com/ccapndave/fuse-box-elm-plugin/blob/master/src/index.ts

Let's assume that the entry point to my application is app.ts.

fuse.ts

fuse.bundle("app")
  .watch()
  .instructions("> js/app.ts")
  .hmr();

app.ts

import * as Elm from "../elm/src/Main.elm"

Main.elm

import Model exposing (Model)
import Update exposing (update, subscriptions, Msg(..))
import View exposing (view)

So elm-make will compile Model.elm, Update.elm and View.elm into its output automatically. I can get a list of the absolute paths of all dependencies using a find-elm-dependencies npm module.

The goal is to make HMR work, so that if I edit View.elm, elm-make will run on Main.elm, and then inject the code into the browser.

Here is my (failed) attempt. This does actually make HMR work (I think), but it runs elm-make for Main.elm, Model.elm, Update.elm and View.elm instead of just running it for Main.elm. I sort of copied the relevant parts from BabelPlugin.ts without properly understanding them.

//import { File, WorkFlowContext, Plugin } from "fuse-box";
import { File, WorkFlowContext, Plugin } from "../../fuse-box/.dev";
import { ChildProcess } from "child_process";
import { resolve } from "path";
import { readFile } from "fs";
import { tmpName } from "tmp";
import * as spawn from "cross-spawn";
import { findAllDependencies } from "find-elm-dependencies"

const tmp = () =>
  new Promise((resolve, reject) =>
    tmpName((err, path) => (err ? reject(err) : resolve(path)))
  );

export interface ElmPluginOptions {
  warn?: boolean;
  debug?: boolean;
}

export class ElmPluginClass implements Plugin {
  // Match Elm files
  public test: RegExp = /\.elm$/;

  public context: WorkFlowContext;

  public options: ElmPluginOptions;

  constructor(options: ElmPluginOptions = {}) {
    this.options = { ...options };
  }

  public init(context: WorkFlowContext): void {
    this.context = context;
    context.allowExtension(".elm");
  }

  public getElmMakePath(): string {
    try {
      return resolve("node_modules/.bin/elm-make");
    } catch (_) {}

    return "elm-make";
  }

  public async transform(file: File): Promise<any> {
    if (this.context.useCache) {
      if (file.loadFromCache()) {
          return;
      }
    }

    file.loadContents();
    
    // Get the path to elm-make
    const elmMakePath: string = this.getElmMakePath();

    // Create temporary JS file
    const tmpFilename: string = `${await tmp()}.js`;

    return new Promise((resolve, reject) => {
      // Construct the arguments for elm-make
      const args = [
        "--yes",
        "--output",
        tmpFilename,
        this.options.warn ? "--warn" : null,
        this.options.debug ? "--debug" : null,
        file.absPath
      ].filter(x => x !== null);

      const proc: ChildProcess = spawn(elmMakePath, args, { stdio: "inherit" });

      proc.on("error", (err: NodeJS.ErrnoException) => {
        if (err.code === "ENOENT") {
          reject(
            `Could not find Elm compiler @ "${elmMakePath}"
             \nHave you installed elm yet? If not, please install "elm" via npm`
          );
        } else if (err.code === "EACCES") {
          reject(
            `Elm compiler @ "${elmMakePath}" did not have permission to run
            \nYou may need give it executable permissions`
          );
        } else {
          reject(
            `Error attempting to run Elm compiler @ "${elmMakePath}" \n ${err}`
          );
        }
      });

      proc.on("close", (code: Number) => {
        if (code === 0) {
          readFile(tmpFilename, (err: NodeJS.ErrnoException, data: Buffer) => {
            if (err) {
              reject(err);
            } else {
              file.contents = data.toString();
              
              findAllDependencies(file.absPath)
                .then((paths: string[]) => {
                  file.analysis.dependencies = paths;

                  if (this.context.useCache) {
                    this.context.emitJavascriptHotReload(file);
                    this.context.cache.writeStaticCache(file, file.sourceMap);
                  }

                  resolve(file);
                })
                .catch((err: string) => {
                  reject(err);
                });

              resolve(file);
            }
          });
        } else {
          reject("Failed to compile Elm.");
        }
      });
    });
  }
}

export const ElmPlugin = (options?: ElmPluginOptions) =>
  new ElmPluginClass(options);
@ccapndave
Copy link
Author

  1. Its a flat array of absolute paths matching all the Elm files imported in the app. For example:
[ '/Users/dave/Projects/Couloir/ctp/title-sss/app/packages/couloir-core/src/Util/MaybeExtra.elm',
  '/Users/dave/Projects/Couloir/ctp/title-sss/app/elm/src/Model.elm',
  '/Users/dave/Projects/Couloir/ctp/title-sss/app/elm/src/Update.elm',
  '/Users/dave/Projects/Couloir/ctp/title-sss/app/elm/src/View.elm',
  '/Users/dave/Projects/Couloir/ctp/title-sss/app/elm/src/Persister.elm',
  ... lots and lots more
]
  1. Unfortunately elm-make can only take a file as input, not a string or things on stdin (hopefully this will be coming in a future version).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment