Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Elm assets #1968

Merged
merged 30 commits into from Sep 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
563297e
Implemented change so that any asset can force a reload in HMR.
benthepoet Aug 30, 2018
7f2d72e
Added asset for Elm.
benthepoet Aug 30, 2018
0386b89
Registered extension.
benthepoet Aug 30, 2018
acca76d
Set Elm assets to force a page reload.
benthepoet Aug 30, 2018
fad0f8c
Added comment referencing optimal Terser configuration.
benthepoet Aug 30, 2018
2062007
Added integration test for Elm.
benthepoet Aug 30, 2018
ae839b3
Excluded package folder.
benthepoet Aug 30, 2018
52e5aac
Fixed test.
benthepoet Aug 30, 2018
7e44412
Fixed path for integration test.
benthepoet Aug 31, 2018
1c1c937
Fixed linting issues.
benthepoet Aug 31, 2018
a2127ea
Added test for production mode.
benthepoet Aug 31, 2018
a6ec420
Fixed module path.
benthepoet Aug 31, 2018
6844a1b
Removed assertion.
benthepoet Aug 31, 2018
51d07ad
Fixed assertion for minification test.
benthepoet Aug 31, 2018
0990345
Updated comment.
benthepoet Aug 31, 2018
c1a8e8e
Removed formatting in GraphQL test.
benthepoet Aug 31, 2018
ba651ce
Updated yarn.lock.
benthepoet Aug 31, 2018
ecfd64a
Switched minification to a single pass.
benthepoet Aug 31, 2018
dcd7d42
Fixed minification options.
benthepoet Aug 31, 2018
c013178
Disabled rename pass.
benthepoet Aug 31, 2018
4026284
Added error handling for minification.
benthepoet Aug 31, 2018
646dc93
Adjusted error throw.
benthepoet Aug 31, 2018
28a5823
Set compress to perform 2 passes.
benthepoet Aug 31, 2018
03bd986
Fixed linting issue.
benthepoet Aug 31, 2018
c0766da
Renamed flag to be better describe its purpose.
benthepoet Sep 2, 2018
9cb6d70
Added Elm package file to asset dependencies.
benthepoet Sep 2, 2018
100ffda
Added a missing await.
benthepoet Sep 2, 2018
a588ea8
Adjusted package version and replaced helper function.
benthepoet Sep 2, 2018
71358f4
Removed a redundant package.
benthepoet Sep 23, 2018
d856fd6
Auto install elm and create elm.json if needed
devongovett Sep 24, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this dependency used for? I don't see it used anywhere in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This provides the actual elm binaries that node-elm-compiler uses to compile the source files. The package isn't a dependency of node-elm-compiler, so unfortunately we have to pull it in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. So since this is a dev dependency in parcel, so we might need to install it in a user's project as well, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, updated to auto install elm when the command isn't available globally.

"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 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 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 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
131 changes: 131 additions & 0 deletions src/assets/ElmAsset.js
@@ -0,0 +1,131 @@
const process = require('process');
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 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 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 test/integration/elm/elm.json
@@ -0,0 +1,24 @@
{
benthepoet marked this conversation as resolved.
Show resolved Hide resolved
"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 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 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
}