diff --git a/packages/gatsby/src/redux/actions/internal.ts b/packages/gatsby/src/redux/actions/internal.ts index 19c811ce50087..04a33541a1e1f 100644 --- a/packages/gatsby/src/redux/actions/internal.ts +++ b/packages/gatsby/src/redux/actions/internal.ts @@ -1,4 +1,5 @@ import { + IGatsbyPlugin, ProgramStatus, ICreatePageDependencyAction, IDeleteComponentDependenciesAction, @@ -81,7 +82,7 @@ export const replaceComponentQuery = ({ */ export const replaceStaticQuery = ( args: any, - plugin: Plugin | null | undefined = null + plugin: IGatsbyPlugin | null | undefined = null ): IReplaceStaticQueryAction => { return { type: `REPLACE_STATIC_QUERY`, @@ -98,7 +99,7 @@ export const replaceStaticQuery = ( */ export const queryExtracted = ( { componentPath, query }: { componentPath: string; query: string }, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IQueryExtractedAction => { return { @@ -116,7 +117,7 @@ export const queryExtracted = ( */ export const queryExtractionGraphQLError = ( { componentPath, error }: { componentPath: string; error: string }, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IQueryExtractionGraphQLErrorAction => { return { @@ -135,7 +136,7 @@ export const queryExtractionGraphQLError = ( */ export const queryExtractedBabelSuccess = ( { componentPath }, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IQueryExtractedBabelSuccessAction => { return { @@ -153,7 +154,7 @@ export const queryExtractedBabelSuccess = ( */ export const queryExtractionBabelError = ( { componentPath, error }: { componentPath: string; error: Error }, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IQueryExtractionBabelErrorAction => { return { @@ -170,7 +171,7 @@ export const queryExtractionBabelError = ( */ export const setProgramStatus = ( status: ProgramStatus, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): ISetProgramStatusAction => { return { @@ -187,7 +188,7 @@ export const setProgramStatus = ( */ export const pageQueryRun = ( { path, componentPath, isPage }, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IPageQueryRunAction => { return { @@ -204,7 +205,7 @@ export const pageQueryRun = ( */ export const removeStaleJob = ( contentDigest: string, - plugin: Plugin, + plugin: IGatsbyPlugin, traceId?: string ): IRemoveStaleJobAction => { return { diff --git a/packages/gatsby/src/redux/actions/restricted.js b/packages/gatsby/src/redux/actions/restricted.js deleted file mode 100644 index 85be2a3a00b9a..0000000000000 --- a/packages/gatsby/src/redux/actions/restricted.js +++ /dev/null @@ -1,459 +0,0 @@ -// @flow -const { camelCase } = require(`lodash`) -const report = require(`gatsby-cli/lib/reporter`) -const { parseTypeDef } = require(`../../schema/types/type-defs`) - -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 e.g. namespacing the schema). - * - * @availableIn [createSchemaCustomization, 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 [createSchemaCustomization, 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`, `@fileByRelativePath` and - * `@proxy` 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.createSchemaCustomization = ({ 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.createSchemaCustomization = ({ 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: Array.isArray(types) - ? types.map(parseTypeDef) - : parseTypeDef(types), - } -} - -const { reservedExtensionNames } = require(`../../schema/extensions`) -import type GraphQLFieldExtensionDefinition from "../../schema/extensions" -/** - * Add a field extension to the GraphQL schema. - * - * Extensions allow defining custom behavior which can be added to fields - * via directive (in SDL) or on the `extensions` prop (with Type Builders). - * - * The extension definition takes a `name`, an `extend` function, and optional - * extension `args` for options. The `extend` function has to return a (partial) - * field config, and receives the extension options and the previous field config - * as arguments. - * - * @availableIn [createSchemaCustomization, sourceNodes] - * - * @param {GraphQLFieldExtensionDefinition} extension The field extension definition - * @example - * exports.createSchemaCustomization = ({ actions }) => { - * const { createFieldExtension } = actions - * createFieldExtension({ - * name: 'motivate', - * args: { - * caffeine: 'Int' - * }, - * extend(options, prevFieldConfig) { - * return { - * type: 'String', - * args: { - * sunshine: { - * type: 'Int', - * defaultValue: 0, - * }, - * }, - * resolve(source, args, context, info) { - * const motivation = (options.caffeine || 0) - args.sunshine - * if (motivation > 5) return 'Work! Work! Work!' - * return 'Maybe tomorrow.' - * }, - * } - * }, - * }) - * } - */ -actions.createFieldExtension = ( - extension: GraphQLFieldExtensionDefinition, - plugin?: Plugin, - traceId?: string -) => (dispatch, getState) => { - const { name } = extension || {} - const { fieldExtensions } = getState().schemaCustomization - - if (!name) { - report.error(`The provided field extension must have a \`name\` property.`) - } else if (reservedExtensionNames.includes(name)) { - report.error( - `The field extension name \`${name}\` is reserved for internal use.` - ) - } else if (fieldExtensions[name]) { - report.error( - `A field extension with the name \`${name}\` has already been registered.` - ) - } else { - dispatch({ - type: `CREATE_FIELD_EXTENSION`, - plugin, - traceId, - payload: { name, extension }, - }) - } -} - -/** - * Write GraphQL schema to file - * - * Writes out inferred and explicitly specified type definitions. This is not - * the full GraphQL schema, but only the types necessary to recreate all type - * definitions, i.e. it does not include directives, built-ins, and derived - * types for filtering, sorting, pagination etc. Optionally, you can define a - * list of types to include/exclude. This is recommended to avoid including - * definitions for plugin-created types. - * - * @availableIn [createSchemaCustomization] - * - * @param {object} $0 - * @param {string} [$0.path] The path to the output file, defaults to `schema.gql` - * @param {object} [$0.include] Configure types to include - * @param {string[]} [$0.include.types] Only include these types - * @param {string[]} [$0.include.plugins] Only include types owned by these plugins - * @param {object} [$0.exclude] Configure types to exclude - * @param {string[]} [$0.exclude.types] Do not include these types - * @param {string[]} [$0.exclude.plugins] Do not include types owned by these plugins - * @param {boolean} [withFieldTypes] Include field types, defaults to `true` - */ -actions.printTypeDefinitions = ( - { - path = `schema.gql`, - include, - exclude, - withFieldTypes = true, - }: { - path?: string, - include?: { types?: Array, plugins?: Array }, - exclude?: { types?: Array, plugins?: Array }, - withFieldTypes?: boolean, - }, - plugin?: Plugin, - traceId?: string -) => { - return { - type: `PRINT_SCHEMA_REQUESTED`, - plugin, - traceId, - payload: { - path, - include, - exclude, - withFieldTypes, - }, - } -} - -/** - * Make functionality available on field resolver `context` - * - * @availableIn [createSchemaCustomization] - * - * @param {object} context Object to make available on `context`. - * When called from a plugin, the context value will be namespaced under - * the camel-cased plugin name without the "gatsby-" prefix - * @example - * const getHtml = md => remark().use(html).process(md) - * exports.createSchemaCustomization = ({ actions }) => { - * actions.createResolverContext({ getHtml }) - * } - * // The context value can then be accessed in any field resolver like this: - * exports.createSchemaCustomization = ({ actions }) => { - * actions.createTypes(schema.buildObjectType({ - * name: 'Test', - * interfaces: ['Node'], - * fields: { - * md: { - * type: 'String!', - * async resolve(source, args, context, info) { - * const processed = await context.transformerRemark.getHtml(source.internal.contents) - * return processed.contents - * } - * } - * } - * })) - * } - */ -actions.createResolverContext = ( - context: object, - plugin?: Plugin, - traceId?: string -) => dispatch => { - if (!context || typeof context !== `object`) { - report.error( - `Expected context value passed to \`createResolverContext\` to be an object. Received "${context}".` - ) - } else { - const { name } = plugin || {} - const payload = - !name || name === `default-site-plugin` - ? context - : { [camelCase(name.replace(/^gatsby-/, ``))]: context } - dispatch({ - type: `CREATE_RESOLVER_CONTEXT`, - plugin, - traceId, - payload, - }) - } -} - -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({ - createFieldExtension: { - [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], - }, - createTypes: { - [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], - [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], - }, - createResolverContext: { - [ALLOWED_IN]: [`createSchemaCustomization`], - }, - addThirdPartySchema: { - [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], - [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], - }, - printTypeDefinitions: { - [ALLOWED_IN]: [`createSchemaCustomization`], - }, -}) - -module.exports = { actions, availableActionsByAPI } diff --git a/packages/gatsby/src/redux/actions/restricted.ts b/packages/gatsby/src/redux/actions/restricted.ts new file mode 100644 index 0000000000000..8cb4a68b89f0b --- /dev/null +++ b/packages/gatsby/src/redux/actions/restricted.ts @@ -0,0 +1,517 @@ +import { camelCase } from "lodash" +import { GraphQLSchema, GraphQLOutputType } from "graphql" +import { ActionCreator } from "redux" +import { ThunkAction } from "redux-thunk" +import report from "gatsby-cli/lib/reporter" +import { parseTypeDef } from "../../schema/types/type-defs" +import { + GraphQLFieldExtensionDefinition, + reservedExtensionNames, +} from "../../schema/extensions" +import { GatsbyGraphQLType } from "../../schema/types/type-builders" +import { + IGatsbyPlugin, + ActionsUnion, + IAddThirdPartySchema, + ICreateTypes, + IGatsbyState, + ICreateFieldExtension, + IPrintTypeDefinitions, + ICreateResolverContext, + IGatsbyPluginContext, +} from "../types" + +type RestrictionActionNames = + | "createFieldExtension" + | "createTypes" + | "createResolverContext" + | "addThirdPartySchema" + | "printTypeDefinitions" + +type SomeActionCreator = + | ActionCreator + | ActionCreator> + +export 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 e.g. namespacing the schema). + * + * @availableIn [createSchemaCustomization, sourceNodes] + * + * @param {Object} $0 + * @param {GraphQLSchema} $0.schema GraphQL schema to add + */ + addThirdPartySchema: ( + { schema }: { schema: GraphQLSchema }, + plugin: IGatsbyPlugin, + traceId?: string + ): IAddThirdPartySchema => { + return { + type: `ADD_THIRD_PARTY_SCHEMA`, + plugin, + traceId, + payload: schema, + } + }, + + /** + * Add type definitions to the GraphQL schema. + * + * @availableIn [createSchemaCustomization, 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`, `@fileByRelativePath` and + * `@proxy` 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.createSchemaCustomization = ({ 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.createSchemaCustomization = ({ 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) + * } + */ + createTypes: ( + types: + | string + | GraphQLOutputType + | GatsbyGraphQLType + | Array, + plugin: IGatsbyPlugin, + traceId?: string + ): ICreateTypes => { + return { + type: `CREATE_TYPES`, + plugin, + traceId, + payload: Array.isArray(types) + ? types.map(parseTypeDef) + : parseTypeDef(types), + } + }, + + /** + * Add a field extension to the GraphQL schema. + * + * Extensions allow defining custom behavior which can be added to fields + * via directive (in SDL) or on the `extensions` prop (with Type Builders). + * + * The extension definition takes a `name`, an `extend` function, and optional + * extension `args` for options. The `extend` function has to return a (partial) + * field config, and receives the extension options and the previous field config + * as arguments. + * + * @availableIn [createSchemaCustomization, sourceNodes] + * + * @param {GraphQLFieldExtensionDefinition} extension The field extension definition + * @example + * exports.createSchemaCustomization = ({ actions }) => { + * const { createFieldExtension } = actions + * createFieldExtension({ + * name: 'motivate', + * args: { + * caffeine: 'Int' + * }, + * extend(options, prevFieldConfig) { + * return { + * type: 'String', + * args: { + * sunshine: { + * type: 'Int', + * defaultValue: 0, + * }, + * }, + * resolve(source, args, context, info) { + * const motivation = (options.caffeine || 0) - args.sunshine + * if (motivation > 5) return 'Work! Work! Work!' + * return 'Maybe tomorrow.' + * }, + * } + * }, + * }) + * } + */ + createFieldExtension: ( + extension: GraphQLFieldExtensionDefinition, + plugin: IGatsbyPlugin, + traceId?: string + ): ThunkAction => ( + dispatch, + getState + ): void => { + const { name } = extension || {} + const { fieldExtensions } = getState().schemaCustomization + + if (!name) { + report.error( + `The provided field extension must have a \`name\` property.` + ) + } else if (reservedExtensionNames.includes(name)) { + report.error( + `The field extension name \`${name}\` is reserved for internal use.` + ) + } else if (fieldExtensions[name]) { + report.error( + `A field extension with the name \`${name}\` has already been registered.` + ) + } else { + dispatch({ + type: `CREATE_FIELD_EXTENSION`, + plugin, + traceId, + payload: { name, extension }, + }) + } + }, + + /** + * Write GraphQL schema to file + * + * Writes out inferred and explicitly specified type definitions. This is not + * the full GraphQL schema, but only the types necessary to recreate all type + * definitions, i.e. it does not include directives, built-ins, and derived + * types for filtering, sorting, pagination etc. Optionally, you can define a + * list of types to include/exclude. This is recommended to avoid including + * definitions for plugin-created types. + * + * @availableIn [createSchemaCustomization] + * + * @param {object} $0 + * @param {string} [$0.path] The path to the output file, defaults to `schema.gql` + * @param {object} [$0.include] Configure types to include + * @param {string[]} [$0.include.types] Only include these types + * @param {string[]} [$0.include.plugins] Only include types owned by these plugins + * @param {object} [$0.exclude] Configure types to exclude + * @param {string[]} [$0.exclude.types] Do not include these types + * @param {string[]} [$0.exclude.plugins] Do not include types owned by these plugins + * @param {boolean} [withFieldTypes] Include field types, defaults to `true` + */ + printTypeDefinitions: ( + { + path = `schema.gql`, + include, + exclude, + withFieldTypes = true, + }: { + path?: string + include?: { types?: Array; plugins?: Array } + exclude?: { types?: Array; plugins?: Array } + withFieldTypes?: boolean + }, + plugin: IGatsbyPlugin, + traceId?: string + ): IPrintTypeDefinitions => { + return { + type: `PRINT_SCHEMA_REQUESTED`, + plugin, + traceId, + payload: { + path, + include, + exclude, + withFieldTypes, + }, + } + }, + + /** + * Make functionality available on field resolver `context` + * + * @availableIn [createSchemaCustomization] + * + * @param {object} context Object to make available on `context`. + * When called from a plugin, the context value will be namespaced under + * the camel-cased plugin name without the "gatsby-" prefix + * @example + * const getHtml = md => remark().use(html).process(md) + * exports.createSchemaCustomization = ({ actions }) => { + * actions.createResolverContext({ getHtml }) + * } + * // The context value can then be accessed in any field resolver like this: + * exports.createSchemaCustomization = ({ actions }) => { + * actions.createTypes(schema.buildObjectType({ + * name: 'Test', + * interfaces: ['Node'], + * fields: { + * md: { + * type: 'String!', + * async resolve(source, args, context, info) { + * const processed = await context.transformerRemark.getHtml(source.internal.contents) + * return processed.contents + * } + * } + * } + * })) + * } + */ + createResolverContext: ( + context: IGatsbyPluginContext, + plugin: IGatsbyPlugin, + traceId?: string + ): ThunkAction => ( + dispatch + ): void => { + if (!context || typeof context !== `object`) { + report.error( + `Expected context value passed to \`createResolverContext\` to be an object. Received "${context}".` + ) + } else { + const { name } = plugin || {} + const payload = + !name || name === `default-site-plugin` + ? context + : { [camelCase(name.replace(/^gatsby-/, ``))]: context } + dispatch({ + type: `CREATE_RESOLVER_CONTEXT`, + plugin, + traceId, + payload, + }) + } + }, +} + +const withDeprecationWarning = ( + actionName: RestrictionActionNames, + action: SomeActionCreator, + api: API, + allowedIn: API[] +): SomeActionCreator => (...args: any[]): ReturnType> => { + report.warn( + `Calling \`${actionName}\` in the \`${api}\` API is deprecated. ` + + `Please use: ${allowedIn.map(a => `\`${a}\``).join(`, `)}.` + ) + return action(...args) +} + +const withErrorMessage = ( + actionName: RestrictionActionNames, + api: API, + allowedIn: API[] +) => () => + // return a thunk that does not dispatch anything + (): void => { + 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` + +type API = string + +type Restrictions = Record< + RestrictionActionNames, + Partial<{ + ALLOWED_IN: API[] + DEPRECATED_IN: API[] + }> +> + +type AvailableActionsByAPI = Record< + API, + { [K in RestrictionActionNames]: SomeActionCreator } +> + +const set = ( + availableActionsByAPI: {}, + api: API, + actionName: RestrictionActionNames, + action: SomeActionCreator +): void => { + availableActionsByAPI[api] = availableActionsByAPI[api] || {} + availableActionsByAPI[api][actionName] = action +} + +const mapAvailableActionsToAPIs = ( + restrictions: Restrictions +): AvailableActionsByAPI => { + const availableActionsByAPI: AvailableActionsByAPI = {} + + const actionNames = Object.keys(restrictions) as (keyof typeof restrictions)[] + actionNames.forEach(actionName => { + const action = actions[actionName] + + const allowedIn: API[] = restrictions[actionName][ALLOWED_IN] || [] + allowedIn.forEach(api => + set(availableActionsByAPI, api, actionName, action) + ) + + const deprecatedIn: API[] = 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 +} + +export const availableActionsByAPI = mapAvailableActionsToAPIs({ + createFieldExtension: { + [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], + }, + createTypes: { + [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], + [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], + }, + createResolverContext: { + [ALLOWED_IN]: [`createSchemaCustomization`], + }, + addThirdPartySchema: { + [ALLOWED_IN]: [`sourceNodes`, `createSchemaCustomization`], + [DEPRECATED_IN]: [`onPreInit`, `onPreBootstrap`], + }, + printTypeDefinitions: { + [ALLOWED_IN]: [`createSchemaCustomization`], + }, +}) diff --git a/packages/gatsby/src/redux/actions/types.js b/packages/gatsby/src/redux/actions/types.js deleted file mode 100644 index 5980f1797479d..0000000000000 --- a/packages/gatsby/src/redux/actions/types.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow -type Plugin = { - name: string, -} - -export type { Plugin } diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index e814c1053494e..b871c5bd76f0b 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -1,5 +1,6 @@ import { IProgram } from "../commands/types" -import { GraphQLSchema } from "graphql" +import { GraphQLFieldExtensionDefinition } from "../schema/extensions" +import { DocumentNode, GraphQLSchema } from "graphql" import { SchemaComposer } from "graphql-compose" type SystemPath = string @@ -71,6 +72,15 @@ export interface IGatsbyNode { [key: string]: unknown } +export interface IGatsbyPlugin { + name: string + version: string +} + +export interface IGatsbyPluginContext { + [key: string]: (...args: any[]) => any +} + type GatsbyNodes = Map export interface IGatsbyState { @@ -223,6 +233,10 @@ export type ActionsUnion = | IQueryExtractionBabelErrorAction | ISetProgramStatusAction | IPageQueryRunAction + | IAddThirdPartySchema + | ICreateTypes + | ICreateFieldExtension + | IPrintTypeDefinitions export interface ICreatePageDependencyAction { type: `CREATE_COMPONENT_DEPENDENCY` @@ -251,7 +265,7 @@ export interface IReplaceComponentQueryAction { export interface IReplaceStaticQueryAction { type: `REPLACE_STATIC_QUERY` - plugin: Plugin | null | undefined + plugin: IGatsbyPlugin | null | undefined payload: { name: string componentPath: string @@ -263,28 +277,28 @@ export interface IReplaceStaticQueryAction { export interface IQueryExtractedAction { type: `QUERY_EXTRACTED` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: { componentPath: string; query: string } } export interface IQueryExtractionGraphQLErrorAction { type: `QUERY_EXTRACTION_GRAPHQL_ERROR` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: { componentPath: string; error: string } } export interface IQueryExtractedBabelSuccessAction { type: `QUERY_EXTRACTION_BABEL_SUCCESS` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: { componentPath: string } } export interface IQueryExtractionBabelErrorAction { type: `QUERY_EXTRACTION_BABEL_ERROR` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: { componentPath: string @@ -294,25 +308,70 @@ export interface IQueryExtractionBabelErrorAction { export interface ISetProgramStatusAction { type: `SET_PROGRAM_STATUS` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: ProgramStatus } export interface IPageQueryRunAction { type: `PAGE_QUERY_RUN` - plugin: Plugin + plugin: IGatsbyPlugin traceId: string | undefined payload: { path: string; componentPath: string; isPage: boolean } } export interface IRemoveStaleJobAction { type: `REMOVE_STALE_JOB_V2` - plugin: Plugin + plugin: IGatsbyPlugin traceId?: string payload: { contentDigest: string } } +export interface IAddThirdPartySchema { + type: `ADD_THIRD_PARTY_SCHEMA` + plugin: IGatsbyPlugin + traceId?: string + payload: GraphQLSchema +} + +export interface ICreateTypes { + type: `CREATE_TYPES` + plugin: IGatsbyPlugin + traceId?: string + payload: DocumentNode | DocumentNode[] +} + +export interface ICreateFieldExtension { + type: `CREATE_FIELD_EXTENSION` + plugin: IGatsbyPlugin + traceId?: string + payload: { + name: string + extension: GraphQLFieldExtensionDefinition + } +} + +export interface IPrintTypeDefinitions { + type: `PRINT_SCHEMA_REQUESTED` + plugin: IGatsbyPlugin + traceId?: string + payload: { + path?: string + include?: { types?: Array; plugins?: Array } + exclude?: { types?: Array; plugins?: Array } + withFieldTypes?: boolean + } +} + +export interface ICreateResolverContext { + type: `CREATE_RESOLVER_CONTEXT` + plugin: IGatsbyPlugin + traceId?: string + payload: + | IGatsbyPluginContext + | { [camelCasedPluginNameWithoutPrefix: string]: IGatsbyPluginContext } +} + export interface ICreateRedirectAction { type: `CREATE_REDIRECT` payload: IRedirect