Skip to content

Commit

Permalink
Add support for Elm assets (#1968)
Browse files Browse the repository at this point in the history
  • Loading branch information
benthepoet authored and devongovett committed Sep 24, 2018
1 parent 80d137d commit 948159b
Show file tree
Hide file tree
Showing 12 changed files with 412 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -12,6 +12,7 @@ lib
.vscode/
.idea/
*.min.js
test/integration/**/elm-stuff
test/integration/**/target
test/integration/**/Cargo.lock
test/**/node_modules
Expand Down
2 changes: 2 additions & 0 deletions packages/core/parcel-bundler/package.json
Expand Up @@ -86,6 +86,7 @@
"codecov": "^3.0.0",
"coffeescript": "^2.0.3",
"cross-env": "^5.1.1",
"elm": "^0.19.0",
"eslint": "^4.13.0",
"glslify-bundle": "^5.0.0",
"glslify-deps": "^1.3.0",
Expand All @@ -97,6 +98,7 @@
"mocha": "^5.1.1",
"ncp": "^2.0.0",
"nib": "^1.1.2",
"node-elm-compiler": "^5.0.1",
"nyc": "^11.1.0",
"postcss-modules": "^1.1.0",
"posthtml-extend": "^0.2.1",
Expand Down
1 change: 1 addition & 0 deletions packages/core/parcel-bundler/src/Asset.js
Expand Up @@ -27,6 +27,7 @@ class Asset {
this.options = options;
this.encoding = 'utf8';
this.type = path.extname(this.name).slice(1);
this.hmrPageReload = false;

this.processed = false;
this.contents = options.rendition ? options.rendition.value : null;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/parcel-bundler/src/HMRServer.js
Expand Up @@ -62,8 +62,8 @@ class HMRServer {
});
}

const containsHtmlAsset = assets.some(asset => asset.type === 'html');
if (containsHtmlAsset) {
const shouldReload = assets.some(asset => asset.hmrPageReload);
if (shouldReload) {
this.broadcast({
type: 'reload'
});
Expand Down
1 change: 1 addition & 0 deletions packages/core/parcel-bundler/src/Parser.js
Expand Up @@ -17,6 +17,7 @@ class Parser {
this.registerExtension('ts', './assets/TypeScriptAsset');
this.registerExtension('tsx', './assets/TypeScriptAsset');
this.registerExtension('coffee', './assets/CoffeeScriptAsset');
this.registerExtension('elm', './assets/ElmAsset');
this.registerExtension('vue', './assets/VueAsset');
this.registerExtension('json', './assets/JSONAsset');
this.registerExtension('json5', './assets/JSONAsset');
Expand Down
130 changes: 130 additions & 0 deletions packages/core/parcel-bundler/src/assets/ElmAsset.js
@@ -0,0 +1,130 @@
const Asset = require('../Asset');
const commandExists = require('command-exists');
const localRequire = require('../utils/localRequire');
const {minify} = require('terser');
const path = require('path');
const spawn = require('cross-spawn');

class ElmAsset extends Asset {
constructor(name, options) {
super(name, options);
this.type = 'js';
this.hmrPageReload = true;
}

async parse() {
let options = {
cwd: path.dirname(this.name)
};

// If elm is not installed globally, install it locally.
try {
await commandExists('elm');
} catch (err) {
await localRequire('elm', this.name);
options.pathToElm = path.join(
path.dirname(require.resolve('elm')),
'bin',
'elm'
);
}

this.elm = await localRequire('node-elm-compiler', this.name);

// Ensure that an elm.json file exists, and initialize one if not.
let elmConfig = await this.getConfig(['elm.json'], {load: false});
if (!elmConfig) {
await this.createElmConfig(options);

// Ensure we are watching elm.json for changes
await this.getConfig(['elm.json'], {load: false});
}

if (this.options.minify) {
options.optimize = true;
}

let compiled = await this.elm.compileToString(this.name, options);
this.contents = compiled.toString();
}

async collectDependencies() {
let dependencies = await this.elm.findAllDependencies(this.name);
for (let dependency of dependencies) {
this.addDependency(dependency, {includedInParent: true});
}
}

async createElmConfig(options) {
let cp = spawn(options.pathToElm || 'elm', ['init']);
cp.stdin.write('y\n');

return new Promise((resolve, reject) => {
cp.on('error', reject);
cp.on('close', function(code) {
if (code !== 0) {
return reject(new Error('elm init failed.'));
}

return resolve();
});
});
}

async generate() {
let output = this.contents;

if (this.options.minify) {
output = pack(output);
}

return {
[this.type]: output
};

// Recommended minification
// Based on:
// - http://elm-lang.org/0.19.0/optimize
function pack(source) {
let options = {
compress: {
keep_fargs: false,
passes: 2,
pure_funcs: [
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9'
],
pure_getters: true,
unsafe: true,
unsafe_comps: true
},
mangle: true,
rename: false
};

let result = minify(source, options);

if (result.error) {
throw result.error;
}

return result.code;
}
}
}

module.exports = ElmAsset;
1 change: 1 addition & 0 deletions packages/core/parcel-bundler/src/assets/HTMLAsset.js
Expand Up @@ -85,6 +85,7 @@ class HTMLAsset extends Asset {
super(name, options);
this.type = 'html';
this.isAstDirty = false;
this.hmrPageReload = true;
}

async parse(code) {
Expand Down
29 changes: 29 additions & 0 deletions packages/core/parcel-bundler/test/elm.js
@@ -0,0 +1,29 @@
const assert = require('assert');
const fs = require('../src/utils/fs');
const {bundle, assertBundleTree, run} = require('./utils');

describe('elm', function() {
it('should produce a basic Elm bundle', async function() {
let b = await bundle(__dirname + '/integration/elm/index.js');

await assertBundleTree(b, {
type: 'js',
assets: ['Main.elm', 'index.js']
});

let output = await run(b);
assert.equal(typeof output().Elm.Main.init, 'function');
});

it('should minify Elm in production mode', async function() {
let b = await bundle(__dirname + '/integration/elm/index.js', {
production: true
});

let output = await run(b);
assert.equal(typeof output().Elm.Main.init, 'function');

let js = await fs.readFile(__dirname + '/dist/index.js', 'utf8');
assert(!js.includes('elm$core'));
});
});
24 changes: 24 additions & 0 deletions packages/core/parcel-bundler/test/integration/elm/elm.json
@@ -0,0 +1,24 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.0",
"elm/core": "1.0.0",
"elm/html": "1.0.0"
},
"indirect": {
"elm/json": "1.0.0",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
5 changes: 5 additions & 0 deletions packages/core/parcel-bundler/test/integration/elm/index.js
@@ -0,0 +1,5 @@
var local = require('./src/Main.elm');

module.exports = function () {
return local;
};
47 changes: 47 additions & 0 deletions packages/core/parcel-bundler/test/integration/elm/src/Main.elm
@@ -0,0 +1,47 @@
module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


type alias Model =
{ count : Int }


initialModel : Model
initialModel =
{ count = 0 }


type Msg
= Increment
| Decrement


update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }

Decrement ->
{ model | count = model.count - 1 }


view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+1" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
]


main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}

0 comments on commit 948159b

Please sign in to comment.