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 multi module compilation for elm #8076

Merged
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
58174b5
Add multi module compilation for Elm
May 8, 2022
21d7832
add invalidation for extra Elm sources
May 8, 2022
4f80133
add cache invalidations for all elm dependencies as well
May 9, 2022
004a394
fix nested dependencies in elm transform for multi module compilation
May 9, 2022
e6d0c08
Merge branch 'v2' of github.com:parcel-bundler/parcel into add-multi-…
May 20, 2022
4e8eec7
elm: use multiple "with" query params for multi module compilation
May 20, 2022
3fcdd78
add test for multiple elm modules
May 20, 2022
baa5bc4
Fix lint
devongovett May 23, 2022
fd76ef0
Merge branch 'v2' into add-multi-module-compilation-for-elm
ChristophP May 27, 2022
e716224
implement multi module elm compilation using a bit of package.json co…
May 29, 2022
96f00dc
fix linting
May 29, 2022
76a4673
Merge branch 'v2' of github.com:parcel-bundler/parcel into add-multi-…
May 29, 2022
d59eb0f
add tests for multi module compilation
May 29, 2022
a2fe570
refactor elm transformer by extracting config stuff in separate file,…
May 29, 2022
753bcf7
ix elm integration tests
May 29, 2022
af093fb
elm: revert multi-compilation to query param solution
Jun 5, 2022
d8f0433
elm: remove unneeded package.json config
Jun 5, 2022
746ca81
elm: implement query normalization to prevent unintentional asset dup…
Jun 5, 2022
f358a78
elm: fix tests for multi module compilation (query string solution)
Jun 6, 2022
a2a4665
integration tests: remove test that used elm config in package.json t…
Jun 6, 2022
9353f45
elm: try to write tests for asset deduplication
Jun 6, 2022
4625209
add fix for crash when the reported error is not a "compile-errors" t…
Jun 14, 2022
a24edcd
elm: remove deduplication logic of query string
Jun 17, 2022
200b5ab
fix linting and formatting
Jun 17, 2022
89e0fee
elm: add flow type annotiation
Jun 17, 2022
b79182a
elm: fix flow types
Jun 17, 2022
d9cb53a
Merge branch 'v2' into add-multi-module-compilation-for-elm
ChristophP Jun 17, 2022
a74be4b
elm: fix integration test
Jun 17, 2022
f96d25a
Merge branch 'add-multi-module-compilation-for-elm' of github.com:Chr…
Jun 17, 2022
199584d
Merge branch 'v2' into add-multi-module-compilation-for-elm
ChristophP Jun 17, 2022
ea19846
Merge branch 'v2' into add-multi-module-compilation-for-elm
devongovett Jun 27, 2022
5fb9d8e
Merge branch 'v2' into add-multi-module-compilation-for-elm
mischnic Jul 20, 2022
67a3504
bump terser to 5.14.2 to fix issues around hanging production build
Jul 31, 2022
fa1769e
Merge branch 'v2' into add-multi-module-compilation-for-elm
devongovett Jul 31, 2022
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
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;
}
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};