-
Notifications
You must be signed in to change notification settings - Fork 67
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
Feature/esm views #1686
Changes from all commits
c5c50db
f139c67
a2c91cb
3d3352d
5fcacfa
8fb8342
b20e694
bdfd0ec
8986bd9
9bd0894
510fb6b
92597a4
8bf2aea
c6df324
5a340d0
3122acf
1f7a830
8b1f89b
f075c72
7c98350
5f3c001
7b45909
f97c6f2
bc9cb4c
87964dd
187f866
e1abbef
8816b88
e5d772f
51e770e
b16fae1
681ab74
07052ac
55df9e0
76ce1bb
dc846d8
43c42b7
bd5e86b
50332fd
fb7db88
5a6dfdc
47e1ec4
64f46f7
41ded67
b2ca58b
8cfe64f
d8562e6
4f1af82
d2aacf6
d2b10e9
69da976
c40158b
074bff4
4baca0a
9f48679
8115519
8b3f1f5
8901b9f
0a0bc65
4abf766
4f9cba9
a31fb3f
af674c5
d6d2e8c
49c0604
69c480c
93e0adb
a60bec5
b0ccaf4
8c0a688
375c7e8
309cf20
4e46075
865b80f
f168606
2130f56
7fa41ae
d34acec
c189293
455cb57
c051e92
94e3790
986aea8
6260a07
b555499
c53c5fa
1444b6f
dd64eab
925b5a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"modular-scripts": minor | ||
--- | ||
|
||
Add `esm-view` modular type |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,9 +27,20 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); | |
const postcssNormalize = require('postcss-normalize'); | ||
const isCI = require('is-ci'); | ||
|
||
const esbuildTargetFactory = process.env.ESBUILD_TARGET_FACTORY | ||
? JSON.parse(process.env.ESBUILD_TARGET_FACTORY) | ||
: 'es2015'; | ||
const isApp = process.env.MODULAR_IS_APP === 'true'; | ||
const isEsmView = !isApp; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was originally going to ask:
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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not sure I understand, but if we are running Webpack we are sure that we're either building an |
||
|
||
// If it's an app, set it at ESBUILD_TARGET_FACTORY or default to es2015 | ||
// If it's not an app it's an ESM view, then we need es2020 | ||
const esbuildTargetFactory = isApp | ||
? process.env.ESBUILD_TARGET_FACTORY | ||
? JSON.parse(process.env.ESBUILD_TARGET_FACTORY) | ||
: 'es2015' | ||
: 'es2020'; | ||
|
||
const { externalDependencies } = process.env.MODULAR_PACKAGE_DEPS | ||
? JSON.parse(process.env.MODULAR_PACKAGE_DEPS) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this throw an error if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're only going to be using There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because then we would need to either compile our The real problem here is that 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed to do this in another PR. |
||
: {}; | ||
|
||
// Source maps are resource heavy and can cause out of memory issue for large source files. | ||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; | ||
|
@@ -60,6 +71,17 @@ const sassModuleRegex = /\.module\.(scss|sass)$/; | |
module.exports = function (webpackEnv) { | ||
const isEnvDevelopment = webpackEnv === 'development'; | ||
const isEnvProduction = webpackEnv === 'production'; | ||
const isEsmViewDevelopment = isEsmView & isEnvDevelopment; | ||
|
||
// This is needed if we're serving a ESM view in development node, since it won't be defined in the view dependencies. | ||
if (externalDependencies.react && isEsmViewDevelopment) { | ||
externalDependencies['react-dom'] = externalDependencies.react; | ||
} | ||
|
||
// Create a map of external dependencies if we're building a ESM view | ||
const dependencyMap = isEsmView | ||
? createExternalDependenciesMap(externalDependencies) | ||
: {}; | ||
|
||
// Variable used for enabling profiling in Production | ||
// passed into alias object. Uses a flag if passed into the build command | ||
|
@@ -135,6 +157,35 @@ module.exports = function (webpackEnv) { | |
}; | ||
|
||
const webpackConfig = { | ||
externals: isApp | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed to do this in another PR. |
||
? undefined | ||
: function ({ request }, callback) { | ||
const parsedModule = parsePackageName(request); | ||
|
||
// If the module is absolute and it is in the import map, we want to externalise it | ||
if ( | ||
parsedModule && | ||
parsedModule.dependencyName && | ||
dependencyMap[parsedModule.dependencyName] | ||
) { | ||
const { dependencyName, submodule } = parsedModule; | ||
|
||
const toRewrite = `${dependencyMap[dependencyName]}${ | ||
submodule ? `/${submodule}` : '' | ||
}`; | ||
|
||
return callback(null, toRewrite); | ||
} | ||
// Otherwise we just want to bundle it | ||
return callback(); | ||
}, | ||
|
||
externalsType: isApp ? undefined : 'module', | ||
experiments: { | ||
outputModule: isApp ? undefined : true, | ||
}, | ||
// Workaround for this bug: https://stackoverflow.com/questions/53905253/cant-set-up-the-hmr-stuck-with-waiting-for-update-signal-from-wds-in-cons | ||
target: 'web', | ||
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', | ||
// Stop compilation early in production | ||
bail: isEnvProduction, | ||
|
@@ -145,8 +196,15 @@ module.exports = function (webpackEnv) { | |
: isEnvDevelopment && 'cheap-module-source-map', | ||
// These are the "entry points" to our application. | ||
// This means they will be the "root" imports that are included in JS bundle. | ||
entry: paths.appIndexJs, | ||
// We bundle a virtual file to trampoline the ESM view as an entry point if we're starting it (ESM views have no ReactDOM.render) | ||
entry: isEsmViewDevelopment ? getVirtualTrampoline() : paths.appIndexJs, | ||
output: { | ||
module: isApp ? undefined : true, | ||
library: isApp | ||
? undefined | ||
: { | ||
type: 'module', | ||
}, | ||
// The build folder. | ||
path: isEnvProduction ? paths.appBuild : undefined, | ||
// Add /* filename */ comments to generated require()s in the output. | ||
|
@@ -223,15 +281,19 @@ module.exports = function (webpackEnv) { | |
// Automatically split vendor and commons | ||
// https://twitter.com/wSokra/status/969633336732905474 | ||
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 | ||
splitChunks: { | ||
chunks: 'all', | ||
}, | ||
splitChunks: isApp | ||
? { | ||
chunks: 'all', | ||
} | ||
: undefined, | ||
// Keep the runtime chunk separated to enable long term caching | ||
// https://twitter.com/wSokra/status/969679223278505985 | ||
// https://github.com/facebook/create-react-app/issues/5358 | ||
runtimeChunk: { | ||
name: (entrypoint) => `runtime-${entrypoint.name}`, | ||
}, | ||
runtimeChunk: isApp | ||
? { | ||
name: (entrypoint) => `runtime-${entrypoint.name}`, | ||
} | ||
: undefined, | ||
}, | ||
resolve: { | ||
// This allows you to set a fallback for where webpack should look for modules. | ||
|
@@ -249,7 +311,7 @@ module.exports = function (webpackEnv) { | |
// for React Native Web. | ||
extensions: paths.moduleFileExtensions | ||
.map((ext) => `.${ext}`) | ||
.filter((ext) => useTypeScript || !ext.includes('ts')), | ||
.filter((ext) => useTypeScript || !isApp || !ext.includes('ts')), | ||
alias: { | ||
// Support React Native Web | ||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ | ||
|
@@ -458,43 +520,64 @@ module.exports = function (webpackEnv) { | |
}, | ||
plugins: [ | ||
// Generates an `index.html` file with the <script> injected. | ||
new HtmlWebpackPlugin( | ||
Object.assign( | ||
{}, | ||
{ | ||
inject: true, | ||
template: paths.appHtml, | ||
}, | ||
isEnvProduction | ||
? { | ||
minify: { | ||
removeComments: true, | ||
collapseWhitespace: true, | ||
removeRedundantAttributes: true, | ||
useShortDoctype: true, | ||
removeEmptyAttributes: true, | ||
removeStyleLinkTypeAttributes: true, | ||
keepClosingSlash: true, | ||
minifyJS: true, | ||
minifyCSS: true, | ||
minifyURLs: true, | ||
}, | ||
} | ||
: undefined, | ||
), | ||
), | ||
isApp | ||
? new HtmlWebpackPlugin( | ||
Object.assign( | ||
{}, | ||
{ | ||
inject: true, | ||
template: paths.appHtml, | ||
}, | ||
isEnvProduction | ||
? { | ||
minify: { | ||
removeComments: true, | ||
collapseWhitespace: true, | ||
removeRedundantAttributes: true, | ||
useShortDoctype: true, | ||
removeEmptyAttributes: true, | ||
removeStyleLinkTypeAttributes: true, | ||
keepClosingSlash: true, | ||
minifyJS: true, | ||
minifyCSS: true, | ||
minifyURLs: true, | ||
}, | ||
} | ||
: undefined, | ||
), | ||
) | ||
: isEsmViewDevelopment | ||
? // We need to provide a synthetic index.html in case we're starting a ESM view | ||
new HtmlWebpackPlugin( | ||
Object.assign( | ||
{}, | ||
{ | ||
inject: true, | ||
templateContent: ` | ||
<html> | ||
<body> | ||
<div id="root"></div> | ||
</body> | ||
</html> | ||
`, | ||
scriptLoading: 'module', | ||
}, | ||
), | ||
) | ||
: false, | ||
// Inlines the webpack runtime script. This script is too small to warrant | ||
// a network request. | ||
// https://github.com/facebook/create-react-app/issues/5358 | ||
isEnvProduction && | ||
isApp && | ||
isEnvProduction && | ||
shouldInlineRuntimeChunk && | ||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]), | ||
// Makes some environment variables available in index.html. | ||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.: | ||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico"> | ||
// It will be an empty string unless you specify "homepage" | ||
// in `package.json`, in which case it will be the pathname of that URL. | ||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), | ||
isApp && new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), | ||
// This gives some necessary context to module not found errors, such as | ||
// the requesting resource. | ||
new ModuleNotFoundPlugin(paths.appPath), | ||
|
@@ -651,3 +734,49 @@ module.exports = function (webpackEnv) { | |
|
||
return webpackConfig; | ||
}; | ||
|
||
function createExternalDependenciesMap(externalDependencies) { | ||
const externalCdnTemplate = | ||
process.env.EXTERNAL_CDN_TEMPLATE || | ||
'https://cdn.skypack.dev/[name]@[version]'; | ||
|
||
return Object.entries(externalDependencies).reduce( | ||
(acc, [name, version]) => ({ | ||
...acc, | ||
[name]: externalCdnTemplate | ||
.replace('[name]', name) | ||
.replace('[version]', version), | ||
}), | ||
{}, | ||
); | ||
} | ||
|
||
const packageRegex = | ||
/^(@[a-z0-9-~][a-z0-9-._~]*)?\/?([a-z0-9-~][a-z0-9-._~]*)\/?(.*)/; | ||
function parsePackageName(name) { | ||
const parsedName = packageRegex.exec(name); | ||
if (!parsedName) { | ||
return; | ||
} | ||
const [_, scope, module, submodule] = parsedName; | ||
const dependencyName = (scope ? `${scope}/` : '') + module; | ||
return { dependencyName, scope, module, submodule }; | ||
} | ||
|
||
// Virtual entrypoint if we're starting a ESM view - see https://github.com/webpack/webpack/issues/6437 | ||
function getVirtualTrampoline() { | ||
const entryPointPath = `'./${path.relative( | ||
paths.appPath, | ||
paths.appIndexJs, | ||
)}'`; | ||
const string = ` | ||
import ReactDOM from 'react-dom' | ||
import React from 'react'; | ||
import Component from ${entryPointPath}; | ||
const DOMRoot = document.getElementById('root'); | ||
ReactDOM.render(React.createElement(Component, null), DOMRoot); | ||
`; | ||
|
||
const base64 = Buffer.from(string).toString('base64'); | ||
return `./src/_trampoline.js!=!data:text/javascript;base64,${base64}`; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import * as React from 'react'; | ||
|
||
export default function SampleESMView(): JSX.Element { | ||
return <div data-testid="test-this">this is a modular esm-view</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import * as React from 'react'; | ||
import get from 'lodash/get'; | ||
import merge from 'lodash.merge'; | ||
import { difference } from 'lodash'; | ||
|
||
export default function SampleView(): JSX.Element { | ||
return ( | ||
<div> | ||
<pre>{JSON.stringify({ get, merge, difference })}</pre> | ||
</div> | ||
); | ||
} |
There was a problem hiding this comment.
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 matchestrue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds better, but we have this half-convention of prepending env variables passed to the Webpack configuration with
MODULAR_
:It's passed only by our code, so we're pretty sure it's
true
(unlessJSON.stringify(boolean)
changes case), but if you think it's more hygienic I can do that.