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

Add ability to locally define plugins #1126

Merged
merged 13 commits into from
Jun 13, 2017
32 changes: 32 additions & 0 deletions docs/docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,38 @@ module.exports = {
Plugins can take options. See each plugin page below for more detailed documentation
on using each plugin.

## Locally defined plugins

When you want to work on a new plugin, or maybe write one that is only relevant
to your specific use-case, a locally defined plugin is more convenient than
having to create an NPM package for it.

You can place the code in the `plugins` folder in the root of your project like
this:

```
plugins
└── my-own-plugin
├── gatsby-node.js
└── package.json
```

Each plugin requires a package.json file, but the minimum content is just an
empty object `{}`. The `name` and `version` fields are read from the package file.
The name is used to identify the plugin when it mutates the GraphQL data structure.
The version is used to clear the cache when it changes.

For local plugins it is best to leave the version field empty. Gatsby will
generate an md5-hash from all gatsby-* file contents and use that as the version.
This way the cache is automatically flushed when you change the code of your
plugin.

If the name is empty it is inferred from the plugin folder name.

Like all gatsby-* files, the code is not being processed by Babel. If you
want to use javascript syntax which isn't supported by your version of Node.js,
you can place the files in a `src` subfolder and build them to the plugin folder root.

## Official plugins

* [gatsby-plugin-catch-links](/docs/packages/gatsby-plugin-catch-links/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@ Array [
"plugins": Array [],
},
"resolve": "",
"version": undefined,
},
Object {
"name": "default-site-plugin",
"pluginOptions": Object {
"plugins": Array [],
},
"resolve": "",
"version": "n/a",
"version": "d41d8cd98f00b204e9800998ecf8427e",
},
]
`;
Expand Down Expand Up @@ -93,7 +92,7 @@ Array [
"plugins": Array [],
},
"resolve": "",
"version": "n/a",
"version": "d41d8cd98f00b204e9800998ecf8427e",
},
]
`;
108 changes: 89 additions & 19 deletions packages/gatsby/src/bootstrap/load-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,82 @@ const _ = require(`lodash`)
const slash = require(`slash`)
const fs = require(`fs`)
const path = require(`path`)

const crypto = require(`crypto`)
const { store } = require(`../redux`)
const nodeAPIs = require(`../utils/api-node-docs`)
const glob = require(`glob`)

function createFileContentHash(root, globPattern) {
const hash = crypto.createHash(`md5`)
const files = glob.sync(`${root}/${globPattern}`, { nodir: true })

files.forEach(filepath => {
hash.update(fs.readFileSync(filepath))
})

return hash.digest(`hex`)
}

/**
* @typedef {Object} PluginInfo
* @property {string} resolve The absolute path to the plugin
* @property {string} name The plugin name
* @property {string} version The plugin version (can be content hash)
*/

/**
* resolvePlugin
* @param {string} pluginName
* This can be a name of a local plugin, the name of a plugin located in
* node_modules, or a Gatsby internal plugin. In the last case the pluginName
* will be an absolute path.
* @return {PluginInfo}
*/
function resolvePlugin(pluginName) {
// Only find plugins when we're not given an absolute path
if (!fs.existsSync(pluginName)) {
// Find the plugin in the local plugins folder
const resolvedPath = slash(path.resolve(`./plugins/${pluginName}`))

if (fs.existsSync(resolvedPath)) {
if (fs.existsSync(`${resolvedPath}/package.json`)) {
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)

return {
resolve: resolvedPath,
name: packageJSON.name || pluginName,
version:
packageJSON.version || createFileContentHash(resolvedPath, `**`),
}
} else {
// Make package.json a requirement for local plugins too
throw new Error(`Plugin ${pluginName} requires a package.json file`)
}
}
}

/**
* Here we have an absolute path to an internal plugin, or a name of a module
* which should be located in node_modules.
*/
try {
const resolvedPath = slash(path.dirname(require.resolve(pluginName)))

const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)

return {
resolve: resolvedPath,
name: packageJSON.name,
version: packageJSON.version,
}
} catch (err) {
throw new Error(`Unable to find plugin "${pluginName}"`)
}
}

module.exports = async (config = {}) => {
// Instantiate plugins.
Expand All @@ -15,14 +88,10 @@ module.exports = async (config = {}) => {
// Also test adding to redux store.
const processPlugin = plugin => {
if (_.isString(plugin)) {
const resolvedPath = slash(path.dirname(require.resolve(plugin)))
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
const info = resolvePlugin(plugin)

return {
resolve: resolvedPath,
name: packageJSON.name,
version: packageJSON.version,
...info,
pluginOptions: {
plugins: [],
},
Expand All @@ -40,18 +109,19 @@ module.exports = async (config = {}) => {

// Add some default values for tests as we don't actually
// want to try to load anything during tests.
let resolvedPath
let packageJSON = { name: `TEST` }
if (plugin.resolve !== `___TEST___`) {
resolvedPath = slash(path.dirname(require.resolve(plugin.resolve)))
packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
if (plugin.resolve === `___TEST___`) {
return {
name: `TEST`,
pluginOptions: {
plugins: [],
},
}
}

const info = resolvePlugin(plugin.resolve)

return {
resolve: resolvedPath,
name: packageJSON.name,
version: packageJSON.version,
...info,
pluginOptions: _.merge({ plugins: [] }, plugin.options),
}
}
Expand Down Expand Up @@ -86,7 +156,7 @@ module.exports = async (config = {}) => {
plugins.push({
resolve: slash(process.cwd()),
name: `default-site-plugin`,
version: `n/a`,
version: createFileContentHash(process.cwd(), `gatsby-*`),
pluginOptions: {
plugins: [],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { store } = require(`../../redux/`)
const { boundActionCreators } = require(`../../redux/actions`)
const queryCompiler = require(`./query-compiler`).default
const queryRunner = require(`./query-runner`)
const invariant = require(`invariant`)

exports.extractQueries = () => {
const pages = store.getState().pages
Expand Down Expand Up @@ -57,6 +58,11 @@ exports.watch = rootDir => {
queryCompiler().then(queries => {
const pages = store.getState().pageComponents
queries.forEach(({ text }, path) => {
invariant(
pages[path],
`Path ${path} not found in the store pages: ${JSON.stringify(pages)}`
)

if (text !== pages[path].query) {
boundActionCreators.replacePageComponentQuery({
query: text,
Expand Down
19 changes: 10 additions & 9 deletions packages/gatsby/src/schema/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@ const { GraphQLSchema, GraphQLObjectType } = require(`graphql`)
const buildNodeTypes = require(`./build-node-types`)
const buildNodeConnections = require(`./build-node-connections`)
const { store } = require(`../redux`)
const invariant = require(`invariant`)

module.exports = async () => {
console.time(`building schema`)

const typesGQL = await buildNodeTypes()
const connections = buildNodeConnections(_.values(typesGQL))

// Pull off just the graphql node from each type object.
const nodes = _.mapValues(typesGQL, `node`)

invariant(!_.isEmpty(nodes), `There are no available GQL nodes`)
invariant(!_.isEmpty(connections), `There are no available GQL connections`)

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: `RootQueryType`,
fields: () => {
return {
// Pull off just the graphql node from each type object.
..._.mapValues(typesGQL, `node`),
...connections,
}
},
fields: { ...nodes, ...connections },
}),
})

Expand All @@ -28,6 +31,4 @@ module.exports = async () => {
type: `SET_SCHEMA`,
payload: schema,
})

return
}