Skip to content

Commit

Permalink
Add multi module compilation for elm (#8076)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristophP committed Jul 31, 2022
1 parent 128e072 commit fca5c8c
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 73 deletions.
19 changes: 19 additions & 0 deletions packages/core/integration-tests/test/elm.js
Expand Up @@ -133,4 +133,23 @@ describe('elm', function () {
},
);
});

it('should produce extra Modules given in "with" query param', async function () {
const b = await bundle(
path.join(__dirname, '/integration/elm-multiple-apps/src/index.js'),
);

assertBundles(b, [
{
type: 'js',
assets: ['Main.elm', 'index.js', 'esmodule-helpers.js'],
},
]);

const output = await run(b);
const Elm = output.default();
assert.equal(typeof Elm.Main.init, 'function');
assert.equal(typeof Elm.MainB.init, 'function');
assert.equal(typeof Elm.MainC.init, 'function');
});
});
@@ -0,0 +1,24 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
@@ -0,0 +1,10 @@
{
"@parcel/transformer-elm": {
"extraSources": {
"./src/Main.elm": [
"./src/MainB.elm",
"./src/MainC.elm"
]
}
}
}
@@ -0,0 +1,7 @@
module Main exposing (main)

import Html


main =
Html.text "Hello, world!"
@@ -0,0 +1,7 @@
module MainB exposing (main)

import Html


main =
Html.text "Hello, world!"
@@ -0,0 +1,7 @@
module MainC exposing (main)

import Html


main =
Html.text "Hello, world!"
@@ -0,0 +1,5 @@
import { Elm } from './Main.elm?with=MainB.elm&with=MainC.elm';

export default function() {
return Elm;
}
Empty file.
2 changes: 1 addition & 1 deletion packages/transformers/elm/package.json
Expand Up @@ -27,7 +27,7 @@
"elm-hot": "^1.1.5",
"node-elm-compiler": "^5.0.5",
"nullthrows": "^1.1.1",
"terser": "^5.2.1"
"terser": "^5.14.2"
},
"peerDependencies": {
"elm": "^0.19.1-5"
Expand Down
134 changes: 62 additions & 72 deletions packages/transformers/elm/src/ElmTransformer.js
@@ -1,17 +1,17 @@
// @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, {md} from '@parcel/diagnostic';
// $FlowFixMe
import elm from 'node-elm-compiler';
// $FlowFixMe
import elmHMR from 'elm-hot';

import {load, elmBinaryPath} from './loadConfig';

let isWorker;
try {
let worker_threads = require('worker_threads');
Expand All @@ -21,24 +21,11 @@ try {
}

export default (new Transformer({
async loadConfig({config}) {
const elmConfig = await config.getConfig(['elm.json']);
if (!elmConfig) {
elmBinaryPath(); // 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'",
],
},
});
}
return elmConfig.contents;
loadConfig({config}) {
return load({config});
},

async transform({asset, options}) {
async transform({asset, options, logger}) {
const elmBinary = elmBinaryPath();
const compilerConfig = {
spawn,
Expand All @@ -49,25 +36,44 @@ export default (new Transformer({
report: 'json',
};
asset.invalidateOnEnvChange('PARCEL_ELM_NO_DEBUG');
for (const filePath of await elm.findAllDependencies(asset.filePath)) {

const extraSources = resolveExtraSources({asset, logger});

extraSources.forEach(filePath => {
asset.invalidateOnFileChange(filePath);
}
});
const sources = [asset.filePath, ...extraSources];
const dependencies = await Promise.all(
sources.map(source => elm.findAllDependencies(source)),
);
const uniqueDeps = new Set(dependencies.flat());
Array.from(uniqueDeps).forEach(filePath => {
asset.invalidateOnFileChange(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
// $FlowFixMe[method-unbinding]
process.chdir.disabled = isWorker;
let code;
try {
code = await compileToString(elm, elmBinary, asset, compilerConfig);
code = await compileToString(elm, elmBinary, sources, compilerConfig);
} catch (e) {
let compilerJson = e.message.split('\n')[1];
let compilerDiagnostics = JSON.parse(compilerJson);

if (compilerDiagnostics.type === 'compile-errors') {
throw new ThrowableDiagnostic({
diagnostic: compilerDiagnostics.errors.flatMap(
elmCompileErrorToParcelDiagnostics,
),
});
}

// compilerDiagnostics.type === "error"
// happens for example when compiled in prod mode with Debug.log in code
throw new ThrowableDiagnostic({
diagnostic: compilerDiagnostics.errors.flatMap(
elmErrorToParcelDiagnostics,
),
diagnostic: formatElmError(compilerDiagnostics, ''),
});
}

Expand All @@ -82,42 +88,24 @@ export default (new Transformer({
},
}): Transformer);

function elmBinaryPath() {
let elmBinary = resolveLocalElmBinary();

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',
},
// gather extra modules that should be added to the compilation process
function resolveExtraSources({asset, logger}) {
const dirname = path.dirname(asset.filePath);
const relativePaths = asset.query.getAll('with');

if (relativePaths.length > 0) {
logger.info({
message: md`Compiling elm with additional sources: ${md.bold(
JSON.stringify(relativePaths),
)}`,
});
}

return elmBinary;
}

function resolveLocalElmBinary() {
try {
let result = require.resolve('elm/package.json');
// $FlowFixMe
let pkg = require('elm/package.json');
let bin = nullthrows(pkg.bin);
return path.join(
path.dirname(result),
typeof bin === 'string' ? bin : bin.elm,
);
} catch (_) {
return null;
}
return relativePaths.map(relPath => path.join(dirname, relPath));
}

function compileToString(elm, elmBinary, asset, config) {
return elm.compileToString(asset.filePath, {
function compileToString(elm, elmBinary, sources, config) {
return elm.compileToString(sources, {
pathToElm: elmBinary,
...config,
});
Expand Down Expand Up @@ -173,22 +161,24 @@ function formatMessagePiece(piece) {
return md`${piece}`;
}

function elmErrorToParcelDiagnostics(error) {
function elmCompileErrorToParcelDiagnostics(error) {
const relativePath = path.relative(process.cwd(), error.path);
return error.problems.map(problem => {
const padLength = 80 - 5 - problem.title.length - relativePath.length;
const dashes = '-'.repeat(padLength);
const message = [
'',
`-- ${problem.title} ${dashes} ${relativePath}`,
'',
problem.message.map(formatMessagePiece).join(''),
].join('\n');

return {
message,
origin: '@parcel/elm-transformer',
stack: '', // set stack to empty since it is not useful
};
});
return error.problems.map(problem => formatElmError(problem, relativePath));
}

function formatElmError(problem, relativePath) {
const padLength = 80 - 5 - problem.title.length - relativePath.length;
const dashes = '-'.repeat(padLength);
const message = [
'',
`-- ${problem.title} ${dashes} ${relativePath}`,
'',
problem.message.map(formatMessagePiece).join(''),
].join('\n');

return {
message,
origin: '@parcel/elm-transformer',
stack: '', // set stack to empty since it is not useful
};
}
62 changes: 62 additions & 0 deletions packages/transformers/elm/src/loadConfig.js
@@ -0,0 +1,62 @@
// @flow strict-local

import type {Config} from '@parcel/types';
import path from 'path';
import ThrowableDiagnostic from '@parcel/diagnostic';
import commandExists from 'command-exists';
import nullthrows from 'nullthrows';

async function load({config}: {|config: Config|}): Promise<null> {
const elmConfig = await config.getConfig(['elm.json']);
if (!elmConfig) {
elmBinaryPath(); // Check if elm is even installed
throw new ThrowableDiagnostic({
diagnostic: {
origin: '@parcel/elm-transformer',
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'",
],
},
});
}

return null;
}

function elmBinaryPath(): ?string {
let elmBinary = resolveLocalElmBinary();

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;
}

function resolveLocalElmBinary() {
try {
let result = require.resolve('elm/package.json');
// $FlowFixMe
let pkg = require('elm/package.json');
let bin = nullthrows(pkg.bin);
return path.join(
path.dirname(result),
typeof bin === 'string' ? bin : bin.elm,
);
} catch (_) {
return null;
}
}

export {load, elmBinaryPath};

0 comments on commit fca5c8c

Please sign in to comment.