From fca5c8cdeb69be53fbbaf024c8bb42405002e04b Mon Sep 17 00:00:00 2001 From: ChristophP Date: Sun, 31 Jul 2022 20:47:31 +0200 Subject: [PATCH] Add multi module compilation for elm (#8076) --- packages/core/integration-tests/test/elm.js | 19 +++ .../integration/elm-multiple-apps/elm.json | 24 ++++ .../elm-multiple-apps/package.json | 10 ++ .../elm-multiple-apps/src/Main.elm | 7 + .../elm-multiple-apps/src/MainB.elm | 7 + .../elm-multiple-apps/src/MainC.elm | 7 + .../elm-multiple-apps/src/index.js | 5 + .../integration/elm-multiple-apps/yarn.lock | 0 packages/transformers/elm/package.json | 2 +- .../transformers/elm/src/ElmTransformer.js | 134 ++++++++---------- packages/transformers/elm/src/loadConfig.js | 62 ++++++++ 11 files changed, 204 insertions(+), 73 deletions(-) create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/elm.json create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/package.json create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/src/Main.elm create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainB.elm create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainC.elm create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/src/index.js create mode 100644 packages/core/integration-tests/test/integration/elm-multiple-apps/yarn.lock create mode 100644 packages/transformers/elm/src/loadConfig.js diff --git a/packages/core/integration-tests/test/elm.js b/packages/core/integration-tests/test/elm.js index 702be99b4f1..269f1fd27a1 100644 --- a/packages/core/integration-tests/test/elm.js +++ b/packages/core/integration-tests/test/elm.js @@ -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'); + }); }); diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/elm.json b/packages/core/integration-tests/test/integration/elm-multiple-apps/elm.json new file mode 100644 index 00000000000..dea3450db11 --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/elm.json @@ -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": {} + } +} diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/package.json b/packages/core/integration-tests/test/integration/elm-multiple-apps/package.json new file mode 100644 index 00000000000..2142bc875b3 --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/package.json @@ -0,0 +1,10 @@ +{ + "@parcel/transformer-elm": { + "extraSources": { + "./src/Main.elm": [ + "./src/MainB.elm", + "./src/MainC.elm" + ] + } + } +} diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/src/Main.elm b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/Main.elm new file mode 100644 index 00000000000..67393aaa654 --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/Main.elm @@ -0,0 +1,7 @@ +module Main exposing (main) + +import Html + + +main = + Html.text "Hello, world!" diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainB.elm b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainB.elm new file mode 100644 index 00000000000..6fdc2c1116d --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainB.elm @@ -0,0 +1,7 @@ +module MainB exposing (main) + +import Html + + +main = + Html.text "Hello, world!" diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainC.elm b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainC.elm new file mode 100644 index 00000000000..6e3d241ba64 --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/MainC.elm @@ -0,0 +1,7 @@ +module MainC exposing (main) + +import Html + + +main = + Html.text "Hello, world!" diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/src/index.js b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/index.js new file mode 100644 index 00000000000..289ca88f6c4 --- /dev/null +++ b/packages/core/integration-tests/test/integration/elm-multiple-apps/src/index.js @@ -0,0 +1,5 @@ +import { Elm } from './Main.elm?with=MainB.elm&with=MainC.elm'; + +export default function() { + return Elm; +} diff --git a/packages/core/integration-tests/test/integration/elm-multiple-apps/yarn.lock b/packages/core/integration-tests/test/integration/elm-multiple-apps/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/transformers/elm/package.json b/packages/transformers/elm/package.json index 568d723a1d2..1f64a620553 100644 --- a/packages/transformers/elm/package.json +++ b/packages/transformers/elm/package.json @@ -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" diff --git a/packages/transformers/elm/src/ElmTransformer.js b/packages/transformers/elm/src/ElmTransformer.js index 2ae6fa1f9e5..22cdb72295e 100644 --- a/packages/transformers/elm/src/ElmTransformer.js +++ b/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'); @@ -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, @@ -49,9 +36,20 @@ 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 @@ -59,15 +57,23 @@ export default (new Transformer({ 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, ''), }); } @@ -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, }); @@ -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 + }; } diff --git a/packages/transformers/elm/src/loadConfig.js b/packages/transformers/elm/src/loadConfig.js new file mode 100644 index 00000000000..645ca00e81a --- /dev/null +++ b/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 { + 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};