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

feat(gatsby): enable modern builds for gatsby #14289

Closed
wants to merge 16 commits into from
Closed
36 changes: 26 additions & 10 deletions packages/babel-preset-gatsby/src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,9 @@ it(`Specifies proper presets and plugins for test stage`, () => {
})

it(`Specifies proper presets and plugins for build-html stage`, () => {
const currentGatsbyBuildStage = process.env.GATSBY_BUILD_STAGE
let presets, plugins
try {
process.env.GATSBY_BUILD_STAGE = `build-html`
const config = preset()
presets = config.presets
plugins = config.plugins
} finally {
process.env.GATSBY_BUILD_STAGE = currentGatsbyBuildStage
}
const config = preset(null, { stage: `build-html` })
const presets = config.presets
const plugins = config.plugins

expect(presets).toEqual([
[
Expand Down Expand Up @@ -105,6 +98,7 @@ it(`Specifies proper presets and plugins for build-html stage`, () => {
it(`Allows to configure browser targets`, () => {
const targets = `last 1 version`
const { presets } = preset(null, {
stage: `build-javascript`,
targets,
})

Expand All @@ -119,3 +113,25 @@ it(`Allows to configure browser targets`, () => {
},
])
})

it(`Allows to configure modern builds`, () => {
const targets = `last 1 version`
const { presets } = preset(null, {
stage: `build-javascript`,
targets,
modern: true,
})

expect(presets[0]).toEqual([
expect.stringContaining(path.join(`@babel`, `preset-env`)),
{
corejs: 2,
loose: true,
modules: false,
useBuiltIns: `usage`,
targets: {
esmodules: true,
},
},
])
})
9 changes: 8 additions & 1 deletion packages/babel-preset-gatsby/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = function preset(_, options = {}) {
let { targets = null } = options

const pluginBabelConfig = loadCachedConfig()
const stage = process.env.GATSBY_BUILD_STAGE || `test`
const stage = options.stage || `test`

if (!targets) {
if (stage === `build-html` || stage === `test`) {
Expand All @@ -40,6 +40,13 @@ module.exports = function preset(_, options = {}) {
}
}

// when options.modern is set we overwrite targets to contain esmodules = true
if (options.modern) {
targets = {
esmodules: true,
}
}

return {
presets: [
[
Expand Down
10 changes: 7 additions & 3 deletions packages/gatsby/cache-dir/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,13 @@ const fetchResource = resourceName => {

const prefetchResource = resourceName => {
if (resourceName.slice(0, 12) === `component---`) {
return Promise.all(
createComponentUrls(resourceName).map(url => prefetchHelper(url))
)
let componentUrls = createComponentUrls(resourceName)
if (process.env.MODERN_BUILD) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

lets filter inside static-entry to only save modern / legacy assets inside the window.___chunkMapping variable

componentUrls = componentUrls.filter(resource =>
resource.endsWith(`.mjs`)
)
}
return Promise.all(componentUrls.map(url => prefetchHelper(url)))
} else {
const url = createJsonURL(jsonDataPaths[resourceName])
return prefetchHelper(url)
Expand Down
75 changes: 65 additions & 10 deletions packages/gatsby/cache-dir/static-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const fs = require(`fs`)
const { join } = require(`path`)
const { renderToString, renderToStaticMarkup } = require(`react-dom/server`)
const { ServerLocation, Router, isRedirect } = require(`@reach/router`)
const { get, merge, isObject, flatten, uniqBy } = require(`lodash`)
const { get, merge, mergeWith, isObject, flatten, uniqBy } = require(`lodash`)

const apiRunner = require(`./api-runner-ssr`)
const syncRequires = require(`./sync-requires`)
Expand All @@ -14,13 +14,32 @@ const { version: gatsbyVersion } = require(`gatsby/package.json`)
const pagesObjectMap = new Map()
pages.forEach(p => pagesObjectMap.set(p.path, p))

const stats = JSON.parse(
const statsLegacy = JSON.parse(
fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`)
)
const statsModern = JSON.parse(
fs.readFileSync(`${process.cwd()}/public/webpack.stats.modern.json`, `utf-8`)
)

const chunkMapping = JSON.parse(
const chunkMappingLegacy = JSON.parse(
fs.readFileSync(`${process.cwd()}/public/chunk-map.json`, `utf-8`)
)
const chunkMappingModern = JSON.parse(
wardpeet marked this conversation as resolved.
Show resolved Hide resolved
fs.readFileSync(`${process.cwd()}/public/chunk-map.modern.json`, `utf-8`)
wardpeet marked this conversation as resolved.
Show resolved Hide resolved
)

// eslint-disable-next-line consistent-return
const mergeWithArrayConcatenator = (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue)
}
}
const stats = mergeWith(statsLegacy, statsModern, mergeWithArrayConcatenator)
const chunkMapping = mergeWith(
chunkMappingLegacy,
chunkMappingModern,
mergeWithArrayConcatenator
)

// const testRequireError = require("./test-require-error")
// For some extremely mysterious reason, webpack adds the above module *after*
Expand Down Expand Up @@ -210,6 +229,9 @@ export default (pagePath, callback) => {
}
}

const getPreloadValue = name =>
name.endsWith(`.mjs`) ? `modulepreload` : `preload`

// Create paths to scripts
let scriptsAndStyles = flatten(
[`app`, page.componentChunkName].map(s => {
Expand All @@ -226,11 +248,11 @@ export default (pagePath, callback) => {
if (chunk === `/`) {
return null
}
return { rel: `preload`, name: chunk }
return { rel: getPreloadValue(chunk), name: chunk }
})

namedChunkGroups[s].assets.forEach(asset =>
chunks.push({ rel: `preload`, name: asset })
chunks.push({ rel: getPreloadValue(asset), name: asset })
)

const childAssets = namedChunkGroups[s].childAssets
Expand All @@ -247,12 +269,13 @@ export default (pagePath, callback) => {
})
)
.filter(s => isObject(s))
.sort((s1, s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload
.sort((s1, s2) => (s1.rel == getPreloadValue(s1.name) ? -1 : 1)) // given priority to preload

scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name)

const scripts = scriptsAndStyles.filter(
script => script.name && script.name.endsWith(`.js`)
script =>
script.name &&
(script.name.endsWith(`.js`) || script.name.endsWith(`.mjs`))
)
const styles = scriptsAndStyles.filter(
style => style.name && style.name.endsWith(`.css`)
Expand All @@ -275,6 +298,8 @@ export default (pagePath, callback) => {
scripts
.slice(0)
.reverse()
// remove legacy scripts from our preload as we preload our modern files instead
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure if there is a workaround for this that we can have our cake and eat it too. A way to do this is document.write but I rather don't want to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let's add a disclaimer to the PR to say that we lose preload for legacy browers on first run

Copy link
Contributor

Choose a reason for hiding this comment

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

Added the note!

.filter(script => script.name.endsWith(`.mjs`))
.forEach(script => {
// Add preload/prefetch <link>s for scripts.
headComponents.push(
Expand Down Expand Up @@ -368,14 +393,44 @@ export default (pagePath, callback) => {
// Filter out prefetched bundles as adding them as a script tag
// would force high priority fetching.
const bodyScripts = scripts
.filter(s => s.rel !== `prefetch`)
.filter(s => s.rel === `modulepreload`)
.map(s => {
const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(s.name).slice(
1,
-1
)}`
return <script key={scriptPath} src={scriptPath} type="module" async />
})

// Patches browsers who have a flawed noModule - module system
// Sadly we lose preload for legacy browsers
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
// Sadly we lose preload for legacy browsers
// Sadly we lose the benefit of the preload scanner for legacy browsers
// because the the sources are hidden in javascript.

// @see https://caniuse.com/#feat=es6-module
// 1. Safari 10.1 supports modules, but does not support the `nomodule` attribute - it will load <script nomodule> anyway.
// 2. Edge does not executes noModule but still fetches it
const noModuleBugFixScripts = `(function(b){function c(e){var d=b.createElement("script");d.src=e;b.body.appendChild(d)}"noModule"in b.createElement("script")||/Version\\/10\\.1(\\.\\d+)* Safari|Version\\/10\\.\\d(\\.\\d+)*.*Safari|Edge\\/1[6-8]\\.\\d/i.test(navigator.userAgent)||(%scripts%)})(document)`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had no clue on how to write this in a nicer way. Basically it detects if noModule exists or it does not exists and is safari 10.1.x or safari ios 10.x as they do have know type="module" but not nomodule

Suggested change
const noModuleBugFixScripts = `(function(b){function c(e){var d=b.createElement("script");d.src=e;b.body.appendChild(d)}"noModule"in b.createElement("script")||/Version\\/10\\.1(\\.\\d+)* Safari|Version\\/10\\.\\d(\\.\\d+)*.*Safari|Edge\\/1[6-8]\\.\\d/i.test(navigator.userAgent)||(%scripts%)})(document)`
const noModuleBugFixScripts = `(function(b){function c(e){var d=b.createElement("script");d.src=e;b.body.appendChild(d)}"noModule"in b.createElement("script")||/Version\\/10\\.1(\\.\\d+)* Safari|Version\\/10\\.\\d(\\.\\d+)*.*Safari/i.test(navigator.userAgent)||(%scripts%)})(document)`


const legacyScrips = scripts
.filter(s => s.rel !== `prefetch` && s.rel !== `modulepreload`)
.map(s => {
const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(s.name).slice(
1,
-1
)}`
return <script key={scriptPath} src={scriptPath} async />

return `c('${scriptPath}')`
})
bodyScripts.push(
<script
key="noModuleFix"
dangerouslySetInnerHTML={{
__html: noModuleBugFixScripts.replace(
`%scripts%`,
legacyScrips.join(`,`)
),
}}
noModule={true}
/>
)

postBodyComponents.push(...bodyScripts)

Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby/src/commands/build-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const doBuildRenderer = async (program, webpackConfig) => {

const buildRenderer = async (program, stage) => {
const { directory } = program
const config = await webpackConfig(program, directory, stage, null)
const config = await webpackConfig(program, directory, stage)
return await doBuildRenderer(program, config)
}

Expand Down
11 changes: 9 additions & 2 deletions packages/gatsby/src/commands/build-javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ const webpackConfig = require(`../utils/webpack.config`)
module.exports = async program => {
const { directory } = program

const compilerConfig = await webpackConfig(
const legacyConfig = await webpackConfig(
program,
directory,
`build-javascript`
)

const modernConfig = await webpackConfig(
program,
directory,
`build-javascript`,
{ modern: true }
)

return new Promise((resolve, reject) => {
webpack(compilerConfig).run((err, stats) => {
webpack([legacyConfig, modernConfig]).run((err, stats) => {
if (err) {
reject(err)
return
Expand Down
24 changes: 24 additions & 0 deletions packages/gatsby/src/redux/__tests__/__snapshots__/babelrc.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,30 @@ Array [
Array [
Array [
"/path/to/module/babel-preset-gatsby",
Object {
"modern": false,
"stage": "test",
},
],
Object {
"type": "preset",
},
],
Array [
Array [
"/path/to/module/babel-plugin-remove-graphql-queries",
],
Object {
"type": "plugin",
},
],
Array [
Array [
"/path/to/module/babel-preset-gatsby",
Object {
"modern": true,
"stage": "test",
},
],
Object {
"type": "preset",
Expand Down
3 changes: 2 additions & 1 deletion packages/gatsby/src/redux/__tests__/babelrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ describe(`Babelrc actions/reducer`, () => {
const fakeResolver = moduleName => `/path/to/module/${moduleName}`
const babel = { createConfigItem: jest.fn() }

prepareOptions(babel, fakeResolver)
prepareOptions(babel, { stage: `test` }, fakeResolver)
prepareOptions(babel, { modern: true }, fakeResolver)

expect(babel.createConfigItem.mock.calls).toMatchSnapshot()
})
Expand Down
22 changes: 15 additions & 7 deletions packages/gatsby/src/utils/babel-loader-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ const loadCachedConfig = () => {
return pluginBabelConfig
}

const getCustomOptions = () => {
const getCustomOptions = stage => {
const pluginBabelConfig = loadCachedConfig()
const stage = process.env.GATSBY_BUILD_STAGE || `test`
return pluginBabelConfig.stages[stage].options
}

const prepareOptions = (babel, resolve = require.resolve) => {
const prepareOptions = (babel, options = {}, resolve = require.resolve) => {
let pluginBabelConfig = loadCachedConfig()

const stage = process.env.GATSBY_BUILD_STAGE || `test`
const { modern, stage = `test` } = options

// Required plugins/presets
const requiredPlugins = [
Expand Down Expand Up @@ -56,9 +55,18 @@ const prepareOptions = (babel, resolve = require.resolve) => {
const fallbackPresets = []

fallbackPresets.push(
babel.createConfigItem([resolve(`babel-preset-gatsby`)], {
type: `preset`,
})
babel.createConfigItem(
[
resolve(`babel-preset-gatsby`),
{
modern: !!modern,
stage,
},
],
{
type: `preset`,
}
)
)
// Go through babel state and create config items for presets/plugins from.
const reduxPlugins = []
Expand Down
12 changes: 8 additions & 4 deletions packages/gatsby/src/utils/babel-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,31 @@ const {
module.exports = babelLoader.custom(babel => {
const toReturn = {
// Passed the loader options.
customOptions(options) {
customOptions({ modern, stage, ...options }) {
return {
custom: {
modern,
stage,
},
loader: {
cacheDirectory: true,
sourceType: `unambiguous`,
...getCustomOptions(),
...getCustomOptions(stage || `test`),
...options,
},
}
},

// Passed Babel's 'PartialConfig' object.
config(partialConfig) {
config(partialConfig, { customOptions }) {
let { options } = partialConfig
const [
reduxPresets,
reduxPlugins,
requiredPresets,
requiredPlugins,
fallbackPresets,
] = prepareOptions(babel)
] = prepareOptions(babel, customOptions)

// If there is no filesystem babel config present, add our fallback
// presets/plugins.
Expand Down
Loading