Skip to content

Commit

Permalink
Transformer for Elm (#4395)
Browse files Browse the repository at this point in the history
  • Loading branch information
nilshelmig authored Dec 7, 2020
1 parent 9e0bb77 commit cebbec7
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 29 deletions.
1 change: 1 addition & 0 deletions packages/configs/default/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
],
"*.pug": ["@parcel/transformer-pug"],
"*.coffee": ["@parcel/transformer-coffeescript"],
"*.elm": ["@parcel/transformer-elm"],
"*.mdx": ["@parcel/transformer-mdx"],
"*.vue": ["@parcel/transformer-vue"],
"template:*.vue": ["@parcel/transformer-vue"],
Expand Down
1 change: 1 addition & 0 deletions packages/configs/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@parcel/transformer-sugarss": "2.0.0-beta.1",
"@parcel/transformer-toml": "2.0.0-beta.1",
"@parcel/transformer-typescript-types": "2.0.0-beta.1",
"@parcel/transformer-elm": "^2.0.0-alpha.3.1",
"@parcel/transformer-yaml": "2.0.0-beta.1",
"@parcel/transformer-image": "2.0.0-beta.1",
"@parcel/transformer-vue": "2.0.0-beta.1"
Expand Down
71 changes: 47 additions & 24 deletions packages/core/integration-tests/test/elm.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,82 @@
import assert from 'assert';
import {bundle, assertBundleTree, run, outputFS} from '@parcel/test-utils';
import path from 'path';
import {
bundle,
distDir,
assertBundles,
run,
outputFS,
} from '@parcel/test-utils';

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

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

let output = await run(b);
assert.equal(typeof output().Elm.Main.init, 'function');
});
it('should produce a elm bundle with debugger', async function() {
let b = await bundle(__dirname + '/integration/elm/index.js');
let b = await bundle(path.join(__dirname, '/integration/elm/index.js'));

await run(b);
let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
assert(js.includes('elm$browser$Debugger'));
});

it('should apply elm-hot if HMR is enabled', async function() {
let b = await bundle(__dirname + '/integration/elm/index.js', {
hmr: true,
let b = await bundle(path.join(__dirname, '/integration/elm/index.js'), {
hot: true,
});

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

let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
assert(js.includes('[elm-hot]'));
});

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

await run(b);
let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
assert(!js.includes('elm$browser$Debugger'));
});

it('should remove debugger when environment variable `PARCEL_ELM_NO_DEBUG` is set to true', async function() {
let b = await bundle(path.join(__dirname, '/integration/elm/index.js'), {
env: {PARCEL_ELM_NO_DEBUG: true},
});

await run(b);
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
assert(!js.includes('elm$browser$Debugger'));
});

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

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

let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
assert(!js.includes('elm$core'));
assert(js.includes('Elm'));
assert(js.includes('init'));
});
});
6 changes: 3 additions & 3 deletions packages/core/integration-tests/test/integration/elm/elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"source-directories": [
"src"
],
"elm-version": "0.19.0",
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
"indirect": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Empty file.
1 change: 1 addition & 0 deletions packages/core/parcel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,7 @@ asset graph. They mostly call out to different compilers and preprocessors.
- `@parcel/transformer-wasm`
- `@parcel/transformer-webmanifest`
- `@parcel/transformer-yaml`
- `@parcel/transformer-elm`
- ...

### Bundlers
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type PackageJSON = {
devDependencies?: PackageDependencies,
peerDependencies?: PackageDependencies,
sideEffects?: boolean | FilePath | Array<FilePath>,
bin?: string | {|[string]: FilePath|},
...
};

Expand Down
30 changes: 30 additions & 0 deletions packages/transformers/elm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@parcel/transformer-elm",
"version": "2.0.0-beta.1",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git"
},
"main": "lib/ElmTransformer.js",
"source": "src/ElmTransformer.js",
"engines": {
"node": ">= 10.0.0",
"parcel": "^2.0.0-alpha.3.1"
},
"dependencies": {
"@parcel/diagnostic": "^2.0.0-beta.1",
"@parcel/plugin": "^2.0.0-beta.1",
"command-exists": "^1.2.8",
"cross-spawn": "^7.0.3",
"nullthrows": "^1.1.1",
"terser": "^5.2.1"
}
}
162 changes: 162 additions & 0 deletions packages/transformers/elm/src/ElmTransformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// @flow strict-local

import {Transformer} from '@parcel/plugin';
import commandExists from 'command-exists';
import spawn from 'cross-spawn';
import path from 'path';
import {minify} from 'terser';
import nullthrows from 'nullthrows';
import ThrowableDiagnostic from '@parcel/diagnostic';

let isWorker;
try {
let worker_threads = require('worker_threads');
isWorker = worker_threads.threadId > 0;
} catch (_) {
isWorker = false;
}

export default (new Transformer({
async loadConfig({config, options}) {
const elmConfig = await config.getConfig(['elm.json']);
if (!elmConfig) {
await elmBinaryPath(config.searchPath, options); // Check if elm is even installed
throw new ThrowableDiagnostic({
diagnostic: {
message: "The 'elm.json' file is missing.",
hints: [
"Initialize your elm project by running 'elm init'",
"If you installed elm as project dependency then run 'yarn elm init' or 'npx elm init'",
],
},
});
}
config.setResult(elmConfig.contents);
},

async transform({asset, options}) {
const elmBinary = await elmBinaryPath(asset.filePath, options);
const elm = await options.packageManager.require(
'node-elm-compiler',
asset.filePath,
{
autoinstall: options.autoinstall,
saveDev: true,
},
);

const compilerConfig = {
spawn,
cwd: path.dirname(asset.filePath),
// $FlowFixMe[sketchy-null-string]
debug: !options.env.PARCEL_ELM_NO_DEBUG && options.mode !== 'production',
optimize: asset.env.minify,
};
asset.invalidateOnEnvChange('PARCEL_ELM_NO_DEBUG');
for (const filePath of await elm.findAllDependencies(asset.filePath)) {
asset.addIncludedFile(filePath);
}

// Workaround for `chdir` not working in workers
// this can be removed after https://github.com/isaacs/node-graceful-fs/pull/200 was mergend and used in parcel
process.chdir.disabled = isWorker;

let code = await compileToString(elm, elmBinary, asset, compilerConfig);
if (options.hot) {
code = await injectHotModuleReloadRuntime(code, asset.filePath, options);
}
if (compilerConfig.optimize) code = await minifyElmOutput(code);

asset.type = 'js';
asset.setCode(code);
return [asset];
},
}): Transformer);

async function elmBinaryPath(searchPath, options) {
let elmBinary = await resolveLocalElmBinary(searchPath, options);

if (elmBinary == null && !commandExists.sync('elm')) {
throw new ThrowableDiagnostic({
diagnostic: {
message: "Can't find 'elm' binary.",
hints: [
"You can add it as an dependency for your project by running 'yarn add -D elm' or 'npm add -D elm'",
'If you want to install it globally then follow instructions on https://elm-lang.org/',
],
origin: '@parcel/elm-transformer',
},
});
}

return elmBinary;
}

async function resolveLocalElmBinary(searchPath, options) {
try {
let result = await options.packageManager.resolve(
'elm/package.json',
searchPath,
{autoinstall: false},
);

let bin = nullthrows(result.pkg?.bin);
return path.join(
path.dirname(result.resolved),
typeof bin === 'string' ? bin : bin.elm,
);
} catch (_) {
return null;
}
}

function compileToString(elm, elmBinary, asset, config) {
return elm.compileToString(asset.filePath, {
pathToElm: elmBinary,
...config,
});
}

async function injectHotModuleReloadRuntime(code, filePath, options) {
const elmHMR = await options.packageManager.require('elm-hot', filePath, {
autoinstall: options.autoinstall,
saveDev: true,
});
return elmHMR.inject(code);
}

async function minifyElmOutput(source) {
// Recommended minification
// Based on: http://elm-lang.org/0.19.0/optimize
let result = await minify(source, {
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,
});

if (result.code != null) return result.code;
throw result.error;
}
Loading

0 comments on commit cebbec7

Please sign in to comment.