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

Feature/esm views #1686

Merged
merged 89 commits into from
May 12, 2022
Merged

Feature/esm views #1686

merged 89 commits into from
May 12, 2022

Conversation

cristiano-belloni
Copy link
Contributor

@cristiano-belloni cristiano-belloni commented May 3, 2022

Introduce new type esm-view, which builds like app (both webpack & esbuild), but:

  • Has a single entrypoint exporting something (typically a React component)
  • Builds an ES Module
  • Rewrites dependencies to native imports to a CDN URL (which dependencies and which CDN is configurable)
  • Can specify which dependencies are rewritten and which ones are bundled with an allow list and block list env variable
  • Creates a synthetic trampoline + index.html to run standalone - this behaviour assumes we're building a React component (as per default template), we might have different configuration options in the future

See the doc PR (WIP) for more detailed info: #1704

@coveralls
Copy link
Collaborator

coveralls commented May 5, 2022

Coverage Status

Coverage decreased (-1.4%) to 25.172% when pulling 925b5a1 on feature/esm-views into 50d456c on main.

@cristiano-belloni cristiano-belloni marked this pull request as ready for review May 5, 2022 10:48
? JSON.parse(process.env.ESBUILD_TARGET_FACTORY)
: 'es2015';
const isApp = process.env.MODULAR_IS_APP === 'true';
const isEsmView = !isApp;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally going to ask:

Is it safe to rely on process.env.MODULAR_IS_APP !== 'true' here? For my reference... where do the MODULAR_IS_APP / ESBUILD_TARGET_FACTORY environment variables come from? Are they set by the user or by reading configuration from a file etc?

having read further down I see these are set by code in the build. Might it be useful to have an environment variable to define the output type here (ie: ESM vs compiled)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having read further down I see these are set by code in the build. Might it be useful to have an environment variable to define the output type here (ie: ESM vs compiled)?

Not sure I understand, but if we are running Webpack we are sure that we're either building an app or an esm-view. Defining MODULAR_COMPILE_TARGET='esm' | 'compile' (for example) will be functionally equivalent (isApp = MODULAR_COMPILE_TARGET === 'compile') but maybe a bit more indirect to read?

@@ -135,6 +157,35 @@ module.exports = function (webpackEnv) {
};

const webpackConfig = {
externals: isApp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there are many app only configuration options here, might it be worth using a merge utility such as webpack-merge so the config can be:

const webpackConfig = merge(
  defaultConfig(),
  isApp && appConfig(),
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to do this in another PR.

packages/modular-scripts/src/addPackage.ts Outdated Show resolved Hide resolved
packages/modular-scripts/src/serve.ts Outdated Show resolved Hide resolved
const esbuildTargetFactory = process.env.ESBUILD_TARGET_FACTORY
? JSON.parse(process.env.ESBUILD_TARGET_FACTORY)
: 'es2015';
const isApp = process.env.MODULAR_IS_APP === 'true';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: IS_MODULAR_APP? and also to bring it to lowercase before checking if it matches true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: IS_MODULAR_APP?

It sounds better, but we have this half-convention of prepending env variables passed to the Webpack configuration with MODULAR_:

MODULAR_ROOT: modularRoot,
MODULAR_PACKAGE: target,
MODULAR_PACKAGE_NAME: targetName,

also to bring it to lowercase before checking if it matches true

It's passed only by our code, so we're pretty sure it's true (unless JSON.stringify(boolean) changes case), but if you think it's more hygienic I can do that.

: 'es2020';

const { externalDependencies } = process.env.MODULAR_PACKAGE_DEPS
? JSON.parse(process.env.MODULAR_PACKAGE_DEPS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this throw an error if MODULAR_PACKAGE_DEPS is anything other than an object or boolean? Can we be more defensive with this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're only going to be using externalDependencies, can we just have the dependencies in the root of the object instead of nesting it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. This env var is generated at time of execution and passed in with no expectation of user configuration.

If that's the case, why doesn't the build script just fetch the external dependencies itself instead of reading it from the env var?

Copy link
Contributor Author

@cristiano-belloni cristiano-belloni May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case, why doesn't the build script just fetch the external dependencies itself instead of reading it from the env var?

Because then we would need to either compile our getPackageDependencies.ts into javascript at runtime for webpack.config.js to use it, or "downgrade" it to js. There is no easy way for the webpack config to invoke our typescript utilities, and we use getPackageDependencies for other things (possibly even ghost testing in the future).

The real problem here is that webpack.config.js is not invoked by us, but by Webpack itself and we have to pass parameters to it via the environment, which is something we inherited from create-react-app.

I'm sure there's a way to solve this in a better way (ie generate a JSON configuration file from our build scripts), and I would like to do that, but that's for another PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this script run in the same process as the script that generates the configuration? If so we could switch to using process.modular = { packageDependencies, isApp, ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this script run in the same process as the script that generates the configuration? If so we could switch to using process.modular = { packageDependencies, isApp, ... }

I'm not really sure what you mean, but this is not really a script we execute, it's the actual Webpack configuration (that can be a javascript file), executed by Webpack itself. What we do is a bit convoluted:

  1. In build/index.ts we call all our ts utilities, get the dependencies, set the environment variables and start a new javascript process in via execa - the process is contained in react-scripts/scripts/build.js
  2. build.js starts webpack, with const compiler = webpack(configFactory('production'));. This is taken directly from CRA.
  3. The webpack library starts a Webpack process, which in turn executes the configuration at webpack.config.js (this file, which needs to be js since is directly required by Webpack, afaik).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to do this in another PR.

const string = `
import ReactDOM from 'react-dom'
import React from 'react';
import Component from './src/index.tsx';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we template in paths.appIndexJs in case the component is being exported out of an index.jsx file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, done!

@@ -129,7 +130,7 @@ async function addPackage({

const modularTemplateType = modularTemplatePackageJson?.modular
?.templateType as string;
if (!['app', 'view', 'package'].includes(modularTemplateType)) {
if (!['app', 'esm-view', 'view', 'package'].includes(modularTemplateType)) {
throw new Error(
`${templateName} has modular type: ${modularTemplateType}, which does not exist, please use update this template`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm, we're going to have to publish a modular-template-esm-view package as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done in advance, we've been already burned by this :) -> https://www.npmjs.com/package/modular-scripts

);

// Create and write trampoline file
const trampolineBuildResult = await createViewTrampoline(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createViewTrampoline uses esbuild which means it's not build system agnostic. How does this play with a standalone build run with USE_MODULAR_WEBPACK=true

Copy link
Contributor Author

@cristiano-belloni cristiano-belloni May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build the trampoline with esbuild either way. The trampoline is not user code, we 100% control its template, so how it's built makes no difference to the user. We only build it to correctly replace its deps (react-dom and react) with the CDN template + allow / block list that the user has provided, via the esbuild plugin.

<html>
<body>
<div id="root"></div>
<script type="module" src="static/js/_trampoline.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the %PUBLIC_URL% template to keep in line with the other index.html files we allow configuration for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Users are not supposed to provide their own index.html for an esm-view.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they have "homepage" set in the package.json, we want to interpolate that into as the PUBLIC_URL when we build apps with webpack (https://github.com/jpmorganchase/modular/blob/main/packages/modular-scripts/react-scripts/config/webpack.config.js#L164-L167 ) . If the esm-view is meant to be able to be built as a standalone app, we should mirror this. It'll be important for building in CI if the assets are going to be nested or not stored in root.

Copy link
Contributor Author

@cristiano-belloni cristiano-belloni May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be important for building in CI if the assets are going to be nested or not stored in root.

The only assets that are allowed for a standalone esm-view are cssEntryPoint, jsEntryPoint and the runtime index.js if we're modular starting the application. We insert them dynamically - respecting the %PUBLIC_URL% - in the createIndex function just below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set the PUBLIC_URL for the js and css entrypoints, why not also set them for the trampoline file?
<script type="module" src="%PUBLIC_URL%/static/js/_trampoline.js"></script>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set the PUBLIC_URL for the js and css entrypoints, why not also set them for the trampoline file? <script type="module" src="%PUBLIC_URL%/static/js/_trampoline.js"></script>

Ah, ok, I understand. This sparked a mini refactoring of api.ts, with common code in just one place and a better interface, plus obviously %PUBLIC_URL% interpolation. Done.

@steveukx steveukx added this to the ESM View Support milestone May 10, 2022
@@ -50,6 +50,7 @@ async function addPackage({
choices: [
{ title: 'A plain package', value: 'package' },
{ title: 'A view within an application', value: 'view' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to be clear what the difference between view and esm-view, could we update the title of view to indicate it's not an esm?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cangarugula Happy to change it. Not sure of how to word this though, "A non-ESM view within an application" sounds bad. Do you have suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll bring some more options to the table to help us think through what we're trying to convey.

The first and most concise is to simply say what these things are and require the reader to view the docs to understand what "view" and "app" mean.

  • A plain package
  • A view compiled to ES Modules
  • A view compiled to a bundle file
  • A standalone app
  • Choose my own template

Following my previous logic, we could even shorten "A standalone app" to "An app".

However, if we wanted to be more descriptive, we could throw in an indication of how these things relate to micro-frontends, which is modular's thing:

  • A plain package
  • A micro-frontend view compiled to ES Modules
  • A micro-frontend view compiled to a bundle file
  • A micro-frontend container app
  • Choose my own template

We can even shorten things a little bit:

  • A plain package
  • A micro-frontend view (ES Modules)
  • A micro-frontend view (bundle file)
  • A micro-frontend container app
  • Choose my own template

What do people think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add the type to better disambiguate?

  • A plain package (package)
  • A micro-frontend view compiled to ES Modules (esm-view)
  • A micro-frontend view compiled to a bundle file (view)
  • A micro-frontend container app (app)
  • Choose my own template

@cangarugula @steveukx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add "react" in any of these titles or is that already obvious?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably just choose one and change it in the future if we think it's confusing. I'll start a poll later.

Copy link
Contributor Author

@cristiano-belloni cristiano-belloni May 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cangarugula @benpryke and @steveukx chose Option C. I incorporated it and:

  1. Incorporated @benpryke's suggestion of not repeating the word "app"
  2. Incorporated @cangarugula's preference of specifying "react" for esm-views, views and apps

@cristiano-belloni cristiano-belloni merged commit da4eeed into main May 12, 2022
@cristiano-belloni cristiano-belloni deleted the feature/esm-views branch May 12, 2022 13:35
@github-actions github-actions bot mentioned this pull request May 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants