Skip to content

Commit

Permalink
Composing gatsby sites
Browse files Browse the repository at this point in the history
This is the first step towards gatsby themes. It is low level and
defines the way multiple gatsby sites compose by defining the way in
which gatsby-config's compose.

For those that are mathematically inclined, this defines a monoid for
the gatsby-config data structure such that `(siteA <> siteB) <> siteC
=== siteA <> (siteB <> siteC)`.

This method of composition opens the door to themes and sub-themes and
allows us to get more input into how to deal with potentially
conflicting artifacts (such as two singleton plugins being defined).

A theme is defined as a parameterizable gatsby site. This means that
gatsby-config can be a function that accepts configuration from the
end user or a subtheme.
  • Loading branch information
ChristopherBiscardi committed Oct 9, 2018
1 parent f008878 commit 73e8c76
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 2 deletions.
33 changes: 31 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,41 @@ 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: [
{ resolve: themeName, options: themeConfig },
...(themeConfigObj.plugins || []),
],
}
}
).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
83 changes: 83 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,83 @@
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 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/",
},
})
})
})
16 changes: 16 additions & 0 deletions packages/gatsby/src/utils/merge-gatsby-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const _ = require("lodash")
/**
* Defines how a theme object is merged with the user's config
*/
module.exports = (a, b) =>
_.uniq(Object.keys(a).concat(Object.keys(b))).reduce((acc, key) => {
const mergeFn = mergeAlgo[key]
acc[key] = mergeFn ? mergeFn(a[key], b[key]) : b[key] || a[key]
return acc
}, {})

const mergeAlgo = {
siteMetadata: (a, b) => _.merge({}, a, b),
plugins: (a = [], b = []) => a.concat(b),
mapping: (a, b) => _.merge({}, a, b),
}

0 comments on commit 73e8c76

Please sign in to comment.