From c59cf7af9d49f3f05e84ce3a0f10c767da9325cc Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Mon, 23 Mar 2020 15:38:44 -0400 Subject: [PATCH 1/9] chore(gatsby): Update load-plugins to Typescript. --- packages/gatsby/package.json | 2 + packages/gatsby/src/bootstrap/index.js | 2 +- .../load-plugins/__tests__/load-plugins.js | 2 +- .../load-plugins/{index.js => index.ts} | 52 +++++--- .../load-plugins/{load.js => load.ts} | 82 ++++++------ .../src/bootstrap/load-plugins/types.ts | 50 ++++++++ .../load-plugins/{validate.js => validate.ts} | 120 +++++++++++++----- 7 files changed, 213 insertions(+), 97 deletions(-) rename packages/gatsby/src/bootstrap/load-plugins/{index.js => index.ts} (52%) rename packages/gatsby/src/bootstrap/load-plugins/{load.js => load.ts} (84%) create mode 100644 packages/gatsby/src/bootstrap/load-plugins/types.ts rename packages/gatsby/src/bootstrap/load-plugins/{validate.js => validate.ts} (73%) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 4df70b8a8165d..d16bdf48d0932 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -158,7 +158,9 @@ "@babel/runtime": "^7.9.6", "@types/hapi__joi": "^16.0.12", "@types/reach__router": "^1.3.5", + "@types/semver": "^7.1.0", "@types/socket.io": "^2.1.4", + "@types/string-similarity": "^3.0.0", "@types/tmp": "^0.2.0", "babel-preset-gatsby-package": "^0.4.2", "cross-env": "^5.2.1", diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index f3da9b8014b27..d6163b5908dd7 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -16,7 +16,7 @@ import { createSchemaCustomization } from "../utils/create-schema-customization" import { startPluginRunner } from "../redux/plugin-runner" const { store, emitter } = require(`../redux`) import { internalActions } from "../redux/actions" -const loadPlugins = require(`./load-plugins`) +const { loadPlugins } = require(`./load-plugins`) const loadThemes = require(`./load-themes`) const reporter = require(`gatsby-cli/lib/reporter`) import { getConfigFile } from "./get-config-file" diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js index 5f524000d507e..d0e9d7787b952 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js @@ -1,4 +1,4 @@ -const loadPlugins = require(`../index`) +const { loadPlugins } = require(`../index`) const { slash } = require(`gatsby-core-utils`) describe(`Load plugins`, () => { diff --git a/packages/gatsby/src/bootstrap/load-plugins/index.js b/packages/gatsby/src/bootstrap/load-plugins/index.ts similarity index 52% rename from packages/gatsby/src/bootstrap/load-plugins/index.js rename to packages/gatsby/src/bootstrap/load-plugins/index.ts index a1f3daad5e845..43fcc9e2b2dbb 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/index.js +++ b/packages/gatsby/src/bootstrap/load-plugins/index.ts @@ -1,32 +1,36 @@ -const _ = require(`lodash`) +import _ from "lodash" -const { store } = require(`../../redux`) +import { store } from "../../redux" import * as nodeAPIs from "../../utils/api-node-docs" import * as browserAPIs from "../../utils/api-browser-docs" -const ssrAPIs = require(`../../../cache-dir/api-ssr-docs`) -const { loadPlugins } = require(`./load`) -const { +import ssrAPIs from "../../../cache-dir/api-ssr-docs" +import { loadPlugins as loadPluginsInternal } from "./load" +import { collatePluginAPIs, handleBadExports, handleMultipleReplaceRenderers, -} = require(`./validate`) + ICurrentAPIs, +} from "./validate" +import { IPluginInfo, IFlattenedPlugin, ISiteConfig } from "./types" -const getAPI = api => - _.keys(api).reduce((merged, key) => { +const getAPI = (api: ICurrentAPIs): ICurrentAPIs => + _.keys(api).reduce>((merged, key) => { merged[key] = _.keys(api[key]) return merged - }, {}) + }, {}) as ICurrentAPIs // Create a "flattened" array of plugins with all subplugins // brought to the top-level. This simplifies running gatsby-* files // for subplugins. -const flattenPlugins = plugins => { - const flattened = [] - const extractPlugins = plugin => { - plugin.pluginOptions.plugins.forEach(subPlugin => { - flattened.push(subPlugin) - extractPlugins(subPlugin) - }) +const flattenPlugins = (plugins: IPluginInfo[]): IPluginInfo[] => { + const flattened: IPluginInfo[] = [] + const extractPlugins = (plugin: IPluginInfo): void => { + if (plugin.pluginOptions && plugin.pluginOptions.plugins) { + plugin.pluginOptions.plugins.forEach(subPlugin => { + flattened.push(subPlugin) + extractPlugins(subPlugin) + }) + } } plugins.forEach(plugin => { @@ -37,22 +41,28 @@ const flattenPlugins = plugins => { return flattened } -module.exports = async (config = {}, rootDir = null) => { +export async function loadPlugins( + config: ISiteConfig = {}, + rootDir: string | null = null +): Promise { const currentAPIs = getAPI({ browser: browserAPIs, node: nodeAPIs, ssr: ssrAPIs, }) + // Collate internal plugins, site config plugins, site default plugins - const plugins = loadPlugins(config, rootDir) + const pluginInfos = loadPluginsInternal(config, rootDir) // Create a flattened array of the plugins - let flattenedPlugins = flattenPlugins(plugins) + const pluginArray = flattenPlugins(pluginInfos) // Work out which plugins use which APIs, including those which are not // valid Gatsby APIs, aka 'badExports' - const x = collatePluginAPIs({ currentAPIs, flattenedPlugins }) - flattenedPlugins = x.flattenedPlugins + const x = collatePluginAPIs({ currentAPIs, flattenedPlugins: pluginArray }) + + // From this point on, these are fully-resolved plugins. + let flattenedPlugins = x.flattenedPlugins const badExports = x.badExports // Show errors for any non-Gatsby APIs exported from plugins diff --git a/packages/gatsby/src/bootstrap/load-plugins/load.js b/packages/gatsby/src/bootstrap/load-plugins/load.ts similarity index 84% rename from packages/gatsby/src/bootstrap/load-plugins/load.js rename to packages/gatsby/src/bootstrap/load-plugins/load.ts index 339982e46c235..0b930b763ad7b 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/load.js +++ b/packages/gatsby/src/bootstrap/load-plugins/load.ts @@ -1,16 +1,24 @@ -const _ = require(`lodash`) -const { slash } = require(`gatsby-core-utils`) -const fs = require(`fs`) -const path = require(`path`) -const crypto = require(`crypto`) -const glob = require(`glob`) -const { warnOnIncompatiblePeerDependency } = require(`./validate`) -const { store } = require(`../../redux`) -const existsSync = require(`fs-exists-cached`).sync +import _ from "lodash" +import { slash } from "gatsby-core-utils" +import fs from "fs" +import path from "path" +import crypto from "crypto" +import glob from "glob" +import { warnOnIncompatiblePeerDependency } from "./validate" +import { store } from "../../redux" +import { sync as existsSync } from "fs-exists-cached" import { createNodeId } from "../../utils/create-node-id" -const { createRequireFromPath } = require(`gatsby-core-utils`) - -function createFileContentHash(root, globPattern) { +import { createRequireFromPath } from "gatsby-core-utils" +import { + IPluginInfo, + PluginRef, + IPluginRefObject, + IPluginRefOptions, + ISiteConfig, +} from "./types" +import { PackageJson } from "../../.." + +function createFileContentHash(root: string, globPattern: string): string { const hash = crypto.createHash(`md5`) const files = glob.sync(`${root}/${globPattern}`, { nodir: true }) @@ -25,33 +33,30 @@ function createFileContentHash(root, globPattern) { * Make sure key is unique to plugin options. E.g. there could * be multiple source-filesystem plugins, with different names * (docs, blogs). - * @param {*} name Name of the plugin - * @param {*} pluginObject + * + * @param name Name of the plugin */ -const createPluginId = (name, pluginObject = null) => +const createPluginId = ( + name: string, + pluginObject: IPluginRefObject | null = null +): string => createNodeId( name + (pluginObject ? JSON.stringify(pluginObject.options) : ``), `Plugin` ) /** - * @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 + * @param 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. - * @param {string} rootDir + * @param rootDir * This is the project location, from which are found the plugins - * @return {PluginInfo} */ -function resolvePlugin(pluginName, rootDir) { +export function resolvePlugin( + pluginName: string, + rootDir: string | null +): IPluginInfo { // Only find plugins when we're not given an absolute path if (!existsSync(pluginName)) { // Find the plugin in the local plugins folder @@ -61,7 +66,7 @@ function resolvePlugin(pluginName, rootDir) { if (existsSync(`${resolvedPath}/package.json`)) { const packageJSON = JSON.parse( fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) - ) + ) as PackageJson const name = packageJSON.name || pluginName warnOnIncompatiblePeerDependency(name, packageJSON) @@ -119,14 +124,17 @@ function resolvePlugin(pluginName, rootDir) { } } -const loadPlugins = (config = {}, rootDir = null) => { +export function loadPlugins( + config: ISiteConfig = {}, + rootDir: string | null = null +): IPluginInfo[] { // Instantiate plugins. - const plugins = [] + const plugins: IPluginInfo[] = [] // Create fake little site with a plugin for testing this // w/ snapshots. Move plugin processing to its own module. // Also test adding to redux store. - const processPlugin = plugin => { + function processPlugin(plugin: PluginRef): IPluginInfo { if (_.isString(plugin)) { const info = resolvePlugin(plugin, rootDir) @@ -147,7 +155,7 @@ const loadPlugins = (config = {}, rootDir = null) => { } // Plugins can have plugins. - const subplugins = [] + const subplugins: IPluginInfo[] = [] if (plugin.options.plugins) { plugin.options.plugins.forEach(p => { subplugins.push(processPlugin(p)) @@ -164,6 +172,7 @@ const loadPlugins = (config = {}, rootDir = null) => { return { id: createPluginId(name, plugin), name, + version: `0.0.0-test`, pluginOptions: { plugins: [], }, @@ -232,7 +241,7 @@ const loadPlugins = (config = {}, rootDir = null) => { const program = store.getState().program // default options for gatsby-plugin-page-creator - let pageCreatorOptions = { + let pageCreatorOptions: IPluginRefOptions | undefined = { path: slash(path.join(program.directory, `src/pages`)), pathCheck: false, } @@ -241,7 +250,7 @@ const loadPlugins = (config = {}, rootDir = null) => { const pageCreatorPlugin = config.plugins.find( plugin => plugin.resolve === `gatsby-plugin-page-creator` && - slash(plugin.options.path || ``) === + slash((plugin.options && plugin.options.path) || ``) === slash(path.join(program.directory, `src/pages`)) ) if (pageCreatorPlugin) { @@ -274,8 +283,3 @@ const loadPlugins = (config = {}, rootDir = null) => { return plugins } - -module.exports = { - loadPlugins, - resolvePlugin, -} diff --git a/packages/gatsby/src/bootstrap/load-plugins/types.ts b/packages/gatsby/src/bootstrap/load-plugins/types.ts new file mode 100644 index 0000000000000..07d461e867fa7 --- /dev/null +++ b/packages/gatsby/src/bootstrap/load-plugins/types.ts @@ -0,0 +1,50 @@ +export interface ISiteConfig { + plugins?: IPluginRefObject[] +} + +// There are two top-level "Plugin" concepts: +// 1. IPluginInfo, for processed plugins, and +// 2. PluginRef, for plugin configs + +export interface IPluginInfo { + /** Unique ID describing a plugin */ + id: string + + /** The absolute path to the plugin */ + resolve: string + + /** The plugin name */ + name: string + + /** The plugin version (can be content hash) */ + version: string + + /** Options passed to the plugin */ + pluginOptions?: IPluginInfoOptions +} + +export interface IPluginInfoOptions { + plugins?: IPluginInfo[] + path?: string + [option: string]: unknown +} + +export interface IFlattenedPlugin extends IPluginInfo { + skipSSR: boolean + ssrAPIs: string[] + nodeAPIs: string[] + browserAPIs: string[] +} + +export interface IPluginRefObject { + resolve: string + options?: IPluginRefOptions +} + +export type PluginRef = string | IPluginRefObject + +export interface IPluginRefOptions { + plugins?: PluginRef[] + path?: string + [option: string]: unknown +} diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.ts similarity index 73% rename from packages/gatsby/src/bootstrap/load-plugins/validate.js rename to packages/gatsby/src/bootstrap/load-plugins/validate.ts index fd9c8bc433e99..6e5754258de81 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -1,12 +1,34 @@ -const _ = require(`lodash`) -const semver = require(`semver`) -const stringSimilarity = require(`string-similarity`) -const { version: gatsbyVersion } = require(`gatsby/package.json`) -const reporter = require(`gatsby-cli/lib/reporter`) -const { resolveModuleExports } = require(`../resolve-module-exports`) -const { getLatestAPIs } = require(`../../utils/get-latest-apis`) +import _ from "lodash" +import * as semver from "semver" +import * as stringSimilarity from "string-similarity" +import { version as gatsbyVersion } from "gatsby/package.json" +import * as reporter from "gatsby-cli/lib/reporter" +import {resolveModuleExports} from "../resolve-module-exports" +import {getLatestAPIs} from "../../utils/get-latest-apis" +import { IPluginInfo, IFlattenedPlugin } from "./types" -const getGatsbyUpgradeVersion = entries => +interface IApi { + version?: string +} + +interface IEntry { + exportName: string + pluginName: string + pluginVersion: string + api?: IApi +} + +type ExportType = "node" | "browser" | "ssr" + +type IEntryMap = { + [exportType in ExportType]: IEntry[] +} + +export type ICurrentAPIs = { + [exportType in ExportType]: string[] +} + +const getGatsbyUpgradeVersion = (entries: readonly IEntry[]): string => entries.reduce((version, entry) => { if (entry.api && entry.api.version) { return semver.gt(entry.api.version, version || `0.0.0`) @@ -18,8 +40,12 @@ const getGatsbyUpgradeVersion = entries => // Given a plugin object, an array of the API names it exports and an // array of valid API names, return an array of invalid API exports. -const getBadExports = (plugin, pluginAPIKeys, apis) => { - let badExports = [] +function getBadExports( + plugin: IPluginInfo, + pluginAPIKeys: readonly string[], + apis: readonly string[] +): IEntry[] { + let badExports: IEntry[] = [] // Discover any exports from plugins which are not "known" badExports = badExports.concat( _.difference(pluginAPIKeys, apis).map(e => { @@ -33,7 +59,18 @@ const getBadExports = (plugin, pluginAPIKeys, apis) => { return badExports } -const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { +function getErrorContext( + badExports: IEntry[], + exportType: ExportType, + currentAPIs: ICurrentAPIs, + latestAPIs: { [exportType in ExportType]: { [exportName: string]: IApi } } +): { + errors: string[] + entries: IEntry[] + exportType: ExportType + fixes: string[] + sourceMessage: string +} { const entries = badExports.map(ex => { return { ...ex, @@ -42,10 +79,10 @@ const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { }) const gatsbyUpgradeVersion = getGatsbyUpgradeVersion(entries) - const errors = [] - const fixes = [].concat( - gatsbyUpgradeVersion ? [`npm install gatsby@^${gatsbyUpgradeVersion}`] : [] - ) + const errors: string[] = [] + const fixes = gatsbyUpgradeVersion + ? [`npm install gatsby@^${gatsbyUpgradeVersion}`] + : [] entries.forEach(entry => { const similarities = stringSimilarity.findBestMatch( @@ -88,18 +125,22 @@ const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { ] .concat(errors) .concat( - fixes.length > 0 && [ - `\n`, - `Some of the following may help fix the error(s):`, - ...fixes, - ] + fixes.length > 0 + ? [`\n`, `Some of the following may help fix the error(s):`, ...fixes] + : [] ) .filter(Boolean) .join(`\n`), } } -const handleBadExports = async ({ currentAPIs, badExports }) => { +export async function handleBadExports({ + currentAPIs, + badExports, +}: { + currentAPIs: ICurrentAPIs + badExports: { [api in ExportType]: IEntry[] } +}): Promise { const hasBadExports = Object.keys(badExports).find( api => badExports[api].length > 0 ) @@ -111,7 +152,7 @@ const handleBadExports = async ({ currentAPIs, badExports }) => { if (entries.length > 0) { const context = getErrorContext( entries, - exportType, + exportType as keyof typeof badExports, currentAPIs, latestAPIs ) @@ -127,9 +168,15 @@ const handleBadExports = async ({ currentAPIs, badExports }) => { /** * Identify which APIs each plugin exports */ -const collatePluginAPIs = ({ currentAPIs, flattenedPlugins }) => { +export function collatePluginAPIs({ + currentAPIs, + flattenedPlugins, +}: { + currentAPIs: ICurrentAPIs + flattenedPlugins: (IPluginInfo & Partial)[] +}): { flattenedPlugins: IFlattenedPlugin[]; badExports: IEntryMap } { // Get a list of bad exports - const badExports = { + const badExports: IEntryMap = { node: [], browser: [], ssr: [], @@ -181,10 +228,17 @@ const collatePluginAPIs = ({ currentAPIs, flattenedPlugins }) => { } }) - return { flattenedPlugins, badExports } + return { + flattenedPlugins: flattenedPlugins as IFlattenedPlugin[], + badExports, + } } -const handleMultipleReplaceRenderers = ({ flattenedPlugins }) => { +export const handleMultipleReplaceRenderers = ({ + flattenedPlugins, +}: { + flattenedPlugins: IFlattenedPlugin[] +}): IFlattenedPlugin[] => { // multiple replaceRenderers may cause problems at build time const rendererPlugins = flattenedPlugins .filter(plugin => plugin.ssrAPIs.includes(`replaceRenderer`)) @@ -214,7 +268,7 @@ const handleMultipleReplaceRenderers = ({ flattenedPlugins }) => { // For each plugin in ignorable, set a skipSSR flag to true // This prevents apiRunnerSSR() from attempting to run it later - const messages = [] + const messages: string[] = [] flattenedPlugins.forEach((fp, i) => { if (ignorable.includes(fp.name)) { messages.push( @@ -233,7 +287,10 @@ const handleMultipleReplaceRenderers = ({ flattenedPlugins }) => { return flattenedPlugins } -function warnOnIncompatiblePeerDependency(name, packageJSON) { +export function warnOnIncompatiblePeerDependency( + name: string, + packageJSON: object +): void { // Note: In the future the peer dependency should be enforced for all plugins. const gatsbyPeerDependency = _.get(packageJSON, `peerDependencies.gatsby`) if ( @@ -247,10 +304,3 @@ function warnOnIncompatiblePeerDependency(name, packageJSON) { ) } } - -module.exports = { - collatePluginAPIs, - handleBadExports, - handleMultipleReplaceRenderers, - warnOnIncompatiblePeerDependency, -} From 96b718446574985de5764d0c7c75dc993acb8706 Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Mon, 23 Mar 2020 15:40:11 -0400 Subject: [PATCH 2/9] define gastsby typings in terms of actual types This ensure's Gastby's typings are consistent with reality. --- packages/gatsby/index.d.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index df045ff3f9e36..a53dd8c7ac47f 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -12,6 +12,7 @@ import { ComposeUnionTypeConfig, } from "graphql-compose" import { GraphQLOutputType } from "graphql" +import { PluginRef } from "./src/bootstrap/load-plugins/types" export { default as Link, @@ -168,13 +169,7 @@ export interface GatsbyConfig { /** When you want to reuse common pieces of data across the site (for example, your site title), you can store that here. */ siteMetadata?: Record /** Plugins are Node.js packages that implement Gatsby APIs. The config file accepts an array of plugins. Some plugins may need only to be listed by name, while others may take options. */ - plugins?: Array< - | string - | { - resolve: string - options: Record - } - > + plugins?: Array /** It’s common for sites to be hosted somewhere other than the root of their domain. Say we have a Gatsby site at `example.com/blog/`. In this case, we would need a prefix (`/blog`) added to all paths on the site. */ pathPrefix?: string /** In some circumstances you may want to deploy assets (non-HTML resources such as JavaScript, CSS, etc.) to a separate domain. `assetPrefix` allows you to use Gatsby with assets hosted from a separate domain */ From 124b45114af95c55b42761914b8382a04c4909ec Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 24 Mar 2020 11:06:32 -0400 Subject: [PATCH 3/9] Update yarn.lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 7c12e49240063..cd63ef2a068bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4333,6 +4333,11 @@ version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" +"@types/string-similarity@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/string-similarity/-/string-similarity-3.0.0.tgz#12b655f1aab0156049657a4e9287caf3e6718d93" + integrity sha512-vhHkPKxl0cudrbxr5Dog1HVgUGXtmyYP95qy1da/h5gFEzIqDMN/+SjJAS7/6DEAdeI+AJQX8zrdWXL3wP4FRA== + "@types/tapable@*": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" From e66247895d1685aa48dbe7841cc25079cb2e0da5 Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 26 May 2020 12:06:07 -0400 Subject: [PATCH 4/9] Apply lint fixes for validate.ts. --- packages/gatsby/src/bootstrap/load-plugins/validate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index 6e5754258de81..fe85506bc0a83 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -3,8 +3,8 @@ import * as semver from "semver" import * as stringSimilarity from "string-similarity" import { version as gatsbyVersion } from "gatsby/package.json" import * as reporter from "gatsby-cli/lib/reporter" -import {resolveModuleExports} from "../resolve-module-exports" -import {getLatestAPIs} from "../../utils/get-latest-apis" +import { resolveModuleExports } from "../resolve-module-exports" +import { getLatestAPIs } from "../../utils/get-latest-apis" import { IPluginInfo, IFlattenedPlugin } from "./types" interface IApi { From d6f6dc95e61c69aa291db1591f2d00011565552e Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 26 May 2020 12:08:53 -0400 Subject: [PATCH 5/9] Typecheck fix. --- packages/gatsby/src/bootstrap/load-plugins/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index fe85506bc0a83..5111fbfce7d77 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -2,7 +2,7 @@ import _ from "lodash" import * as semver from "semver" import * as stringSimilarity from "string-similarity" import { version as gatsbyVersion } from "gatsby/package.json" -import * as reporter from "gatsby-cli/lib/reporter" +import { reporter } from "gatsby-cli/lib/reporter" import { resolveModuleExports } from "../resolve-module-exports" import { getLatestAPIs } from "../../utils/get-latest-apis" import { IPluginInfo, IFlattenedPlugin } from "./types" From 91484eacc3b8a762468bd43f0ed15201cfb46af4 Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 26 May 2020 12:33:52 -0400 Subject: [PATCH 6/9] Fix typecheck issues. --- packages/gatsby/src/bootstrap/load-plugins/load.ts | 10 +++++++--- packages/gatsby/src/bootstrap/load-plugins/types.ts | 3 ++- packages/gatsby/src/bootstrap/load-plugins/validate.ts | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/load.ts b/packages/gatsby/src/bootstrap/load-plugins/load.ts index 0b930b763ad7b..8c84c81801aa6 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/load.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/load.ts @@ -148,7 +148,10 @@ export function loadPlugins( plugin.options = plugin.options || {} // Throw an error if there is an "option" key. - if (_.isEmpty(plugin.options) && !_.isEmpty(plugin.option)) { + if ( + _.isEmpty(plugin.options) && + !_.isEmpty((plugin as { option?: unknown }).option) + ) { throw new Error( `Plugin "${plugin.resolve}" has an "option" key in the configuration. Did you mean "options"?` ) @@ -248,7 +251,8 @@ export function loadPlugins( if (config.plugins) { const pageCreatorPlugin = config.plugins.find( - plugin => + (plugin): plugin is IPluginRefObject => + typeof plugin !== `string` && plugin.resolve === `gatsby-plugin-page-creator` && slash((plugin.options && plugin.options.path) || ``) === slash(path.join(program.directory, `src/pages`)) @@ -262,7 +266,7 @@ export function loadPlugins( // TypeScript support by default! use the user-provided one if it exists const typescriptPlugin = (config.plugins || []).find( plugin => - plugin.resolve === `gatsby-plugin-typescript` || + (plugin as IPluginRefObject).resolve === `gatsby-plugin-typescript` || plugin === `gatsby-plugin-typescript` ) diff --git a/packages/gatsby/src/bootstrap/load-plugins/types.ts b/packages/gatsby/src/bootstrap/load-plugins/types.ts index 07d461e867fa7..6e5d9a8d87ab5 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/types.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/types.ts @@ -1,5 +1,5 @@ export interface ISiteConfig { - plugins?: IPluginRefObject[] + plugins?: PluginRef[] } // There are two top-level "Plugin" concepts: @@ -39,6 +39,7 @@ export interface IFlattenedPlugin extends IPluginInfo { export interface IPluginRefObject { resolve: string options?: IPluginRefOptions + parentDir?: string } export type PluginRef = string | IPluginRefObject diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index 5111fbfce7d77..fe85506bc0a83 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -2,7 +2,7 @@ import _ from "lodash" import * as semver from "semver" import * as stringSimilarity from "string-similarity" import { version as gatsbyVersion } from "gatsby/package.json" -import { reporter } from "gatsby-cli/lib/reporter" +import * as reporter from "gatsby-cli/lib/reporter" import { resolveModuleExports } from "../resolve-module-exports" import { getLatestAPIs } from "../../utils/get-latest-apis" import { IPluginInfo, IFlattenedPlugin } from "./types" From 1518c5129a57f88aefc140f82d151f3739b11ea4 Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 26 May 2020 13:27:37 -0400 Subject: [PATCH 7/9] Typecheck fix. --- packages/gatsby/src/bootstrap/load-plugins/index.ts | 5 ++++- packages/gatsby/src/bootstrap/load-plugins/validate.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/index.ts b/packages/gatsby/src/bootstrap/load-plugins/index.ts index 43fcc9e2b2dbb..812adab27c26b 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/index.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/index.ts @@ -9,11 +9,14 @@ import { collatePluginAPIs, handleBadExports, handleMultipleReplaceRenderers, + ExportType, ICurrentAPIs, } from "./validate" import { IPluginInfo, IFlattenedPlugin, ISiteConfig } from "./types" -const getAPI = (api: ICurrentAPIs): ICurrentAPIs => +const getAPI = ( + api: { [exportType in ExportType]: { [api: string]: boolean } } +): ICurrentAPIs => _.keys(api).reduce>((merged, key) => { merged[key] = _.keys(api[key]) return merged diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index fe85506bc0a83..ff1e3dec313f1 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -2,7 +2,7 @@ import _ from "lodash" import * as semver from "semver" import * as stringSimilarity from "string-similarity" import { version as gatsbyVersion } from "gatsby/package.json" -import * as reporter from "gatsby-cli/lib/reporter" +import reporter from "gatsby-cli/lib/reporter" import { resolveModuleExports } from "../resolve-module-exports" import { getLatestAPIs } from "../../utils/get-latest-apis" import { IPluginInfo, IFlattenedPlugin } from "./types" @@ -18,7 +18,7 @@ interface IEntry { api?: IApi } -type ExportType = "node" | "browser" | "ssr" +export type ExportType = "node" | "browser" | "ssr" type IEntryMap = { [exportType in ExportType]: IEntry[] From 302c7764305323cc3390548d830d1bc1bbe06380 Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Tue, 26 May 2020 16:13:07 -0400 Subject: [PATCH 8/9] Update tests to TS and Fix them. --- ...d-plugins.js.snap => load-plugins.ts.snap} | 0 .../{validate.js.snap => validate.ts.snap} | 0 .../{load-plugins.js => load-plugins.ts} | 11 ++-- .../__tests__/{validate.js => validate.ts} | 51 +++++++++++++------ .../src/bootstrap/load-plugins/types.ts | 2 +- .../src/bootstrap/load-plugins/validate.ts | 2 +- 6 files changed, 44 insertions(+), 22 deletions(-) rename packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/{load-plugins.js.snap => load-plugins.ts.snap} (100%) rename packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/{validate.js.snap => validate.ts.snap} (100%) rename packages/gatsby/src/bootstrap/load-plugins/__tests__/{load-plugins.js => load-plugins.ts} (93%) rename packages/gatsby/src/bootstrap/load-plugins/__tests__/{validate.js => validate.ts} (89%) diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.js.snap b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap similarity index 100% rename from packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.js.snap rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.ts.snap diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.ts.snap similarity index 100% rename from packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.ts.snap diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts similarity index 93% rename from packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts index d0e9d7787b952..8ac7bab90e4fe 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts @@ -1,5 +1,6 @@ -const { loadPlugins } = require(`../index`) -const { slash } = require(`gatsby-core-utils`) +import { loadPlugins } from "../index" +import { slash } from "gatsby-core-utils" +import { IFlattenedPlugin } from "../types" describe(`Load plugins`, () => { /** @@ -8,7 +9,9 @@ describe(`Load plugins`, () => { * Version can be updated (we use external plugin in default config). * Both will cause snapshots to differ. */ - const replaceFieldsThatCanVary = plugins => + const replaceFieldsThatCanVary = ( + plugins: IFlattenedPlugin[] + ): IFlattenedPlugin[] => plugins.map(plugin => { if (plugin.pluginOptions && plugin.pluginOptions.path) { plugin.pluginOptions = { @@ -176,7 +179,7 @@ describe(`Load plugins`, () => { plugins = replaceFieldsThatCanVary(plugins) const tsplugins = plugins.filter( - plugin => plugin.name === `gatsby-plugin-typescript` + (plugin: { name: string }) => plugin.name === `gatsby-plugin-typescript` ) // TODO: I think we should probably be de-duping, so this should be 1. diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts similarity index 89% rename from packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts index c118e78575c49..842f2c554aa65 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts @@ -8,19 +8,26 @@ jest.mock(`gatsby-cli/lib/reporter`, () => { jest.mock(`../../resolve-module-exports`) jest.mock(`../../../utils/get-latest-apis`) -const reporter = require(`gatsby-cli/lib/reporter`) -const { +import reporter from "gatsby-cli/lib/reporter" +import { collatePluginAPIs, handleBadExports, handleMultipleReplaceRenderers, warnOnIncompatiblePeerDependency, -} = require(`../validate`) -const { getLatestAPIs } = require(`../../../utils/get-latest-apis`) + ICurrentAPIs, + ExportType, + IEntry, +} from "../validate" +import { getLatestAPIs } from "../../../utils/get-latest-apis" beforeEach(() => { - Object.keys(reporter).forEach(key => reporter[key].mockReset()) - getLatestAPIs.mockClear() - getLatestAPIs.mockResolvedValue({ + Object.keys(reporter).forEach(key => (reporter[key] as jest.Mock).mockReset()) + + const mocked = (getLatestAPIs as unknown) as jest.MockedFunction< + typeof getLatestAPIs + > + mocked.mockClear() + mocked.mockResolvedValue({ browser: {}, node: {}, ssr: {}, @@ -101,8 +108,16 @@ describe(`collatePluginAPIs`, () => { }) describe(`handleBadExports`, () => { - const getValidExports = () => { + const getValidExports = (): { + currentAPIs: ICurrentAPIs + badExports: { [api in ExportType]: IEntry[] } + } => { return { + currentAPIs: { + node: [], + browser: [], + ssr: [], + }, badExports: { node: [], browser: [], @@ -137,6 +152,7 @@ describe(`handleBadExports`, () => { ssr: [ { exportName, + pluginVersion: `1.0.0`, pluginName: `default-site-plugin`, }, ], @@ -196,7 +212,8 @@ describe(`handleBadExports`, () => { it(`Adds fixes to context if newer API introduced in Gatsby`, async () => { const version = `2.2.0` - getLatestAPIs.mockResolvedValueOnce({ + const mocked = getLatestAPIs as jest.MockedFunction + mocked.mockResolvedValueOnce({ browser: {}, ssr: {}, node: { @@ -220,6 +237,7 @@ describe(`handleBadExports`, () => { { exportName: `validatePluginOptions`, pluginName: `gatsby-source-contentful`, + pluginVersion: version, }, ], }, @@ -249,11 +267,6 @@ describe(`handleBadExports`, () => { browser: [``], ssr: [``], }, - latestAPIs: { - browser: {}, - ssr: {}, - node: {}, - }, badExports: { browser: [], ssr: [], @@ -261,6 +274,7 @@ describe(`handleBadExports`, () => { { exportName: typoOrOldAPI, pluginName: `default-site-plugin`, + pluginVersion: `2.1.0`, }, ], }, @@ -269,7 +283,10 @@ describe(`handleBadExports`, () => { ) expect(reporter.error).toHaveBeenCalledTimes(typoAPIs.length) - const calls = reporter.error.mock.calls + const calls = ((reporter.error as unknown) as jest.MockedFunction< + typeof reporter.error + >).mock.calls + calls.forEach(([call]) => { expect(call).toEqual( expect.objectContaining({ @@ -346,7 +363,9 @@ describe(`handleMultipleReplaceRenderers`, () => { describe(`warnOnIncompatiblePeerDependency`, () => { beforeEach(() => { - reporter.warn.mockClear() + ;((reporter.warn as unknown) as jest.MockedFunction< + typeof reporter.warn + >).mockClear() }) it(`Does not warn when no peer dependency`, () => { diff --git a/packages/gatsby/src/bootstrap/load-plugins/types.ts b/packages/gatsby/src/bootstrap/load-plugins/types.ts index 6e5d9a8d87ab5..b9f0b8876945d 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/types.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/types.ts @@ -30,7 +30,7 @@ export interface IPluginInfoOptions { } export interface IFlattenedPlugin extends IPluginInfo { - skipSSR: boolean + skipSSR?: boolean ssrAPIs: string[] nodeAPIs: string[] browserAPIs: string[] diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index ff1e3dec313f1..4b24668db90c3 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -11,7 +11,7 @@ interface IApi { version?: string } -interface IEntry { +export interface IEntry { exportName: string pluginName: string pluginVersion: string From 562118a979f29fff9041364b3c97e8a431858c10 Mon Sep 17 00:00:00 2001 From: Eyas Date: Wed, 27 May 2020 14:11:53 -0400 Subject: [PATCH 9/9] Cast validate.ts use of resolveModuleExports --- .../src/bootstrap/load-plugins/__tests__/validate.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts index 842f2c554aa65..f774ca2960a34 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts @@ -19,6 +19,7 @@ import { IEntry, } from "../validate" import { getLatestAPIs } from "../../../utils/get-latest-apis" +import { resolveModuleExports } from "../../resolve-module-exports" beforeEach(() => { Object.keys(reporter).forEach(key => (reporter[key] as jest.Mock).mockReset()) @@ -48,8 +49,11 @@ describe(`collatePluginAPIs`, () => { } beforeEach(() => { - const { resolveModuleExports } = require(`../../resolve-module-exports`) - resolveModuleExports(MOCK_RESULTS) + // We call the manual /__mocks__/ implementation of resolveModuleExports, + // which in addition to the normal parameters, also takes a mock results. + // In the future, we might just use jest to mock the return value instead + // of relying on manual mocks. + resolveModuleExports(MOCK_RESULTS as any) }) it(`Identifies APIs used by a site's plugins`, async () => {