From 19466a262cd10866ecdf8af88f01978f98d15750 Mon Sep 17 00:00:00 2001 From: stefanprobst Date: Thu, 6 Jun 2019 19:05:02 +0200 Subject: [PATCH] refactor(gatsby): restrict actions available in Node APIs (#14283) --- .../src/gatsby-node.js | 1 + .../src/redux/actions/__tests__/restricted.js | 41 ++ .../src/redux/actions/add-page-dependency.js | 2 +- packages/gatsby/src/redux/actions/index.js | 25 ++ packages/gatsby/src/redux/actions/internal.js | 210 +++++++++ .../redux/{actions.js => actions/public.js} | 424 +----------------- .../gatsby/src/redux/actions/restricted.js | 271 +++++++++++ packages/gatsby/src/redux/actions/types.js | 6 + packages/gatsby/src/utils/api-runner-node.js | 15 +- www/gatsby-node.js | 28 ++ www/src/components/api-reference/doc-block.js | 26 ++ www/src/pages/docs/actions.js | 51 ++- 12 files changed, 666 insertions(+), 434 deletions(-) create mode 100644 packages/gatsby/src/redux/actions/__tests__/restricted.js create mode 100644 packages/gatsby/src/redux/actions/index.js create mode 100644 packages/gatsby/src/redux/actions/internal.js rename packages/gatsby/src/redux/{actions.js => actions/public.js} (76%) create mode 100644 packages/gatsby/src/redux/actions/restricted.js create mode 100644 packages/gatsby/src/redux/actions/types.js diff --git a/packages/gatsby-transformer-documentationjs/src/gatsby-node.js b/packages/gatsby-transformer-documentationjs/src/gatsby-node.js index 4af2a2f1f32bf..4ab007320b8e7 100644 --- a/packages/gatsby-transformer-documentationjs/src/gatsby-node.js +++ b/packages/gatsby-transformer-documentationjs/src/gatsby-node.js @@ -300,6 +300,7 @@ exports.onCreateNode = async ({ node, actions, ...helpers }) => { `since`, `lends`, `examples`, + `tags`, ]) picked.optional = false diff --git a/packages/gatsby/src/redux/actions/__tests__/restricted.js b/packages/gatsby/src/redux/actions/__tests__/restricted.js new file mode 100644 index 0000000000000..b4be6ebb6dc19 --- /dev/null +++ b/packages/gatsby/src/redux/actions/__tests__/restricted.js @@ -0,0 +1,41 @@ +const { availableActionsByAPI } = require(`../restricted`) + +const report = require(`gatsby-cli/lib/reporter`) +report.warn = jest.fn() +report.error = jest.fn() +afterEach(() => { + report.warn.mockClear() + report.error.mockClear() +}) + +const dispatchWithThunk = actionOrThunk => + typeof actionOrThunk === `function` ? actionOrThunk() : actionOrThunk + +describe(`Restricted actions`, () => { + it(`handles actions allowed in API`, () => { + const action = dispatchWithThunk( + availableActionsByAPI.sourceNodes.createTypes() + ) + expect(action).toEqual({ type: `CREATE_TYPES` }) + expect(report.warn).not.toHaveBeenCalled() + expect(report.error).not.toHaveBeenCalled() + }) + + it(`handles actions deprecated in API`, () => { + const action = dispatchWithThunk( + availableActionsByAPI.onPreBootstrap.createTypes() + ) + expect(action).toEqual({ type: `CREATE_TYPES` }) + expect(report.warn).toHaveBeenCalled() + expect(report.error).not.toHaveBeenCalled() + }) + + it(`handles actions forbidden in API`, () => { + const action = dispatchWithThunk( + availableActionsByAPI.onPostBootstrap.createTypes() + ) + expect(action).toBeUndefined() + expect(report.warn).not.toHaveBeenCalled() + expect(report.error).toHaveBeenCalled() + }) +}) diff --git a/packages/gatsby/src/redux/actions/add-page-dependency.js b/packages/gatsby/src/redux/actions/add-page-dependency.js index 89d72454472d8..2019e96b5cf39 100644 --- a/packages/gatsby/src/redux/actions/add-page-dependency.js +++ b/packages/gatsby/src/redux/actions/add-page-dependency.js @@ -1,7 +1,7 @@ const _ = require(`lodash`) const { store } = require(`../`) -const { actions } = require(`../actions.js`) +const { actions } = require(`./internal.js`) function createPageDependency({ path, nodeId, connection }) { const state = store.getState() diff --git a/packages/gatsby/src/redux/actions/index.js b/packages/gatsby/src/redux/actions/index.js new file mode 100644 index 0000000000000..80a05797e9c7f --- /dev/null +++ b/packages/gatsby/src/redux/actions/index.js @@ -0,0 +1,25 @@ +const { bindActionCreators } = require(`redux`) +const { store } = require(`..`) + +const { actions: internalActions } = require(`./internal`) +const { actions: publicActions } = require(`./public`) +const { + actions: restrictedActions, + availableActionsByAPI, +} = require(`./restricted`) + +exports.internalActions = internalActions +exports.publicActions = publicActions +exports.restrictedActions = restrictedActions +exports.restrictedActionsAvailableInAPI = availableActionsByAPI + +const actions = { + ...internalActions, + ...publicActions, + ...restrictedActions, +} + +exports.actions = actions + +// Deprecated, remove in v3 +exports.boundActionCreators = bindActionCreators(actions, store.dispatch) diff --git a/packages/gatsby/src/redux/actions/internal.js b/packages/gatsby/src/redux/actions/internal.js new file mode 100644 index 0000000000000..8d8e77f61bce6 --- /dev/null +++ b/packages/gatsby/src/redux/actions/internal.js @@ -0,0 +1,210 @@ +// @flow +import type { Plugin } from "./types" + +const actions = {} + +/** + * Create a dependency between a page and data. Probably for + * internal use only. + * @param {Object} $0 + * @param {string} $0.path the path to the page + * @param {string} $0.nodeId A node ID + * @param {string} $0.connection A connection type + * @private + */ +actions.createPageDependency = ( + { + path, + nodeId, + connection, + }: { path: string, nodeId: string, connection: string }, + plugin: string = `` +) => { + return { + type: `CREATE_COMPONENT_DEPENDENCY`, + plugin, + payload: { + path, + nodeId, + connection, + }, + } +} + +/** + * Delete dependencies between an array of pages and data. Probably for + * internal use only. Used when deleting pages. + * @param {Array} paths the paths to delete. + * @private + */ +actions.deleteComponentsDependencies = (paths: string[]) => { + return { + type: `DELETE_COMPONENTS_DEPENDENCIES`, + payload: { + paths, + }, + } +} + +/** + * When the query watcher extracts a GraphQL query, it calls + * this to store the query with its component. + * @private + */ +actions.replaceComponentQuery = ({ + query, + componentPath, +}: { + query: string, + componentPath: string, +}) => { + return { + type: `REPLACE_COMPONENT_QUERY`, + payload: { + query, + componentPath, + }, + } +} + +/** + * When the query watcher extracts a "static" GraphQL query from + * components, it calls this to store the query with its component. + * @private + */ +actions.replaceStaticQuery = (args: any, plugin?: ?Plugin = null) => { + return { + type: `REPLACE_STATIC_QUERY`, + plugin, + payload: args, + } +} + +/** + * + * Report that a query has been extracted from a component. Used by + * query-compilier.js. + * + * @param {Object} $0 + * @param {componentPath} $0.componentPath The path to the component that just had + * its query read. + * @param {query} $0.query The GraphQL query that was extracted from the component. + * @private + */ +actions.queryExtracted = ( + { componentPath, query }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `QUERY_EXTRACTED`, + plugin, + traceId, + payload: { componentPath, query }, + } +} + +/** + * + * Report that the Relay Compilier found a graphql error when attempting to extract a query + * + * @param {Object} $0 + * @param {componentPath} $0.componentPath The path to the component that just had + * its query read. + * @param {error} $0.error The GraphQL query that was extracted from the component. + * @private + */ +actions.queryExtractionGraphQLError = ( + { componentPath, error }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `QUERY_EXTRACTION_GRAPHQL_ERROR`, + plugin, + traceId, + payload: { componentPath, error }, + } +} + +/** + * + * Report that babel was able to extract the graphql query. + * Indicates that the file is free of JS errors. + * + * @param {Object} $0 + * @param {componentPath} $0.componentPath The path to the component that just had + * its query read. + * @private + */ +actions.queryExtractedBabelSuccess = ( + { componentPath }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `QUERY_EXTRACTION_BABEL_SUCCESS`, + plugin, + traceId, + payload: { componentPath }, + } +} + +/** + * + * Report that the Relay Compilier found a babel error when attempting to extract a query + * + * @param {Object} $0 + * @param {componentPath} $0.componentPath The path to the component that just had + * its query read. + * @param {error} $0.error The Babel error object + * @private + */ +actions.queryExtractionBabelError = ( + { componentPath, error }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `QUERY_EXTRACTION_BABEL_ERROR`, + plugin, + traceId, + payload: { componentPath, error }, + } +} + +/** + * Set overall program status e.g. `BOOTSTRAPING` or `BOOTSTRAP_FINISHED`. + * + * @param {string} Program status + * @private + */ +actions.setProgramStatus = (status, plugin: Plugin, traceId?: string) => { + return { + type: `SET_PROGRAM_STATUS`, + plugin, + traceId, + payload: status, + } +} + +/** + * Broadcast that a page's query was run. + * + * @param {string} Path to the page component that changed. + * @private + */ +actions.pageQueryRun = ( + { path, componentPath, isPage }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `PAGE_QUERY_RUN`, + plugin, + traceId, + payload: { path, componentPath, isPage }, + } +} + +module.exports = { actions } diff --git a/packages/gatsby/src/redux/actions.js b/packages/gatsby/src/redux/actions/public.js similarity index 76% rename from packages/gatsby/src/redux/actions.js rename to packages/gatsby/src/redux/actions/public.js index 6af1e3d32665e..094903ff2ed64 100644 --- a/packages/gatsby/src/redux/actions.js +++ b/packages/gatsby/src/redux/actions/public.js @@ -2,7 +2,6 @@ const Joi = require(`joi`) const chalk = require(`chalk`) const _ = require(`lodash`) -const { bindActionCreators } = require(`redux`) const { stripIndent } = require(`common-tags`) const report = require(`gatsby-cli/lib/reporter`) const path = require(`path`) @@ -11,13 +10,13 @@ const truePath = require(`true-case-path`) const url = require(`url`) const kebabHash = require(`kebab-hash`) const slash = require(`slash`) -const { hasNodeChanged, getNode } = require(`../db/nodes`) -const { trackInlineObjectsInRootNode } = require(`../db/node-tracking`) -const { store } = require(`./index`) +const { hasNodeChanged, getNode } = require(`../../db/nodes`) +const { trackInlineObjectsInRootNode } = require(`../../db/node-tracking`) +const { store } = require(`..`) const fileExistsSync = require(`fs-exists-cached`).sync -const joiSchemas = require(`../joi-schemas/joi`) -const { generateComponentChunkName } = require(`../utils/js-chunk-names`) -const apiRunnerNode = require(`../utils/api-runner-node`) +const joiSchemas = require(`../../joi-schemas/joi`) +const { generateComponentChunkName } = require(`../../utils/js-chunk-names`) +const apiRunnerNode = require(`../../utils/api-runner-node`) const actions = {} @@ -36,6 +35,8 @@ const findChildrenRecursively = (children = []) => { return children } +import type { Plugin } from "./types" + type Job = { id: string, } @@ -56,10 +57,6 @@ type Page = { updatedAt: number, } -type Plugin = { - name: string, -} - type ActionOptions = { traceId: ?string, parentSpan: ?Object, @@ -831,9 +828,7 @@ actions.createNodeField = ( node.fields = {} } - /** - * Normalized name of the field that will be used in schema - */ + // Normalized name of the field that will be used in schema const schemaFieldName = _.includes(name, `___NODE`) ? name.split(`___`)[0] : name @@ -892,83 +887,6 @@ actions.createParentChildLink = ( } } -/** - * Create a dependency between a page and data. Probably for - * internal use only. - * @param {Object} $0 - * @param {string} $0.path the path to the page - * @param {string} $0.nodeId A node ID - * @param {string} $0.connection A connection type - * @private - */ -actions.createPageDependency = ( - { - path, - nodeId, - connection, - }: { path: string, nodeId: string, connection: string }, - plugin: string = `` -) => { - return { - type: `CREATE_COMPONENT_DEPENDENCY`, - plugin, - payload: { - path, - nodeId, - connection, - }, - } -} - -/** - * Delete dependencies between an array of pages and data. Probably for - * internal use only. Used when deleting pages. - * @param {Array} paths the paths to delete. - * @private - */ -actions.deleteComponentsDependencies = (paths: string[]) => { - return { - type: `DELETE_COMPONENTS_DEPENDENCIES`, - payload: { - paths, - }, - } -} - -/** - * When the query watcher extracts a GraphQL query, it calls - * this to store the query with its component. - * @private - */ -actions.replaceComponentQuery = ({ - query, - componentPath, -}: { - query: string, - componentPath: string, -}) => { - return { - type: `REPLACE_COMPONENT_QUERY`, - payload: { - query, - componentPath, - }, - } -} - -/** - * When the query watcher extracts a "static" GraphQL query from - * components, it calls this to store the query with its component. - * @private - */ -actions.replaceStaticQuery = (args: any, plugin?: ?Plugin = null) => { - return { - type: `REPLACE_STATIC_QUERY`, - plugin, - payload: args, - } -} - /** * Merge additional configuration into the current webpack config. A few * configurations options will be ignored if set, in order to try prevent accidental breakage. @@ -1188,9 +1106,7 @@ actions.setPluginStatus = ( } } -/** - * Check if path is absolute and add pathPrefix in front if it's not - */ +// Check if path is absolute and add pathPrefix in front if it's not const maybeAddPathPrefix = (path, pathPrefix) => { const parsed = url.parse(path) const isRelativeProtocol = path.startsWith(`//`) @@ -1242,322 +1158,4 @@ actions.createRedirect = ({ } } -/** - * Add a third-party schema to be merged into main schema. Schema has to be a - * graphql-js GraphQLSchema object. - * - * This schema is going to be merged as-is. This can easily break the main - * Gatsby schema, so it's user's responsibility to make sure it doesn't happen - * (by eg namespacing the schema). - * - * @param {Object} $0 - * @param {GraphQLSchema} $0.schema GraphQL schema to add - */ -actions.addThirdPartySchema = ( - { schema }: { schema: GraphQLSchema }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `ADD_THIRD_PARTY_SCHEMA`, - plugin, - traceId, - payload: schema, - } -} - -import type GatsbyGraphQLType from "../schema/types/type-builders" -/** - * Add type definitions to the GraphQL schema. - * - * @param {string | GraphQLOutputType | GatsbyGraphQLType | string[] | GraphQLOutputType[] | GatsbyGraphQLType[]} types Type definitions - * - * Type definitions can be provided either as - * [`graphql-js` types](https://graphql.org/graphql-js/), in - * [GraphQL schema definition language (SDL)](https://graphql.org/learn/) - * or using Gatsby Type Builders available on the `schema` API argument. - * - * Things to note: - * * needs to be called *before* schema generation. It is recommended to use - * `createTypes` in the `sourceNodes` API. - * * type definitions targeting node types, i.e. `MarkdownRemark` and others - * added in `sourceNodes` or `onCreateNode` APIs, need to implement the - * `Node` interface. Interface fields will be added automatically, but it - * is mandatory to label those types with `implements Node`. - * * by default, explicit type definitions from `createTypes` will be merged - * with inferred field types, and default field resolvers for `Date` (which - * adds formatting options) and `File` (which resolves the field value as - * a `relativePath` foreign-key field) are added. This behavior can be - * customised with `@infer`, `@dontInfer` directives or extensions. Fields - * may be assigned resolver (and other option like args) with additional - * directives. Currently `@dateformat`, `@link` and `@fileByRelativePath` are - * available. - * - * - * Schema customization controls: - * * `@infer` - run inference on the type and add fields that don't exist on the - * defined type to it. - * * `@dontInfer` - don't run any inference on the type - * - * Extensions to add resolver options: - * * `@dateformat` - add date formatting arguments. Accepts `formatString` and - * `locale` options that sets the defaults for this field - * * `@link` - connect to a different Node. Arguments `by` and `from`, which - * define which field to compare to on a remote node and which field to use on - * the source node - * * `@fileByRelativePath` - connect to a File node. Same arguments. The - * difference from link is that this normalizes the relative path to be - * relative from the path where source node is found. - * * `proxy` - in case the underlying node data contains field names with - * characters that are invalid in GraphQL, `proxy` allows to explicitly - * proxy those properties to fields with valid field names. Takes a `from` arg. - * - * - * @example - * exports.sourceNodes = ({ actions }) => { - * const { createTypes } = actions - * const typeDefs = ` - * """ - * Markdown Node - * """ - * type MarkdownRemark implements Node @infer { - * frontmatter: Frontmatter! - * } - * - * """ - * Markdown Frontmatter - * """ - * type Frontmatter @infer { - * title: String! - * author: AuthorJson! @link - * date: Date! @dateformat - * published: Boolean! - * tags: [String!]! - * } - * - * """ - * Author information - * """ - * # Does not include automatically inferred fields - * type AuthorJson implements Node @dontInfer { - * name: String! - * birthday: Date! @dateformat(locale: "ru") - * } - * ` - * createTypes(typeDefs) - * } - * - * // using Gatsby Type Builder API - * exports.sourceNodes = ({ actions, schema }) => { - * const { createTypes } = actions - * const typeDefs = [ - * schema.buildObjectType({ - * name: 'MarkdownRemark', - * fields: { - * frontmatter: 'Frontmatter!' - * }, - * interfaces: ['Node'], - * extensions: { - * infer: true, - * }, - * }), - * schema.buildObjectType({ - * name: 'Frontmatter', - * fields: { - * title: { - * type: 'String!', - * resolve(parent) { - * return parent.title || '(Untitled)' - * } - * }, - * author: { - * type: 'AuthorJson' - * extensions: { - * link: {}, - * }, - * } - * date: { - * type: 'Date!' - * extensions: { - * dateformat: {}, - * }, - * }, - * published: 'Boolean!', - * tags: '[String!]!', - * } - * }), - * schema.buildObjectType({ - * name: 'AuthorJson', - * fields: { - * name: 'String!' - * birthday: { - * type: 'Date!' - * extensions: { - * dateformat: { - * locale: 'ru', - * }, - * }, - * }, - * }, - * interfaces: ['Node'], - * extensions: { - * infer: false, - * }, - * }), - * ] - * createTypes(typeDefs) - * } - */ -actions.createTypes = ( - types: - | string - | GraphQLOutputType - | GatsbyGraphQLType - | Array, - plugin: Plugin, - traceId?: string -) => { - return { - type: `CREATE_TYPES`, - plugin, - traceId, - payload: types, - } -} - -/** - * - * Report that a query has been extracted from a component. Used by - * query-compilier.js. - * - * @param {Object} $0 - * @param {componentPath} $0.componentPath The path to the component that just had - * its query read. - * @param {query} $0.query The GraphQL query that was extracted from the component. - * @private - */ -actions.queryExtracted = ( - { componentPath, query }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `QUERY_EXTRACTED`, - plugin, - traceId, - payload: { componentPath, query }, - } -} - -/** - * - * Report that the Relay Compilier found a graphql error when attempting to extract a query - * - * @param {Object} $0 - * @param {componentPath} $0.componentPath The path to the component that just had - * its query read. - * @param {error} $0.error The GraphQL query that was extracted from the component. - * @private - */ -actions.queryExtractionGraphQLError = ( - { componentPath, error }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `QUERY_EXTRACTION_GRAPHQL_ERROR`, - plugin, - traceId, - payload: { componentPath, error }, - } -} - -/** - * - * Report that babel was able to extract the graphql query. - * Indicates that the file is free of JS errors. - * - * @param {Object} $0 - * @param {componentPath} $0.componentPath The path to the component that just had - * its query read. - * @private - */ -actions.queryExtractedBabelSuccess = ( - { componentPath }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `QUERY_EXTRACTION_BABEL_SUCCESS`, - plugin, - traceId, - payload: { componentPath }, - } -} - -/** - * - * Report that the Relay Compilier found a babel error when attempting to extract a query - * - * @param {Object} $0 - * @param {componentPath} $0.componentPath The path to the component that just had - * its query read. - * @param {error} $0.error The Babel error object - * @private - */ -actions.queryExtractionBabelError = ( - { componentPath, error }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `QUERY_EXTRACTION_BABEL_ERROR`, - plugin, - traceId, - payload: { componentPath, error }, - } -} - -/** - * Set overall program status e.g. `BOOTSTRAPING` or `BOOTSTRAP_FINISHED`. - * - * @param {string} Program status - * @private - */ -actions.setProgramStatus = (status, plugin: Plugin, traceId?: string) => { - return { - type: `SET_PROGRAM_STATUS`, - plugin, - traceId, - payload: status, - } -} - -/** - * Broadcast that a page's query was run. - * - * @param {string} Path to the page component that changed. - * @private - */ -actions.pageQueryRun = ( - { path, componentPath, isPage }, - plugin: Plugin, - traceId?: string -) => { - return { - type: `PAGE_QUERY_RUN`, - plugin, - traceId, - payload: { path, componentPath, isPage }, - } -} - -/** - * All action creators wrapped with a dispatch. - */ -exports.actions = actions - -/** - * All action creators wrapped with a dispatch. - *DEPRECATED* - */ -exports.boundActionCreators = bindActionCreators(actions, store.dispatch) +module.exports = { actions } diff --git a/packages/gatsby/src/redux/actions/restricted.js b/packages/gatsby/src/redux/actions/restricted.js new file mode 100644 index 0000000000000..0911551d34808 --- /dev/null +++ b/packages/gatsby/src/redux/actions/restricted.js @@ -0,0 +1,271 @@ +// @flow +const report = require(`gatsby-cli/lib/reporter`) + +import type { Plugin } from "./types" + +const actions = {} + +/** + * Add a third-party schema to be merged into main schema. Schema has to be a + * graphql-js GraphQLSchema object. + * + * This schema is going to be merged as-is. This can easily break the main + * Gatsby schema, so it's user's responsibility to make sure it doesn't happen + * (by eg namespacing the schema). + * + * @availableIn [sourceNodes] + * + * @param {Object} $0 + * @param {GraphQLSchema} $0.schema GraphQL schema to add + */ +actions.addThirdPartySchema = ( + { schema }: { schema: GraphQLSchema }, + plugin: Plugin, + traceId?: string +) => { + return { + type: `ADD_THIRD_PARTY_SCHEMA`, + plugin, + traceId, + payload: schema, + } +} + +import type GatsbyGraphQLType from "../../schema/types/type-builders" +/** + * Add type definitions to the GraphQL schema. + * + * @availableIn [sourceNodes] + * + * @param {string | GraphQLOutputType | GatsbyGraphQLType | string[] | GraphQLOutputType[] | GatsbyGraphQLType[]} types Type definitions + * + * Type definitions can be provided either as + * [`graphql-js` types](https://graphql.org/graphql-js/), in + * [GraphQL schema definition language (SDL)](https://graphql.org/learn/) + * or using Gatsby Type Builders available on the `schema` API argument. + * + * Things to note: + * * type definitions targeting node types, i.e. `MarkdownRemark` and others + * added in `sourceNodes` or `onCreateNode` APIs, need to implement the + * `Node` interface. Interface fields will be added automatically, but it + * is mandatory to label those types with `implements Node`. + * * by default, explicit type definitions from `createTypes` will be merged + * with inferred field types, and default field resolvers for `Date` (which + * adds formatting options) and `File` (which resolves the field value as + * a `relativePath` foreign-key field) are added. This behavior can be + * customised with `@infer`, `@dontInfer` directives or extensions. Fields + * may be assigned resolver (and other option like args) with additional + * directives. Currently `@dateformat`, `@link` and `@fileByRelativePath` are + * available. + * + * + * Schema customization controls: + * * `@infer` - run inference on the type and add fields that don't exist on the + * defined type to it. + * * `@dontInfer` - don't run any inference on the type + * + * Extensions to add resolver options: + * * `@dateformat` - add date formatting arguments. Accepts `formatString` and + * `locale` options that sets the defaults for this field + * * `@link` - connect to a different Node. Arguments `by` and `from`, which + * define which field to compare to on a remote node and which field to use on + * the source node + * * `@fileByRelativePath` - connect to a File node. Same arguments. The + * difference from link is that this normalizes the relative path to be + * relative from the path where source node is found. + * * `proxy` - in case the underlying node data contains field names with + * characters that are invalid in GraphQL, `proxy` allows to explicitly + * proxy those properties to fields with valid field names. Takes a `from` arg. + * + * + * @example + * exports.sourceNodes = ({ actions }) => { + * const { createTypes } = actions + * const typeDefs = ` + * """ + * Markdown Node + * """ + * type MarkdownRemark implements Node @infer { + * frontmatter: Frontmatter! + * } + * + * """ + * Markdown Frontmatter + * """ + * type Frontmatter @infer { + * title: String! + * author: AuthorJson! @link + * date: Date! @dateformat + * published: Boolean! + * tags: [String!]! + * } + * + * """ + * Author information + * """ + * # Does not include automatically inferred fields + * type AuthorJson implements Node @dontInfer { + * name: String! + * birthday: Date! @dateformat(locale: "ru") + * } + * ` + * createTypes(typeDefs) + * } + * + * // using Gatsby Type Builder API + * exports.sourceNodes = ({ actions, schema }) => { + * const { createTypes } = actions + * const typeDefs = [ + * schema.buildObjectType({ + * name: 'MarkdownRemark', + * fields: { + * frontmatter: 'Frontmatter!' + * }, + * interfaces: ['Node'], + * extensions: { + * infer: true, + * }, + * }), + * schema.buildObjectType({ + * name: 'Frontmatter', + * fields: { + * title: { + * type: 'String!', + * resolve(parent) { + * return parent.title || '(Untitled)' + * } + * }, + * author: { + * type: 'AuthorJson' + * extensions: { + * link: {}, + * }, + * } + * date: { + * type: 'Date!' + * extensions: { + * dateformat: {}, + * }, + * }, + * published: 'Boolean!', + * tags: '[String!]!', + * } + * }), + * schema.buildObjectType({ + * name: 'AuthorJson', + * fields: { + * name: 'String!' + * birthday: { + * type: 'Date!' + * extensions: { + * dateformat: { + * locale: 'ru', + * }, + * }, + * }, + * }, + * interfaces: ['Node'], + * extensions: { + * infer: false, + * }, + * }), + * ] + * createTypes(typeDefs) + * } + */ +actions.createTypes = ( + types: + | string + | GraphQLOutputType + | GatsbyGraphQLType + | Array, + plugin: Plugin, + traceId?: string +) => { + return { + type: `CREATE_TYPES`, + plugin, + traceId, + payload: types, + } +} + +const withDeprecationWarning = (actionName, action, api, allowedIn) => ( + ...args +) => { + report.warn( + `Calling \`${actionName}\` in the \`${api}\` API is deprecated. ` + + `Please use: ${allowedIn.map(a => `\`${a}\``).join(`, `)}.` + ) + return action(...args) +} + +const withErrorMessage = (actionName, api, allowedIn) => () => + // return a thunk that does not dispatch anything + () => { + report.error( + `\`${actionName}\` is not available in the \`${api}\` API. ` + + `Please use: ${allowedIn.map(a => `\`${a}\``).join(`, `)}.` + ) + } + +const nodeAPIs = Object.keys(require(`../../utils/api-node-docs`)) + +const ALLOWED_IN = `ALLOWED_IN` +const DEPRECATED_IN = `DEPRECATED_IN` + +const set = (availableActionsByAPI, api, actionName, action) => { + availableActionsByAPI[api] = availableActionsByAPI[api] || {} + availableActionsByAPI[api][actionName] = action +} + +const mapAvailableActionsToAPIs = restrictions => { + const availableActionsByAPI = {} + + const actionNames = Object.keys(restrictions) + actionNames.forEach(actionName => { + const action = actions[actionName] + + const allowedIn = restrictions[actionName][ALLOWED_IN] + allowedIn.forEach(api => + set(availableActionsByAPI, api, actionName, action) + ) + + const deprecatedIn = restrictions[actionName][DEPRECATED_IN] + deprecatedIn.forEach(api => + set( + availableActionsByAPI, + api, + actionName, + withDeprecationWarning(actionName, action, api, allowedIn) + ) + ) + + const forbiddenIn = nodeAPIs.filter( + api => ![...allowedIn, ...deprecatedIn].includes(api) + ) + forbiddenIn.forEach(api => + set( + availableActionsByAPI, + api, + actionName, + withErrorMessage(actionName, api, allowedIn) + ) + ) + }) + + return availableActionsByAPI +} + +const availableActionsByAPI = mapAvailableActionsToAPIs({ + createTypes: { + [ALLOWED_IN]: [`sourceNodes`], + [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], + }, + addThirdPartySchema: { + [ALLOWED_IN]: [`sourceNodes`], + [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], + }, +}) + +module.exports = { actions, availableActionsByAPI } diff --git a/packages/gatsby/src/redux/actions/types.js b/packages/gatsby/src/redux/actions/types.js new file mode 100644 index 0000000000000..5980f1797479d --- /dev/null +++ b/packages/gatsby/src/redux/actions/types.js @@ -0,0 +1,6 @@ +// @flow +type Plugin = { + name: string, +} + +export type { Plugin } diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js index f5b7dc0154dd8..3fd09b2cfbf4f 100644 --- a/packages/gatsby/src/utils/api-runner-node.js +++ b/packages/gatsby/src/utils/api-runner-node.js @@ -1,6 +1,7 @@ const Promise = require(`bluebird`) const _ = require(`lodash`) const chalk = require(`chalk`) +const { bindActionCreators } = require(`redux`) const tracer = require(`opentracing`).globalTracer() const reporter = require(`gatsby-cli/lib/reporter`) @@ -85,8 +86,18 @@ const runAPI = (plugin, api, args) => { hasNodeChanged, getNodeAndSavePathDependency, } = require(`../db/nodes`) - const { boundActionCreators } = require(`../redux/actions`) - + const { + publicActions, + restrictedActionsAvailableInAPI, + } = require(`../redux/actions`) + const availableActions = { + ...publicActions, + ...(restrictedActionsAvailableInAPI[api] || {}), + } + const boundActionCreators = bindActionCreators( + availableActions, + store.dispatch + ) const doubleBoundActionCreators = doubleBind( boundActionCreators, api, diff --git a/www/gatsby-node.js b/www/gatsby-node.js index 2be84c57e097c..9f9bf2d39cc1c 100644 --- a/www/gatsby-node.js +++ b/www/gatsby-node.js @@ -961,3 +961,31 @@ exports.onCreateWebpackConfig = ({ actions, plugins }) => { ], }) } + +// Patch `DocumentationJs` type to handle custom `@availableIn` jsdoc tag +exports.createResolvers = ({ createResolvers }) => { + createResolvers({ + DocumentationJs: { + availableIn: { + type: `[String]`, + resolve(source) { + const { tags } = source + if (!tags || !tags.length) { + return [] + } + + const availableIn = tags.find(tag => tag.title === `availableIn`) + if (availableIn) { + return availableIn.description + .split(`\n`)[0] + .replace(/[[\]]/g, ``) + .split(`,`) + .map(api => api.trim()) + } + + return [] + }, + }, + }, + }) +} diff --git a/www/src/components/api-reference/doc-block.js b/www/src/components/api-reference/doc-block.js index 03be781415d27..a61fee27e974d 100644 --- a/www/src/components/api-reference/doc-block.js +++ b/www/src/components/api-reference/doc-block.js @@ -127,6 +127,31 @@ const APILink = ({ definition, githubPath }) => { return null } +const AvailableIn = ({ definition }) => { + if (definition.availableIn && definition.availableIn.length) { + return ( +
+ Only available in: + {definition.availableIn.map(api => ( + + {api} + + ))} +
+ ) + } + + return null +} + const Description = ({ definition }) => { if (definition.description) { return ( @@ -208,6 +233,7 @@ const DocBlock = ({ {definition.optional && } {showSignature && !showSignatureNextToTitle && signatureElement} + func.name - ).filter(func => func.name !== `deleteNodes`) + const docs = this.props.data.allFile.nodes.reduce((acc, node) => { + const doc = node.childrenDocumentationJs.map(def => { + def.codeLocation.file = node.relativePath + return def + }) + return acc.concat(doc) + }, []) - const githubPath = `https://github.com/gatsbyjs/gatsby/blob/${ - process.env.COMMIT_SHA - }/packages/${this.props.data.file.relativePath}` + const funcs = sortBy(docs, func => func.name).filter( + func => func.name !== `deleteNodes` + ) return ( @@ -69,7 +72,7 @@ exports.