Skip to content

Commit cebbec7

Browse files
authored
Transformer for Elm (#4395)
1 parent 9e0bb77 commit cebbec7

File tree

11 files changed

+271
-29
lines changed

11 files changed

+271
-29
lines changed

packages/configs/default/index.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
],
2828
"*.pug": ["@parcel/transformer-pug"],
2929
"*.coffee": ["@parcel/transformer-coffeescript"],
30+
"*.elm": ["@parcel/transformer-elm"],
3031
"*.mdx": ["@parcel/transformer-mdx"],
3132
"*.vue": ["@parcel/transformer-vue"],
3233
"template:*.vue": ["@parcel/transformer-vue"],

packages/configs/default/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@parcel/transformer-sugarss": "2.0.0-beta.1",
6262
"@parcel/transformer-toml": "2.0.0-beta.1",
6363
"@parcel/transformer-typescript-types": "2.0.0-beta.1",
64+
"@parcel/transformer-elm": "^2.0.0-alpha.3.1",
6465
"@parcel/transformer-yaml": "2.0.0-beta.1",
6566
"@parcel/transformer-image": "2.0.0-beta.1",
6667
"@parcel/transformer-vue": "2.0.0-beta.1"
+47-24
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,82 @@
11
import assert from 'assert';
2-
import {bundle, assertBundleTree, run, outputFS} from '@parcel/test-utils';
2+
import path from 'path';
3+
import {
4+
bundle,
5+
distDir,
6+
assertBundles,
7+
run,
8+
outputFS,
9+
} from '@parcel/test-utils';
310

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

8-
await assertBundleTree(b, {
9-
type: 'js',
10-
assets: ['Main.elm', 'index.js'],
11-
});
15+
await assertBundles(b, [
16+
{
17+
type: 'js',
18+
assets: ['Main.elm', 'index.js'],
19+
},
20+
]);
1221

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

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

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

29-
await assertBundleTree(b, {
30-
type: 'js',
31-
assets: ['Main.elm', 'hmr-runtime.js', 'index.js'],
32-
});
38+
await assertBundles(b, [
39+
{
40+
type: 'js',
41+
assets: ['HMRRuntime.js', 'Main.elm', 'index.js'],
42+
},
43+
]);
3344

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

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

4354
await run(b);
44-
let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
55+
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
56+
assert(!js.includes('elm$browser$Debugger'));
57+
});
58+
59+
it('should remove debugger when environment variable `PARCEL_ELM_NO_DEBUG` is set to true', async function() {
60+
let b = await bundle(path.join(__dirname, '/integration/elm/index.js'), {
61+
env: {PARCEL_ELM_NO_DEBUG: true},
62+
});
63+
64+
await run(b);
65+
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
4566
assert(!js.includes('elm$browser$Debugger'));
4667
});
4768

4869
it('should minify Elm in production mode', async function() {
49-
let b = await bundle(__dirname + '/integration/elm/index.js', {
50-
production: true,
70+
let b = await bundle(path.join(__dirname, '/integration/elm/index.js'), {
71+
mode: 'production',
72+
minify: true,
5173
});
5274

53-
let output = await run(b);
54-
assert.equal(typeof output().Elm.Main.init, 'function');
75+
await run(b);
5576

56-
let js = await outputFS.readFile(__dirname + '/dist/index.js', 'utf8');
77+
let js = await outputFS.readFile(path.join(distDir, 'index.js'), 'utf8');
5778
assert(!js.includes('elm$core'));
79+
assert(js.includes('Elm'));
80+
assert(js.includes('init'));
5881
});
5982
});

packages/core/integration-tests/test/integration/elm/elm.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
"source-directories": [
44
"src"
55
],
6-
"elm-version": "0.19.0",
6+
"elm-version": "0.19.1",
77
"dependencies": {
88
"direct": {
9-
"elm/browser": "1.0.1",
10-
"elm/core": "1.0.2",
9+
"elm/browser": "1.0.2",
10+
"elm/core": "1.0.5",
1111
"elm/html": "1.0.0"
1212
},
1313
"indirect": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

packages/core/integration-tests/test/integration/elm/yarn.lock

Whitespace-only changes.

packages/core/parcel/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,7 @@ asset graph. They mostly call out to different compilers and preprocessors.
11181118
- `@parcel/transformer-wasm`
11191119
- `@parcel/transformer-webmanifest`
11201120
- `@parcel/transformer-yaml`
1121+
- `@parcel/transformer-elm`
11211122
- ...
11221123

11231124
### Bundlers

packages/core/types/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export type PackageJSON = {
241241
devDependencies?: PackageDependencies,
242242
peerDependencies?: PackageDependencies,
243243
sideEffects?: boolean | FilePath | Array<FilePath>,
244+
bin?: string | {|[string]: FilePath|},
244245
...
245246
};
246247

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@parcel/transformer-elm",
3+
"version": "2.0.0-beta.1",
4+
"license": "MIT",
5+
"publishConfig": {
6+
"access": "public"
7+
},
8+
"funding": {
9+
"type": "opencollective",
10+
"url": "https://opencollective.com/parcel"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/parcel-bundler/parcel.git"
15+
},
16+
"main": "lib/ElmTransformer.js",
17+
"source": "src/ElmTransformer.js",
18+
"engines": {
19+
"node": ">= 10.0.0",
20+
"parcel": "^2.0.0-alpha.3.1"
21+
},
22+
"dependencies": {
23+
"@parcel/diagnostic": "^2.0.0-beta.1",
24+
"@parcel/plugin": "^2.0.0-beta.1",
25+
"command-exists": "^1.2.8",
26+
"cross-spawn": "^7.0.3",
27+
"nullthrows": "^1.1.1",
28+
"terser": "^5.2.1"
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// @flow strict-local
2+
3+
import {Transformer} from '@parcel/plugin';
4+
import commandExists from 'command-exists';
5+
import spawn from 'cross-spawn';
6+
import path from 'path';
7+
import {minify} from 'terser';
8+
import nullthrows from 'nullthrows';
9+
import ThrowableDiagnostic from '@parcel/diagnostic';
10+
11+
let isWorker;
12+
try {
13+
let worker_threads = require('worker_threads');
14+
isWorker = worker_threads.threadId > 0;
15+
} catch (_) {
16+
isWorker = false;
17+
}
18+
19+
export default (new Transformer({
20+
async loadConfig({config, options}) {
21+
const elmConfig = await config.getConfig(['elm.json']);
22+
if (!elmConfig) {
23+
await elmBinaryPath(config.searchPath, options); // Check if elm is even installed
24+
throw new ThrowableDiagnostic({
25+
diagnostic: {
26+
message: "The 'elm.json' file is missing.",
27+
hints: [
28+
"Initialize your elm project by running 'elm init'",
29+
"If you installed elm as project dependency then run 'yarn elm init' or 'npx elm init'",
30+
],
31+
},
32+
});
33+
}
34+
config.setResult(elmConfig.contents);
35+
},
36+
37+
async transform({asset, options}) {
38+
const elmBinary = await elmBinaryPath(asset.filePath, options);
39+
const elm = await options.packageManager.require(
40+
'node-elm-compiler',
41+
asset.filePath,
42+
{
43+
autoinstall: options.autoinstall,
44+
saveDev: true,
45+
},
46+
);
47+
48+
const compilerConfig = {
49+
spawn,
50+
cwd: path.dirname(asset.filePath),
51+
// $FlowFixMe[sketchy-null-string]
52+
debug: !options.env.PARCEL_ELM_NO_DEBUG && options.mode !== 'production',
53+
optimize: asset.env.minify,
54+
};
55+
asset.invalidateOnEnvChange('PARCEL_ELM_NO_DEBUG');
56+
for (const filePath of await elm.findAllDependencies(asset.filePath)) {
57+
asset.addIncludedFile(filePath);
58+
}
59+
60+
// Workaround for `chdir` not working in workers
61+
// this can be removed after https://github.com/isaacs/node-graceful-fs/pull/200 was mergend and used in parcel
62+
process.chdir.disabled = isWorker;
63+
64+
let code = await compileToString(elm, elmBinary, asset, compilerConfig);
65+
if (options.hot) {
66+
code = await injectHotModuleReloadRuntime(code, asset.filePath, options);
67+
}
68+
if (compilerConfig.optimize) code = await minifyElmOutput(code);
69+
70+
asset.type = 'js';
71+
asset.setCode(code);
72+
return [asset];
73+
},
74+
}): Transformer);
75+
76+
async function elmBinaryPath(searchPath, options) {
77+
let elmBinary = await resolveLocalElmBinary(searchPath, options);
78+
79+
if (elmBinary == null && !commandExists.sync('elm')) {
80+
throw new ThrowableDiagnostic({
81+
diagnostic: {
82+
message: "Can't find 'elm' binary.",
83+
hints: [
84+
"You can add it as an dependency for your project by running 'yarn add -D elm' or 'npm add -D elm'",
85+
'If you want to install it globally then follow instructions on https://elm-lang.org/',
86+
],
87+
origin: '@parcel/elm-transformer',
88+
},
89+
});
90+
}
91+
92+
return elmBinary;
93+
}
94+
95+
async function resolveLocalElmBinary(searchPath, options) {
96+
try {
97+
let result = await options.packageManager.resolve(
98+
'elm/package.json',
99+
searchPath,
100+
{autoinstall: false},
101+
);
102+
103+
let bin = nullthrows(result.pkg?.bin);
104+
return path.join(
105+
path.dirname(result.resolved),
106+
typeof bin === 'string' ? bin : bin.elm,
107+
);
108+
} catch (_) {
109+
return null;
110+
}
111+
}
112+
113+
function compileToString(elm, elmBinary, asset, config) {
114+
return elm.compileToString(asset.filePath, {
115+
pathToElm: elmBinary,
116+
...config,
117+
});
118+
}
119+
120+
async function injectHotModuleReloadRuntime(code, filePath, options) {
121+
const elmHMR = await options.packageManager.require('elm-hot', filePath, {
122+
autoinstall: options.autoinstall,
123+
saveDev: true,
124+
});
125+
return elmHMR.inject(code);
126+
}
127+
128+
async function minifyElmOutput(source) {
129+
// Recommended minification
130+
// Based on: http://elm-lang.org/0.19.0/optimize
131+
let result = await minify(source, {
132+
compress: {
133+
keep_fargs: false,
134+
passes: 2,
135+
pure_funcs: [
136+
'F2',
137+
'F3',
138+
'F4',
139+
'F5',
140+
'F6',
141+
'F7',
142+
'F8',
143+
'F9',
144+
'A2',
145+
'A3',
146+
'A4',
147+
'A5',
148+
'A6',
149+
'A7',
150+
'A8',
151+
'A9',
152+
],
153+
pure_getters: true,
154+
unsafe: true,
155+
unsafe_comps: true,
156+
},
157+
mangle: true,
158+
});
159+
160+
if (result.code != null) return result.code;
161+
throw result.error;
162+
}

0 commit comments

Comments
 (0)