diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index 08f52138dd072..8adc768cdd327 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -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`) @@ -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.` diff --git a/packages/gatsby/src/joi-schemas/joi.js b/packages/gatsby/src/joi-schemas/joi.js index 621e60cae3811..8a5056a9e69ca 100644 --- a/packages/gatsby/src/joi-schemas/joi.js +++ b/packages/gatsby/src/joi-schemas/joi.js @@ -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(), diff --git a/packages/gatsby/src/utils/__tests__/merge-gatsby-config.js b/packages/gatsby/src/utils/__tests__/merge-gatsby-config.js new file mode 100644 index 0000000000000..292abd5f6fbca --- /dev/null +++ b/packages/gatsby/src/utils/__tests__/merge-gatsby-config.js @@ -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: {} }, + ], + }) + 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/`, + }, + }) + }) +}) diff --git a/packages/gatsby/src/utils/merge-gatsby-config.js b/packages/gatsby/src/utils/merge-gatsby-config.js new file mode 100644 index 0000000000000..2671de74ed9a3 --- /dev/null +++ b/packages/gatsby/src/utils/merge-gatsby-config.js @@ -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), +}