From 025874f1f193bfada764a22599d5deec85b3232f Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 11:38:23 -0700 Subject: [PATCH 01/13] feat(gatsby): cache latest APIs on Gatsby install --- packages/gatsby/package.json | 2 ++ packages/gatsby/scripts/postinstall.js | 41 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 packages/gatsby/scripts/postinstall.js diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 9536843feed97..adaa1aa32368d 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -157,6 +157,7 @@ "dist", "graphql.js", "index.d.ts", + "scripts/postinstall.js", "utils.js" ], "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby#readme", @@ -192,6 +193,7 @@ "build:src": "babel src --out-dir dist --source-maps --verbose --ignore **/gatsby-cli.js,src/internal-plugins/dev-404-page/raw_dev-404-page.js,**/__tests__", "clean-test-bundles": "find test/ -type f -name bundle.js* -exec rm -rf {} +", "prebuild": "rimraf dist && rimraf cache-dir/commonjs", + "postinstall": "node scripts/postinstall.js", "prepare": "cross-env NODE_ENV=production npm run build", "watch": "rimraf dist && mkdir dist && npm run build:internal-plugins && npm run build:rawfiles && npm run build:src -- --watch" }, diff --git a/packages/gatsby/scripts/postinstall.js b/packages/gatsby/scripts/postinstall.js new file mode 100644 index 0000000000000..ba9a8ac936ed0 --- /dev/null +++ b/packages/gatsby/scripts/postinstall.js @@ -0,0 +1,41 @@ +const path = require('path') +const fs = require('fs') +const https = require('https') +const url = require('url') + +const getJSON = (jsonURL, callback) => { + return https.get(jsonURL, res => { + if (res.statusCode === 302) { + let redirect = url.parse(jsonURL) + redirect.pathname = res.headers.location + return getJSON(url.format(redirect), callback) + } + let data = '' + res.on('data', chunk => { + data += chunk + }) + + res.on('end', () => { + try { + return callback(null, JSON.parse(data)) + } catch (e) { + return callback(e) + } + }) + }) + .on('error', callback) +} + +/* + * + */ +getJSON('https://unpkg.com/gatsby/apis.json', (err, data) => { + if (err) { + console.log(`An error occurred attempting to cache Gatsby APIs. We'll try again next run!`) + return + } + return fs.writeFile(path.join(__dirname, '..', 'latest-apis.json'), JSON.stringify(data, null, 2), writeErr => { + console.log(writeErr) + }) +}) + From c6864a711dca5735c55dde37f5770f95e9869f53 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 12:07:57 -0700 Subject: [PATCH 02/13] chore: slightly refactor with axios --- packages/gatsby/.gitignore | 5 ++- packages/gatsby/package.json | 1 + packages/gatsby/scripts/postinstall.js | 42 +------------------- packages/gatsby/src/utils/get-latest-apis.js | 33 +++++++++++++++ yarn.lock | 1 + 5 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 packages/gatsby/src/utils/get-latest-apis.js diff --git a/packages/gatsby/.gitignore b/packages/gatsby/.gitignore index e681c7ff19b85..dba058b704eb1 100644 --- a/packages/gatsby/.gitignore +++ b/packages/gatsby/.gitignore @@ -30,5 +30,8 @@ decls dist # built files -cache-dir/commonjs/ apis.json +cache-dir/commonjs/ + +# cached files +/latest-apis.json diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index adaa1aa32368d..13cb9684d9b85 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -23,6 +23,7 @@ "@reach/router": "^1.1.1", "address": "1.0.3", "autoprefixer": "^9.6.0", + "axios": "^0.19.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^9.0.0", "babel-loader": "^8.0.0", diff --git a/packages/gatsby/scripts/postinstall.js b/packages/gatsby/scripts/postinstall.js index ba9a8ac936ed0..c833abe2ed97a 100644 --- a/packages/gatsby/scripts/postinstall.js +++ b/packages/gatsby/scripts/postinstall.js @@ -1,41 +1,3 @@ -const path = require('path') -const fs = require('fs') -const https = require('https') -const url = require('url') +const getLatestAPIs = require('../dist/utils/get-latest-apis') -const getJSON = (jsonURL, callback) => { - return https.get(jsonURL, res => { - if (res.statusCode === 302) { - let redirect = url.parse(jsonURL) - redirect.pathname = res.headers.location - return getJSON(url.format(redirect), callback) - } - let data = '' - res.on('data', chunk => { - data += chunk - }) - - res.on('end', () => { - try { - return callback(null, JSON.parse(data)) - } catch (e) { - return callback(e) - } - }) - }) - .on('error', callback) -} - -/* - * - */ -getJSON('https://unpkg.com/gatsby/apis.json', (err, data) => { - if (err) { - console.log(`An error occurred attempting to cache Gatsby APIs. We'll try again next run!`) - return - } - return fs.writeFile(path.join(__dirname, '..', 'latest-apis.json'), JSON.stringify(data, null, 2), writeErr => { - console.log(writeErr) - }) -}) - +getLatestAPIs() diff --git a/packages/gatsby/src/utils/get-latest-apis.js b/packages/gatsby/src/utils/get-latest-apis.js new file mode 100644 index 0000000000000..74267c85f25fc --- /dev/null +++ b/packages/gatsby/src/utils/get-latest-apis.js @@ -0,0 +1,33 @@ +const path = require(`path`) +const fs = require(`fs-extra`) +const axios = require(`axios`) + +const API_FILE = `https://unpkg.com/gatsby/apis.json` +const ROOT = path.join(__dirname, `..`, `..`) +const OUTPUT_FILE = path.join(ROOT, `latest-apis.json`) + +module.exports = async function getLatestAPI() { + /* + * Happy path `postinstall` script created the file + */ + if (await fs.exists(OUTPUT_FILE)) { + return fs.readJSON(OUTPUT_FILE) + } + + try { + const { data } = await axios.get(API_FILE) + + await fs.writeFile(OUTPUT_FILE, JSON.stringify(data, null, 2), `utf8`) + + return data + } catch (e) { + // possible offline/network issue + return fs.readJSON(path.join(ROOT, `apis.json`)).catch(() => { + return { + browser: {}, + node: {}, + ssr: {}, + } + }) + } +} diff --git a/yarn.lock b/yarn.lock index 6a74fb0bcced0..db491b4e5000d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4078,6 +4078,7 @@ axios@^0.18.0: axios@^0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== dependencies: follow-redirects "1.5.10" is-buffer "^2.0.2" From f1ea99e3f4cca3c3fd7410a38eec3599fa0e9560 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 17:08:24 -0700 Subject: [PATCH 03/13] feat(gatsby): introduced richer error logging for unknown APIs --- .../reporters/ink/components/error.js | 85 +++++++--- .../src/structured-errors/construct-error.js | 4 +- .../src/structured-errors/error-map.js | 30 ++++ packages/gatsby/src/bootstrap/index.js | 5 +- .../src/bootstrap/load-plugins/index.js | 24 ++- .../src/bootstrap/load-plugins/validate.js | 153 ++++++++++-------- .../src/utils/__tests__/get-latest-apis.js | 64 ++++++++ packages/gatsby/src/utils/api-node-docs.js | 2 +- packages/gatsby/src/utils/api-runner-node.js | 10 -- 9 files changed, 267 insertions(+), 110 deletions(-) create mode 100644 packages/gatsby/src/utils/__tests__/get-latest-apis.js diff --git a/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js b/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js index 769a3a9ca250d..e2a27a1c4f37d 100644 --- a/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js +++ b/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js @@ -3,6 +3,40 @@ import path from "path" import { Color, Box } from "ink" import { get } from "lodash" +const colors = { + WARNING: { + bg: { bgYellow: true, black: true }, + fg: { yellow: true }, + }, + ERROR: { + bg: { + bgRed: true, + black: true, + }, + fg: { + red: true, + }, + }, + INFO: { + bg: { + bgBlue: true, + white: true, + }, + fg: { + blue: true, + }, + }, + DEBUG: { + bg: { + bgCyan: true, + black: true, + }, + fg: { + cyan: true, + }, + }, +} + const File = ({ filePath, location }) => { const lineNumber = get(location, `start.line`) @@ -34,32 +68,34 @@ const DocsLink = ({ docsUrl }) => { ) } -const Error = ({ details }) => ( - // const stackLength = get(details, `stack.length`, 0 - - - +const Error = ({ details }) => { + const color = colors[details.level] || colors.error + return ( + - - - - {` ${details.level} `} - {details.id ? `#${details.id} ` : ``} - - {details.type ? ` ` + details.type : ``} + + + + + {` ${details.level} `} + {details.id ? `#${details.id} ` : ``} + + + {details.type ? ` ` + details.type : ``} + + + {details.text} + {details.filePath && ( + + File:{` `} + + + )} - {details.text} - {details.filePath && ( - - File:{` `} - - - )} + - - - {/* TODO: use this to replace errorFormatter.render in reporter.error func + {/* TODO: use this to replace errorFormatter.render in reporter.error func {stackLength > 0 && ( @@ -74,7 +110,8 @@ const Error = ({ details }) => ( )} */} - -) + + ) +} export default Error diff --git a/packages/gatsby-cli/src/structured-errors/construct-error.js b/packages/gatsby-cli/src/structured-errors/construct-error.js index 45dd17a7aec06..b1ded9d9763b8 100644 --- a/packages/gatsby-cli/src/structured-errors/construct-error.js +++ b/packages/gatsby-cli/src/structured-errors/construct-error.js @@ -15,9 +15,7 @@ const constructError = ({ details }) => { ...result, text: result.text(details.context), stack: details.error ? stackTrace.parse(details.error) : [], - docsUrl: result.docsUrl - ? result.docsUrl - : `https://gatsby.dev/issue-how-to`, + docsUrl: result.docsUrl || `https://gatsby.dev/issue-how-to`, } // validate diff --git a/packages/gatsby-cli/src/structured-errors/error-map.js b/packages/gatsby-cli/src/structured-errors/error-map.js index 9e48133bce1bd..a7f6f273fdadc 100644 --- a/packages/gatsby-cli/src/structured-errors/error-map.js +++ b/packages/gatsby-cli/src/structured-errors/error-map.js @@ -1,3 +1,5 @@ +const { stripIndent } = require(`common-tags`) + const errorMap = { "": { text: context => { @@ -160,6 +162,34 @@ const errorMap = { }`, level: `ERROR`, }, + // invalid or deprecated APIs + "11329": { + text: context => + [ + stripIndent(` + Your plugins must export known APIs from their gatsby-${ + context.exportType + }.js. + + See https://www.gatsbyjs.org/docs/${ + context.exportType + }-apis/ for the list of Gatsby ${context.exportType} APIs. + `), + ] + .concat([``].concat(context.errors)) + .concat( + context.fixes.length > 0 + ? [ + ``, + `Some of the following may help fix the error(s):`, + ``, + ...context.fixes.map(fix => `- ${fix}`), + ] + : [] + ) + .join(`\n`), + level: `WARNING`, + }, } module.exports = { errorMap, defaultError: errorMap[``] } diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index eefe4f6b31d9a..3b9437bf7eea9 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -12,6 +12,7 @@ const telemetry = require(`gatsby-telemetry`) const apiRunnerNode = require(`../utils/api-runner-node`) const getBrowserslist = require(`../utils/browserslist`) +const getLatestAPIs = require(`../utils/get-latest-apis`) const { store, emitter } = require(`../redux`) const loadPlugins = require(`./load-plugins`) const loadThemes = require(`./load-themes`) @@ -103,9 +104,11 @@ module.exports = async (args: BootstrapArgs) => { activity.end() + const apis = await getLatestAPIs() + activity = report.activityTimer(`load plugins`) activity.start() - const flattenedPlugins = await loadPlugins(config, program.directory) + const flattenedPlugins = await loadPlugins(config, program.directory, apis) activity.end() telemetry.decorateEvent(`BUILD_END`, { diff --git a/packages/gatsby/src/bootstrap/load-plugins/index.js b/packages/gatsby/src/bootstrap/load-plugins/index.js index 3f0bf4fcd2a9c..065ffb83dbf92 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/index.js +++ b/packages/gatsby/src/bootstrap/load-plugins/index.js @@ -11,11 +11,11 @@ const { handleMultipleReplaceRenderers, } = require(`./validate`) -const apis = { - node: _.keys(nodeAPIs), - browser: _.keys(browserAPIs), - ssr: _.keys(ssrAPIs), -} +const getAPI = api => + _.keys(api).reduce((merged, key) => { + merged[key] = _.keys(api[key]) + return merged + }, {}) // Create a "flattened" array of plugins with all subplugins // brought to the top-level. This simplifies running gatsby-* files @@ -37,7 +37,15 @@ const flattenPlugins = plugins => { return flattened } -module.exports = async (config = {}, rootDir = null) => { +module.exports = async (config = {}, rootDir = null, latestAPIs = {}) => { + const apis = { + currentAPIs: getAPI({ + browser: browserAPIs, + node: nodeAPIs, + ssr: ssrAPIs, + }), + latestAPIs, + } // Collate internal plugins, site config plugins, site default plugins const plugins = loadPlugins(config, rootDir) @@ -46,12 +54,12 @@ module.exports = async (config = {}, rootDir = null) => { // Work out which plugins use which APIs, including those which are not // valid Gatsby APIs, aka 'badExports' - const x = collatePluginAPIs({ apis, flattenedPlugins }) + const x = collatePluginAPIs({ ...apis, flattenedPlugins }) flattenedPlugins = x.flattenedPlugins const badExports = x.badExports // Show errors for any non-Gatsby APIs exported from plugins - handleBadExports({ apis, badExports }) + handleBadExports({ ...apis, badExports }) // Show errors when ReplaceRenderer has been implemented multiple times flattenedPlugins = handleMultipleReplaceRenderers({ diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.js index 24b8e47a99e34..67a16ad0d8128 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.js @@ -1,9 +1,20 @@ const _ = require(`lodash`) const semver = require(`semver`) +const stringSimiliarity = require(`string-similarity`) const { version: gatsbyVersion } = require(`gatsby/package.json`) const reporter = require(`gatsby-cli/lib/reporter`) const resolveModuleExports = require(`../resolve-module-exports`) +const getGatsbyUpgradeVersion = entries => + entries.reduce((version, entry) => { + if (entry.api && entry.api.version) { + return semver.gt(entry.api.version, version || `0.0.0`) + ? entry.api.version + : version + } + return version + }, ``) + // 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) => { @@ -21,81 +32,94 @@ const getBadExports = (plugin, pluginAPIKeys, apis) => { return badExports } -const getBadExportsMessage = (badExports, exportType, apis) => { - const { stripIndent } = require(`common-tags`) - const stringSimiliarity = require(`string-similarity`) +const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { let capitalized = `${exportType[0].toUpperCase()}${exportType.slice(1)}` if (capitalized === `Ssr`) capitalized = `SSR` - let message = `\n` - message += stripIndent` - Your plugins must export known APIs from their gatsby-${exportType}.js. - The following exports aren't APIs. Perhaps you made a typo or your plugin is outdated? - - See https://www.gatsbyjs.org/docs/${exportType}-apis/ for the list of Gatsby ${capitalized} APIs - ` - - badExports.forEach(bady => { - message += `\n\n` - const similarities = stringSimiliarity.findBestMatch(bady.exportName, apis) - const isDefaultPlugin = bady.pluginName == `default-site-plugin` - const badExportsMigrationMap = { - modifyWebpackConfig: { - replacement: `onCreateWebpackConfig`, - migrationLink: `https://gatsby.dev/update-webpack-config`, - }, - wrapRootComponent: { - replacement: `wrapRootElement`, - migrationLink: `https://gatsby.dev/update-wraprootcomponent`, - }, + const entries = badExports.map(ex => { + return { + ...ex, + api: latestAPIs[exportType][ex.exportName], } - const isOldAPI = Object.keys(badExportsMigrationMap).includes( - bady.exportName + }) + + const gatsbyUpgradeVersion = getGatsbyUpgradeVersion(entries) + let errors = [] + let fixes = [].concat( + gatsbyUpgradeVersion ? [`npm install gatsby@^${gatsbyUpgradeVersion}`] : [] + ) + + entries.forEach(entry => { + const similarities = stringSimiliarity.findBestMatch( + entry.exportName, + currentAPIs[exportType] ) + const isDefaultPlugin = entry.pluginName == `default-site-plugin` + + const message = entry.api + ? entry.api.version + ? `was introduced in gatsby@${entry.api.version}` + : `is not available in your version of Gatsby` + : `isn't an API` - if (isDefaultPlugin && isOldAPI) { - const { replacement, migrationLink } = badExportsMigrationMap[ - bady.exportName - ] - message += stripIndent` - - Your site's gatsby-${exportType}.js is exporting "${ - bady.exportName - }" which was removed in Gatsby v2. Refer to the migration guide for more info on upgrading to "${replacement}": - ` - message += `\n ${migrationLink}` - } else if (isDefaultPlugin) { - message += stripIndent` - - Your site's gatsby-${exportType}.js is exporting a variable named "${ - bady.exportName - }" which isn't an API. - ` + if (isDefaultPlugin) { + errors.push( + `- Your local gatsby-${exportType}.js is exporting a variable named "${ + entry.exportName + }" which ${message}.` + ) } else { - message += stripIndent` - - The plugin "${bady.pluginName}@${ - bady.pluginVersion - }" is exporting a variable named "${bady.exportName}" which isn't an API. - ` + errors.push( + `- The plugin "${entry.pluginName}@${ + entry.pluginVersion + } is exporting a variable "${entry.exportName}" which ${message}.` + ) } - if (similarities.bestMatch.rating > 0.5 && !isOldAPI) { - message += `\n\n` - message += `Perhaps you meant to export "${ - similarities.bestMatch.target - }"?` + if (similarities.bestMatch.rating > 0.5) { + fixes.push( + `Rename "${entry.exportName}" -> "${similarities.bestMatch.target}"` + ) } }) - return message + return { + errors, + entries, + exportType, + fixes, + // note: this is a fallback if gatsby-cli is not updated with structured error + sourceMessage: [ + `Your plugins must export known APIs from their gatsby-node.js.`, + ] + .concat(errors) + .concat( + fixes.length > 0 && [ + `\n`, + `Some of the following may help fix the error(s):`, + ...fixes, + ] + ) + .filter(Boolean) + .join(`\n`), + } } -const handleBadExports = ({ apis, badExports }) => { +const handleBadExports = ({ currentAPIs, latestAPIs, badExports }) => { // Output error messages for all bad exports _.toPairs(badExports).forEach(badItem => { const [exportType, entries] = badItem if (entries.length > 0) { - reporter.panicOnBuild( - getBadExportsMessage(entries, exportType, apis[exportType]) + const context = getErrorContext( + entries, + exportType, + currentAPIs, + latestAPIs ) + reporter.error({ + id: `11329`, + context, + }) } }) } @@ -103,7 +127,7 @@ const handleBadExports = ({ apis, badExports }) => { /** * Identify which APIs each plugin exports */ -const collatePluginAPIs = ({ apis, flattenedPlugins }) => { +const collatePluginAPIs = ({ currentAPIs, flattenedPlugins }) => { // Get a list of bad exports const badExports = { node: [], @@ -133,23 +157,26 @@ const collatePluginAPIs = ({ apis, flattenedPlugins }) => { ) if (pluginNodeExports.length > 0) { - plugin.nodeAPIs = _.intersection(pluginNodeExports, apis.node) + plugin.nodeAPIs = _.intersection(pluginNodeExports, currentAPIs.node) badExports.node = badExports.node.concat( - getBadExports(plugin, pluginNodeExports, apis.node) + getBadExports(plugin, pluginNodeExports, currentAPIs.node) ) // Collate any bad exports } if (pluginBrowserExports.length > 0) { - plugin.browserAPIs = _.intersection(pluginBrowserExports, apis.browser) + plugin.browserAPIs = _.intersection( + pluginBrowserExports, + currentAPIs.browser + ) badExports.browser = badExports.browser.concat( - getBadExports(plugin, pluginBrowserExports, apis.browser) + getBadExports(plugin, pluginBrowserExports, currentAPIs.browser) ) // Collate any bad exports } if (pluginSSRExports.length > 0) { - plugin.ssrAPIs = _.intersection(pluginSSRExports, apis.ssr) + plugin.ssrAPIs = _.intersection(pluginSSRExports, currentAPIs.ssr) badExports.ssr = badExports.ssr.concat( - getBadExports(plugin, pluginSSRExports, apis.ssr) + getBadExports(plugin, pluginSSRExports, currentAPIs.ssr) ) // Collate any bad exports } }) diff --git a/packages/gatsby/src/utils/__tests__/get-latest-apis.js b/packages/gatsby/src/utils/__tests__/get-latest-apis.js new file mode 100644 index 0000000000000..0ab663d4639f0 --- /dev/null +++ b/packages/gatsby/src/utils/__tests__/get-latest-apis.js @@ -0,0 +1,64 @@ +jest.mock(`fs-extra`, () => { + return { + exists: jest.fn(), + readJSON: jest.fn(), + writeFile: jest.fn(), + } +}) +jest.mock(`axios`, () => { + return { + get: jest.fn(), + } +}) +const fs = require(`fs-extra`) +const axios = require(`axios`) +const getLatestAPIs = require(`../get-latest-apis`) + +beforeEach(() => { + ;[fs, axios].forEach(mock => + Object.keys(mock).forEach(key => mock[key].mockClear()) + ) +}) + +const getMockAPIFile = () => { + return { + node: {}, + browser: {}, + ssr: {}, + } +} + +it(`defaults to cached file, if it exists`, async () => { + const apis = getMockAPIFile() + fs.exists.mockResolvedValueOnce(true) + fs.readJSON.mockResolvedValueOnce(apis) + + const data = await getLatestAPIs() + + expect(fs.writeFile).not.toHaveBeenCalled() + expect(data).toEqual(apis) +}) + +describe(`API file not cached`, () => { + beforeEach(() => { + fs.exists.mockResolvedValueOnce(false) + axios.get.mockResolvedValueOnce({ data: getMockAPIFile() }) + }) + + it(`makes a request to unpkg to request file`, async () => { + const data = await getLatestAPIs() + + expect(axios.get).toHaveBeenCalledWith(expect.stringContaining(`unpkg.com`)) + expect(data).toEqual(getMockAPIFile()) + }) + + it(`writes api file`, async () => { + const data = await getLatestAPIs() + + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining(`latest-apis.json`), + JSON.stringify(data, null, 2), + expect.any(String) + ) + }) +}) diff --git a/packages/gatsby/src/utils/api-node-docs.js b/packages/gatsby/src/utils/api-node-docs.js index 9553f5636722d..5139848866543 100644 --- a/packages/gatsby/src/utils/api-node-docs.js +++ b/packages/gatsby/src/utils/api-node-docs.js @@ -242,7 +242,7 @@ exports.setFieldsOnGraphQLNodeType = true * createTypes(typeDefs) * } */ -exports.createSchemaCustomization = true +// exports.createSchemaCustomization = true /** * Add custom field resolvers to the GraphQL schema. diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js index d5bd390a3b739..28910e9eb13c4 100644 --- a/packages/gatsby/src/utils/api-runner-node.js +++ b/packages/gatsby/src/utils/api-runner-node.js @@ -6,7 +6,6 @@ const { bindActionCreators } = require(`redux`) const tracer = require(`opentracing`).globalTracer() const reporter = require(`gatsby-cli/lib/reporter`) const getCache = require(`./get-cache`) -const apiList = require(`./api-node-docs`) const createNodeId = require(`./create-node-id`) const { createContentDigest } = require(`gatsby-core-utils`) const { @@ -245,15 +244,6 @@ module.exports = async (api, args = {}, pluginSource) => apiSpan.setTag(key, value) }) - // Check that the API is documented. - // "FAKE_API_CALL" is used when code needs to trigger something - // to happen once the the API queue is empty. Ideally of course - // we'd have an API (returning a promise) for that. But this - // works nicely in the meantime. - if (!apiList[api] && api !== `FAKE_API_CALL`) { - reporter.panic(`api: "${api}" is not a valid Gatsby api`) - } - const { store } = require(`../redux`) const plugins = store.getState().flattenedPlugins From b7a1930b22f4bd654f22b53c0168dad0d22d3b34 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 17:17:19 -0700 Subject: [PATCH 04/13] chore: warning -> error --- packages/gatsby-cli/src/structured-errors/error-map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby-cli/src/structured-errors/error-map.js b/packages/gatsby-cli/src/structured-errors/error-map.js index a7f6f273fdadc..4fadb7c6aaa76 100644 --- a/packages/gatsby-cli/src/structured-errors/error-map.js +++ b/packages/gatsby-cli/src/structured-errors/error-map.js @@ -188,7 +188,7 @@ const errorMap = { : [] ) .join(`\n`), - level: `WARNING`, + level: `ERROR`, }, } From 7861d1fd5be98ff57b378ac0d2feaffa5c784873 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 17:20:40 -0700 Subject: [PATCH 05/13] chore: remove comment I used to test --- packages/gatsby/src/utils/api-node-docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gatsby/src/utils/api-node-docs.js b/packages/gatsby/src/utils/api-node-docs.js index 5139848866543..9553f5636722d 100644 --- a/packages/gatsby/src/utils/api-node-docs.js +++ b/packages/gatsby/src/utils/api-node-docs.js @@ -242,7 +242,7 @@ exports.setFieldsOnGraphQLNodeType = true * createTypes(typeDefs) * } */ -// exports.createSchemaCustomization = true +exports.createSchemaCustomization = true /** * Add custom field resolvers to the GraphQL schema. From 74b45642da702e7a4b853eeb9459d8ed0fa5eb50 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 17:32:45 -0700 Subject: [PATCH 06/13] fix: wrap postinstall in try/catch --- packages/gatsby/scripts/postinstall.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/scripts/postinstall.js b/packages/gatsby/scripts/postinstall.js index c833abe2ed97a..daa1ec9ad4705 100644 --- a/packages/gatsby/scripts/postinstall.js +++ b/packages/gatsby/scripts/postinstall.js @@ -1,3 +1,6 @@ -const getLatestAPIs = require('../dist/utils/get-latest-apis') - -getLatestAPIs() +try { + const getLatestAPIs = require('../dist/utils/get-latest-apis') + getLatestAPIs() +} catch (e) { + // we're probably just bootstrapping and not published yet! +} From a2dabd08f6a4a3f32e5e18909d610f92937f4fbe Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Thu, 25 Jul 2019 17:41:21 -0700 Subject: [PATCH 07/13] chore: slight tweaks to verbiage --- packages/gatsby/src/bootstrap/load-plugins/validate.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.js index 67a16ad0d8128..dd26e56bbb7ab 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.js @@ -60,11 +60,11 @@ const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { ? entry.api.version ? `was introduced in gatsby@${entry.api.version}` : `is not available in your version of Gatsby` - : `isn't an API` + : `is not a known API` if (isDefaultPlugin) { errors.push( - `- Your local gatsby-${exportType}.js is exporting a variable named "${ + `- Your local gatsby-${exportType}.js is using the API "${ entry.exportName }" which ${message}.` ) @@ -72,7 +72,7 @@ const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { errors.push( `- The plugin "${entry.pluginName}@${ entry.pluginVersion - } is exporting a variable "${entry.exportName}" which ${message}.` + } is using the API "${entry.exportName}" which ${message}.` ) } From b4665f99029c35a4de7f8592ccd2ea688341ca01 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Mon, 29 Jul 2019 15:55:12 -0700 Subject: [PATCH 08/13] fix: always prefer network --- .../src/utils/__tests__/get-latest-apis.js | 47 ++++++++++++++----- packages/gatsby/src/utils/get-latest-apis.js | 10 ++-- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/packages/gatsby/src/utils/__tests__/get-latest-apis.js b/packages/gatsby/src/utils/__tests__/get-latest-apis.js index 0ab663d4639f0..31a78d9285c1c 100644 --- a/packages/gatsby/src/utils/__tests__/get-latest-apis.js +++ b/packages/gatsby/src/utils/__tests__/get-latest-apis.js @@ -16,7 +16,7 @@ const getLatestAPIs = require(`../get-latest-apis`) beforeEach(() => { ;[fs, axios].forEach(mock => - Object.keys(mock).forEach(key => mock[key].mockClear()) + Object.keys(mock).forEach(key => mock[key].mockReset()) ) }) @@ -28,18 +28,7 @@ const getMockAPIFile = () => { } } -it(`defaults to cached file, if it exists`, async () => { - const apis = getMockAPIFile() - fs.exists.mockResolvedValueOnce(true) - fs.readJSON.mockResolvedValueOnce(apis) - - const data = await getLatestAPIs() - - expect(fs.writeFile).not.toHaveBeenCalled() - expect(data).toEqual(apis) -}) - -describe(`API file not cached`, () => { +describe(`default behavior: has network connectivity`, () => { beforeEach(() => { fs.exists.mockResolvedValueOnce(false) axios.get.mockResolvedValueOnce({ data: getMockAPIFile() }) @@ -62,3 +51,35 @@ describe(`API file not cached`, () => { ) }) }) + +describe(`downloading APIs failure`, () => { + beforeEach(() => { + axios.get.mockRejectedValueOnce(new Error(`does not matter`)) + }) + + it(`falls back to downloaded cached file, if it exists`, async () => { + const apis = getMockAPIFile() + fs.exists.mockResolvedValueOnce(true) + fs.readJSON.mockResolvedValueOnce(apis) + + const data = await getLatestAPIs() + + expect(fs.writeFile).not.toHaveBeenCalled() + expect(fs.readJSON).toHaveBeenCalledWith( + expect.stringContaining(`/latest-apis.json`) + ) + expect(data).toEqual(apis) + }) + + it(`falls back to local api.json if latest-apis.json not cached`, async () => { + const apis = getMockAPIFile() + fs.exists.mockResolvedValueOnce(false) + fs.readJSON.mockResolvedValueOnce(apis) + + await getLatestAPIs() + + expect(fs.readJSON).toHaveBeenCalledWith( + expect.stringContaining(`/apis.json`) + ) + }) +}) diff --git a/packages/gatsby/src/utils/get-latest-apis.js b/packages/gatsby/src/utils/get-latest-apis.js index 74267c85f25fc..3f3b1aa51f3f9 100644 --- a/packages/gatsby/src/utils/get-latest-apis.js +++ b/packages/gatsby/src/utils/get-latest-apis.js @@ -7,13 +7,6 @@ const ROOT = path.join(__dirname, `..`, `..`) const OUTPUT_FILE = path.join(ROOT, `latest-apis.json`) module.exports = async function getLatestAPI() { - /* - * Happy path `postinstall` script created the file - */ - if (await fs.exists(OUTPUT_FILE)) { - return fs.readJSON(OUTPUT_FILE) - } - try { const { data } = await axios.get(API_FILE) @@ -21,6 +14,9 @@ module.exports = async function getLatestAPI() { return data } catch (e) { + if (await fs.exists(OUTPUT_FILE)) { + return fs.readJSON(OUTPUT_FILE) + } // possible offline/network issue return fs.readJSON(path.join(ROOT, `apis.json`)).catch(() => { return { From 4c8a7d1f615fc9747e630d710eb155198e529d08 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Mon, 29 Jul 2019 16:04:00 -0700 Subject: [PATCH 09/13] chore: reset changes --- .../reporters/ink/components/error.js | 85 ++++++------------- 1 file changed, 24 insertions(+), 61 deletions(-) diff --git a/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js b/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js index e2a27a1c4f37d..769a3a9ca250d 100644 --- a/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js +++ b/packages/gatsby-cli/src/reporter/reporters/ink/components/error.js @@ -3,40 +3,6 @@ import path from "path" import { Color, Box } from "ink" import { get } from "lodash" -const colors = { - WARNING: { - bg: { bgYellow: true, black: true }, - fg: { yellow: true }, - }, - ERROR: { - bg: { - bgRed: true, - black: true, - }, - fg: { - red: true, - }, - }, - INFO: { - bg: { - bgBlue: true, - white: true, - }, - fg: { - blue: true, - }, - }, - DEBUG: { - bg: { - bgCyan: true, - black: true, - }, - fg: { - cyan: true, - }, - }, -} - const File = ({ filePath, location }) => { const lineNumber = get(location, `start.line`) @@ -68,34 +34,32 @@ const DocsLink = ({ docsUrl }) => { ) } -const Error = ({ details }) => { - const color = colors[details.level] || colors.error - return ( - +const Error = ({ details }) => ( + // const stackLength = get(details, `stack.length`, 0 + + + - - - - - {` ${details.level} `} - {details.id ? `#${details.id} ` : ``} - - - {details.type ? ` ` + details.type : ``} - - + + + + {` ${details.level} `} + {details.id ? `#${details.id} ` : ``} + + {details.type ? ` ` + details.type : ``} - {details.text} - {details.filePath && ( - - File:{` `} - - - )} - + {details.text} + {details.filePath && ( + + File:{` `} + + + )} - {/* TODO: use this to replace errorFormatter.render in reporter.error func + + + {/* TODO: use this to replace errorFormatter.render in reporter.error func {stackLength > 0 && ( @@ -110,8 +74,7 @@ const Error = ({ details }) => { )} */} - - ) -} + +) export default Error From 3cbd0f062a49dd69704cc40d9f5c141ec23d8921 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Mon, 29 Jul 2019 16:13:58 -0700 Subject: [PATCH 10/13] test: fix unit tests --- .../load-plugins/__tests__/validate.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js index a445a0d66f91a..98f15da06f91d 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js @@ -1,6 +1,7 @@ jest.mock(`gatsby-cli/lib/reporter`, () => { return { panicOnBuild: jest.fn(), + error: jest.fn(), warn: jest.fn(), } }) @@ -55,7 +56,7 @@ describe(`collatePluginAPIs`, () => { }, ] - let result = collatePluginAPIs({ apis, flattenedPlugins }) + let result = collatePluginAPIs({ currentAPIs: apis, flattenedPlugins }) expect(result).toMatchSnapshot() }) @@ -82,7 +83,7 @@ describe(`collatePluginAPIs`, () => { }, ] - let result = collatePluginAPIs({ apis, flattenedPlugins }) + let result = collatePluginAPIs({ currentAPIs: apis, flattenedPlugins }) expect(result).toMatchSnapshot() }) }) @@ -90,7 +91,7 @@ describe(`collatePluginAPIs`, () => { describe(`handleBadExports`, () => { it(`Does nothing when there are no bad exports`, async () => { handleBadExports({ - apis: { + currentAPIs: { node: [`these`, `can`, `be`], browser: [`anything`, `as there`], ssr: [`are no`, `bad errors`], @@ -103,9 +104,14 @@ describe(`handleBadExports`, () => { }) }) - it(`Calls reporter.panicOnBuild when bad exports are detected`, async () => { + it(`Calls reporter.error when bad exports are detected`, async () => { handleBadExports({ - apis: { + currentAPIs: { + node: [``], + browser: [``], + ssr: [`notFoo`, `bar`], + }, + latestAPIs: { node: [``], browser: [``], ssr: [`notFoo`, `bar`], @@ -122,7 +128,12 @@ describe(`handleBadExports`, () => { }, }) - expect(reporter.panicOnBuild.mock.calls.length).toBe(1) + expect(reporter.error).toHaveBeenCalledTimes(1) + expect(reporter.error).toHaveBeenCalledWith( + expect.objectContaining({ + id: `11329`, + }) + ) }) }) From bf55206750b3103ccb81b6ae8bb417d770c275fa Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Tue, 30 Jul 2019 13:37:20 -0700 Subject: [PATCH 11/13] test: add a few more tests --- .../load-plugins/__tests__/validate.js | 82 +++++++++++++++++++ .../src/bootstrap/load-plugins/validate.js | 3 - 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js index 98f15da06f91d..4b35aacbd1e61 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js @@ -15,6 +15,10 @@ const { warnOnIncompatiblePeerDependency, } = require(`../validate`) +beforeEach(() => { + Object.keys(reporter).forEach(key => reporter[key].mockReset()) +}) + describe(`collatePluginAPIs`, () => { const MOCK_RESULTS = { "/foo/gatsby-node": [`node-1`, `node-2`], @@ -135,6 +139,84 @@ describe(`handleBadExports`, () => { }) ) }) + + it(`Adds fixes to context if newer API introduced in Gatsby`, () => { + const version = `2.2.0` + + handleBadExports({ + currentAPIs: { + node: [``], + browser: [``], + ssr: [``], + }, + latestAPIs: { + browser: {}, + ssr: {}, + node: { + validatePluginOptions: { + version, + }, + }, + }, + badExports: { + browser: [], + ssr: [], + node: [ + { + exportName: `validatePluginOptions`, + pluginName: `gatsby-source-contentful`, + }, + ], + }, + }) + + expect(reporter.error).toHaveBeenCalledTimes(1) + expect(reporter.error).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + fixes: [`npm install gatsby@^${version}`], + }), + }) + ) + }) + + it(`adds fixes if close match/typo`, () => { + ;[ + [`modifyWebpackConfig`, `onCreateWebpackConfig`], + [`createPagesss`, `createPages`], + ].forEach(([typoOrOldAPI, newAPI]) => { + handleBadExports({ + currentAPIs: { + node: [newAPI], + browser: [``], + ssr: [``], + }, + latestAPIs: { + browser: {}, + ssr: {}, + node: {}, + }, + badExports: { + browser: [], + ssr: [], + node: [ + { + exportName: typoOrOldAPI, + pluginName: `default-site-plugin`, + }, + ], + }, + }) + + expect(reporter.error).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + fixes: [`Rename "${typoOrOldAPI}" -> "${newAPI}"`], + }), + }) + ) + }) + }) }) describe(`handleMultipleReplaceRenderers`, () => { diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.js index dd26e56bbb7ab..a4a4192aa054f 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.js @@ -33,9 +33,6 @@ const getBadExports = (plugin, pluginAPIKeys, apis) => { } const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { - let capitalized = `${exportType[0].toUpperCase()}${exportType.slice(1)}` - if (capitalized === `Ssr`) capitalized = `SSR` - const entries = badExports.map(ex => { return { ...ex, From d4aa82ddaf82d5016d94d5db7b7cd175d0a3360c Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Tue, 30 Jul 2019 13:45:35 -0700 Subject: [PATCH 12/13] test: add some more tests --- .../load-plugins/__tests__/validate.js | 60 +++++++++++++++++-- .../src/bootstrap/load-plugins/validate.js | 2 +- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js index 4b35aacbd1e61..886fb3ac0b3e5 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js @@ -108,24 +108,25 @@ describe(`handleBadExports`, () => { }) }) - it(`Calls reporter.error when bad exports are detected`, async () => { + it(`Calls structured error with reporter.error when bad exports are detected`, async () => { + const exportName = `foo` handleBadExports({ currentAPIs: { node: [``], browser: [``], - ssr: [`notFoo`, `bar`], + ssr: [``], }, latestAPIs: { - node: [``], - browser: [``], - ssr: [`notFoo`, `bar`], + node: {}, + browser: {}, + ssr: {}, }, badExports: { node: [], browser: [], ssr: [ { - exportName: `foo`, + exportName, pluginName: `default-site-plugin`, }, ], @@ -136,6 +137,53 @@ describe(`handleBadExports`, () => { expect(reporter.error).toHaveBeenCalledWith( expect.objectContaining({ id: `11329`, + context: expect.objectContaining({ + exportType: `ssr`, + errors: [ + expect.stringContaining(`"${exportName}" which is not a known API`), + ], + }), + }) + ) + }) + + it(`adds info on plugin if a plugin API error`, () => { + const exportName = `foo` + const pluginName = `gatsby-source-contentful` + const pluginVersion = `2.1.0` + handleBadExports({ + currentAPIs: { + node: [``], + browser: [``], + ssr: [``], + }, + latestAPIs: { + node: {}, + browser: {}, + ssr: {}, + }, + badExports: { + node: [], + browser: [], + ssr: [ + { + exportName, + pluginName, + pluginVersion, + }, + ], + }, + }) + + expect(reporter.error).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + errors: [ + expect.stringContaining( + `${pluginName}@${pluginVersion} is using the API "${exportName}"` + ), + ], + }), }) ) }) diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.js index a4a4192aa054f..ab3eb6cae9bd6 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.js +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.js @@ -67,7 +67,7 @@ const getErrorContext = (badExports, exportType, currentAPIs, latestAPIs) => { ) } else { errors.push( - `- The plugin "${entry.pluginName}@${ + `- The plugin ${entry.pluginName}@${ entry.pluginVersion } is using the API "${entry.exportName}" which ${message}.` ) From 59bf7a38de1e8296dd8e60b9d07f1e475c1bf9e9 Mon Sep 17 00:00:00 2001 From: Dustin Schau Date: Tue, 30 Jul 2019 17:04:22 -0700 Subject: [PATCH 13/13] test: fix windows for being a bozo --- packages/gatsby/src/utils/__tests__/get-latest-apis.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/utils/__tests__/get-latest-apis.js b/packages/gatsby/src/utils/__tests__/get-latest-apis.js index 31a78d9285c1c..ea9ef297f0420 100644 --- a/packages/gatsby/src/utils/__tests__/get-latest-apis.js +++ b/packages/gatsby/src/utils/__tests__/get-latest-apis.js @@ -10,6 +10,7 @@ jest.mock(`axios`, () => { get: jest.fn(), } }) +const path = require(`path`) const fs = require(`fs-extra`) const axios = require(`axios`) const getLatestAPIs = require(`../get-latest-apis`) @@ -66,7 +67,7 @@ describe(`downloading APIs failure`, () => { expect(fs.writeFile).not.toHaveBeenCalled() expect(fs.readJSON).toHaveBeenCalledWith( - expect.stringContaining(`/latest-apis.json`) + expect.stringContaining(`${path.sep}latest-apis.json`) ) expect(data).toEqual(apis) }) @@ -79,7 +80,7 @@ describe(`downloading APIs failure`, () => { await getLatestAPIs() expect(fs.readJSON).toHaveBeenCalledWith( - expect.stringContaining(`/apis.json`) + expect.stringContaining(`${path.sep}apis.json`) ) }) })