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

Composing Gatsby Sites #8917

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions packages/gatsby/src/bootstrap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const crypto = require(`crypto`)
const del = require(`del`)
const path = require(`path`)
const convertHrtime = require(`convert-hrtime`)
const Promise = require(`bluebird`)

const apiRunnerNode = require(`../utils/api-runner-node`)
const mergeGatsbyConfig = require(`../utils/merge-gatsby-config`)
const { graphql } = require(`graphql`)
const { store, emitter } = require(`../redux`)
const loadPlugins = require(`./load-plugins`)
Expand Down Expand Up @@ -62,14 +64,42 @@ module.exports = async (args: BootstrapArgs) => {
})

// Try opening the site's gatsby-config.js file.
let activity = report.activityTimer(`open and validate gatsby-config`, {
let activity = report.activityTimer(`open and validate gatsby-configs`, {
parentSpan: bootstrapSpan,
})
activity.start()
const config = await preferDefault(
let config = await preferDefault(
getConfigFile(program.directory, `gatsby-config`)
)

// theme gatsby configs can be functions or objects
if (config.__experimentalThemes) {
const themesConfig = await Promise.mapSeries(
config.__experimentalThemes,
async ([themeName, themeConfig]) => {
const theme = await preferDefault(
getConfigFile(themeName, `gatsby-config`)
)
// if theme is a function, call it with the themeConfig
let themeConfigObj = theme
if (_.isFunction(theme)) {
themeConfigObj = theme(themeConfig)
}
// themes function as plugins too (gatsby-node, etc)
return {
...themeConfigObj,
plugins: [
...(themeConfigObj.plugins || []),
// theme plugin is last so it's gatsby-node, etc can override it's declared plugins, like a normal site.
{ resolve: themeName, options: themeConfig },
],
}
}
).reduce(mergeGatsbyConfig, {})

config = mergeGatsbyConfig(themesConfig, config)
}

if (config && config.polyfill) {
report.warn(
`Support for custom Promise polyfills has been removed in Gatsby v2. We only support Babel 7's new automatic polyfilling behavior.`
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/joi-schemas/joi.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Joi = require(`joi`)

export const gatsbyConfigSchema = Joi.object().keys({
__experimentalThemes: Joi.array(),
polyfill: Joi.boolean(),
siteMetadata: Joi.object(),
pathPrefix: Joi.string(),
Expand Down
113 changes: 113 additions & 0 deletions packages/gatsby/src/utils/__tests__/merge-gatsby-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const mergeGatsbyConfig = require(`../merge-gatsby-config`)

describe(`Merge gatsby config`, () => {
it(`Merging empty config is an identity operation`, () => {
const emptyConfig = {}
const basicConfig = {
plugins: [`gatsby-mdx`],
}

expect(mergeGatsbyConfig(basicConfig, emptyConfig)).toEqual(basicConfig)
expect(mergeGatsbyConfig(emptyConfig, basicConfig)).toEqual(basicConfig)
})

it(`Merging plugins concatenates them`, () => {
const basicConfig = {
plugins: [`gatsby-mdx`],
}
const morePlugins = {
plugins: [`a-plugin`, `b-plugin`, { resolve: `c-plugin`, options: {} }],
}
expect(mergeGatsbyConfig(basicConfig, morePlugins)).toEqual({
plugins: [
`gatsby-mdx`,
`a-plugin`,
`b-plugin`,
{ resolve: `c-plugin`, options: {} },
ChristopherBiscardi marked this conversation as resolved.
Show resolved Hide resolved
],
})
expect(mergeGatsbyConfig(morePlugins, basicConfig)).toEqual({
plugins: [
`a-plugin`,
`b-plugin`,
{ resolve: `c-plugin`, options: {} },
`gatsby-mdx`,
],
})
})

it(`Merging plugins uniqs them, keeping the first occurrence`, () => {
const basicConfig = {
plugins: [`gatsby-mdx`],
}
const morePlugins = {
plugins: [
`a-plugin`,
`gatsby-mdx`,
`b-plugin`,
{ resolve: `c-plugin`, options: {} },
],
}
expect(mergeGatsbyConfig(basicConfig, morePlugins)).toEqual({
plugins: [
`gatsby-mdx`,
`a-plugin`,
`b-plugin`,
{ resolve: `c-plugin`, options: {} },
],
})
expect(mergeGatsbyConfig(morePlugins, basicConfig)).toEqual({
plugins: [
`a-plugin`,
`gatsby-mdx`,
`b-plugin`,
{ resolve: `c-plugin`, options: {} },
],
})
})

it(`Merging siteMetadata is recursive`, () => {
const a = {
siteMetadata: {
title: `my site`,
something: { else: 1 },
},
}

const b = {
siteMetadata: {
something: { nested: 2 },
},
}

expect(mergeGatsbyConfig(a, b)).toEqual({
siteMetadata: {
title: `my site`,
something: { else: 1, nested: 2 },
},
})
})

it(`Merging proxy is overriden`, () => {
const a = {
proxy: {
prefix: `/something-not/api`,
url: `http://examplesite.com/api/`,
},
}

const b = {
proxy: {
prefix: `/api`,
url: `http://examplesite.com/api/`,
},
}

expect(mergeGatsbyConfig(a, b)).toEqual({
proxy: {
prefix: `/api`,
url: `http://examplesite.com/api/`,
},
})
})
})
39 changes: 39 additions & 0 deletions packages/gatsby/src/utils/merge-gatsby-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const _ = require(`lodash`)
/**
* Defines how a theme object is merged with the user's config
*/
module.exports = (a, b) => {
// a and b are gatsby configs, If they have keys, that means there are values to merge
const allGatsbyConfigKeysWithAValue = _.uniq(
Object.keys(a).concat(Object.keys(b))
)

// reduce the array of mergable keys into a single gatsby config object
const mergedConfig = allGatsbyConfigKeysWithAValue.reduce(
(config, gatsbyConfigKey) => {
// choose a merge function for the config key if there's one defined,
// otherwise use the default value merge function
const mergeFn = howToMerge[gatsbyConfigKey] || howToMerge.byDefault
return {
...config,
[gatsbyConfigKey]: mergeFn(a[gatsbyConfigKey], b[gatsbyConfigKey]),
}
},
{}
)

// return the fully merged config
return mergedConfig
}
const howToMerge = {
/**
* pick a truthy value by default.
* This makes sure that if a single value is defined, that one it used.
* We prefer the "right" value, because the user's config will be "on the right"
*/
byDefault: (a, b) => b || a,
siteMetadata: (objA, objB) => _.merge({}, objA, objB),
// plugins are concatenated and uniq'd, so we don't get two of the same plugin value
plugins: (a = [], b = []) => _.uniqWith(a.concat(b), _.isEqual),
mapping: (objA, objB) => _.merge({}, objA, objB),
}