"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

exports.__esModule = true;
exports.handleBadExports = handleBadExports;
exports.validateConfigPluginsOptions = validateConfigPluginsOptions;
exports.collatePluginAPIs = collatePluginAPIs;
exports.warnOnIncompatiblePeerDependency = warnOnIncompatiblePeerDependency;
exports.handleMultipleReplaceRenderers = void 0;

var _lodash = _interopRequireDefault(require("lodash"));

var _path = _interopRequireDefault(require("path"));

var semver = _interopRequireWildcard(require("semver"));

var stringSimilarity = _interopRequireWildcard(require("string-similarity"));

var _package = require("gatsby/package.json");

var _reporter = _interopRequireDefault(require("gatsby-cli/lib/reporter"));

var _gatsbyPluginUtils = require("gatsby-plugin-utils");

var _commonTags = require("common-tags");

var _gatsbyTelemetry = require("gatsby-telemetry");

var _gatsbyWorker = require("gatsby-worker");

var _resolveModuleExports = require("../resolve-module-exports");

var _getLatestApis = require("../../utils/get-latest-apis");

var _resolvePlugin = require("./resolve-plugin");

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

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.


function getBadExports(plugin, pluginAPIKeys, apis) {
  let badExports = []; // Discover any exports from plugins which are not "known"

  badExports = badExports.concat(_lodash.default.difference(pluginAPIKeys, apis).map(e => {
    return {
      exportName: e,
      pluginName: plugin.name,
      pluginVersion: plugin.version
    };
  }));
  return badExports;
}

function getErrorContext(badExports, exportType, currentAPIs, latestAPIs) {
  const entries = badExports.map(ex => {
    return { ...ex,
      api: latestAPIs[exportType][ex.exportName]
    };
  });
  const gatsbyUpgradeVersion = getGatsbyUpgradeVersion(entries);
  const errors = [];
  const fixes = gatsbyUpgradeVersion ? [`npm install gatsby@^${gatsbyUpgradeVersion}`] : [];
  entries.forEach(entry => {
    const similarities = stringSimilarity.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` : `is not a known API`;

    if (isDefaultPlugin) {
      errors.push(`- Your local gatsby-${exportType}.js is using the API "${entry.exportName}" which ${message}.`);
    } else {
      errors.push(`- The plugin ${entry.pluginName}@${entry.pluginVersion} is using the API "${entry.exportName}" which ${message}.`);
    }

    if (similarities.bestMatch.rating > 0.5) {
      fixes.push(`Rename "${entry.exportName}" -> "${similarities.bestMatch.target}"`);
    }
  });
  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`)
  };
}

async function handleBadExports({
  currentAPIs,
  badExports
}) {
  const hasBadExports = Object.keys(badExports).find(api => badExports[api].length > 0);

  if (hasBadExports) {
    const latestAPIs = await (0, _getLatestApis.getLatestAPIs)(); // Output error messages for all bad exports

    _lodash.default.toPairs(badExports).forEach(badItem => {
      const [exportType, entries] = badItem;

      if (entries.length > 0) {
        const context = getErrorContext(entries, exportType, currentAPIs, latestAPIs);

        _reporter.default.error({
          id: `11329`,
          context
        });
      }
    });
  }
}

async function validatePluginsOptions(plugins, rootDir) {
  let errors = 0;
  const newPlugins = await Promise.all(plugins.map(async plugin => {
    let gatsbyNode;

    try {
      const resolvedPlugin = (0, _resolvePlugin.resolvePlugin)(plugin, rootDir);
      gatsbyNode = require(`${resolvedPlugin.resolve}/gatsby-node`);
    } catch (err) {
      gatsbyNode = {};
    }

    if (!gatsbyNode.pluginOptionsSchema) return plugin;
    const subPluginPaths = new Set();
    let optionsSchema = gatsbyNode.pluginOptionsSchema({
      Joi: _gatsbyPluginUtils.Joi.extend(joi => {
        return {
          type: `subPlugins`,
          base: joi.array().items(joi.alternatives(joi.string(), joi.object({
            resolve: _gatsbyPluginUtils.Joi.string(),
            options: _gatsbyPluginUtils.Joi.object({}).unknown(true)
          }))).custom((arrayValue, helpers) => {
            const entry = helpers.schema._flags.entry;
            return arrayValue.map(value => {
              if (typeof value === `string`) {
                value = {
                  resolve: value
                };
              }

              try {
                const resolvedPlugin = (0, _resolvePlugin.resolvePlugin)(value, rootDir);

                const modulePath = require.resolve(`${resolvedPlugin.resolve}${entry ? `/${entry}` : ``}`);

                value.modulePath = modulePath;
                value.module = require(modulePath);
                const normalizedPath = helpers.state.path.map((key, index) => {
                  // if subplugin is part of an array - swap concrete index key with `[]`
                  if (typeof key === `number` && Array.isArray(helpers.state.ancestors[helpers.state.path.length - index - 1])) {
                    if (index !== helpers.state.path.length - 1) {
                      throw new Error(`No support for arrays not at the end of path`);
                    }

                    return `[]`;
                  }

                  return key;
                }).join(`.`);
                subPluginPaths.add(normalizedPath);
              } catch (err) {
                console.log(err);
              }

              return value;
            });
          }, `Gatsby specific subplugin validation`).default([]),
          args: (schema, args) => {
            if (args !== null && args !== void 0 && args.entry && schema && typeof schema === `object` && schema.$_setFlag) {
              return schema.$_setFlag(`entry`, args.entry, {
                clone: true
              });
            }

            return schema;
          }
        };
      })
    }); // If rootDir and plugin.parentDir are the same, i.e. if this is a plugin a user configured in their gatsby-config.js (and not a sub-theme that added it), this will be ""
    // Otherwise, this will contain (and show) the relative path

    const configDir = plugin.parentDir && rootDir && _path.default.relative(rootDir, plugin.parentDir) || null;

    if (!_gatsbyPluginUtils.Joi.isSchema(optionsSchema) || optionsSchema.type !== `object`) {
      // Validate correct usage of pluginOptionsSchema
      _reporter.default.warn(`Plugin "${plugin.resolve}" has an invalid options schema so we cannot verify your configuration for it.`);

      return plugin;
    }

    try {
      var _plugin$options;

      if (!optionsSchema.describe().keys.plugins) {
        // All plugins have "plugins: []"" added to their options in load.ts, even if they
        // do not have subplugins. We add plugins to the schema if it does not exist already
        // to make sure they pass validation.
        optionsSchema = optionsSchema.append({
          plugins: _gatsbyPluginUtils.Joi.array().length(0)
        });
      }

      const {
        value,
        warning
      } = await (0, _gatsbyPluginUtils.validateOptionsSchema)(optionsSchema, plugin.options || {});
      plugin.options = value; // Handle unknown key warnings

      const validationWarnings = warning === null || warning === void 0 ? void 0 : warning.details;

      if ((validationWarnings === null || validationWarnings === void 0 ? void 0 : validationWarnings.length) > 0) {
        _reporter.default.warn((0, _commonTags.stripIndent)(`
        Warning: there are unknown plugin options for "${plugin.resolve}"${configDir ? `, configured by ${configDir}` : ``}: ${validationWarnings.map(error => error.path.join(`.`)).join(`, `)}
        Please open an issue at https://ghub.io/${plugin.resolve} if you believe this option is valid.
      `));

        (0, _gatsbyTelemetry.trackCli)(`UNKNOWN_PLUGIN_OPTION`, {
          name: plugin.resolve,
          valueString: validationWarnings.map(error => error.path.join(`.`)).join(`, `)
        }); // We do not increment errors++ here as we do not want to process.exit if there are only warnings
      } // Validate subplugins


      if ((_plugin$options = plugin.options) !== null && _plugin$options !== void 0 && _plugin$options.plugins) {
        const {
          errors: subErrors,
          plugins: subPlugins
        } = await validatePluginsOptions(plugin.options.plugins, rootDir);
        plugin.options.plugins = subPlugins;

        if (subPlugins.length > 0) {
          subPluginPaths.add(`plugins`);
        }

        errors += subErrors;
      }

      if (subPluginPaths.size > 0) {
        plugin.subPluginPaths = Array.from(subPluginPaths);
      }
    } catch (error) {
      if (error instanceof _gatsbyPluginUtils.Joi.ValidationError) {
        const validationErrors = error.details;

        if (validationErrors.length > 0) {
          _reporter.default.error({
            id: `11331`,
            context: {
              configDir,
              validationErrors,
              pluginName: plugin.resolve
            }
          });

          errors++;
        }

        return plugin;
      }

      throw error;
    }

    return plugin;
  }));
  return {
    errors,
    plugins: newPlugins
  };
}

async function validateConfigPluginsOptions(config = {}, rootDir) {
  if (!config.plugins) return;
  const {
    errors,
    plugins
  } = await validatePluginsOptions(config.plugins, rootDir);
  config.plugins = plugins;

  if (errors > 0) {
    process.exit(1);
  }
}
/**
 * Identify which APIs each plugin exports
 */


function collatePluginAPIs({
  currentAPIs,
  flattenedPlugins
}) {
  // Get a list of bad exports
  const badExports = {
    node: [],
    browser: [],
    ssr: []
  };
  flattenedPlugins.forEach(plugin => {
    plugin.nodeAPIs = [];
    plugin.browserAPIs = [];
    plugin.ssrAPIs = []; // Discover which APIs this plugin implements and store an array against
    // the plugin node itself *and* in an API to plugins map for faster lookups
    // later.

    const pluginNodeExports = (0, _resolveModuleExports.resolveModuleExports)(`${(plugin === null || plugin === void 0 ? void 0 : plugin.resolveCompiled) || plugin.resolve}/gatsby-node`, {
      mode: `require`
    });
    const pluginBrowserExports = (0, _resolveModuleExports.resolveModuleExports)(`${plugin.resolve}/gatsby-browser`);
    const pluginSSRExports = (0, _resolveModuleExports.resolveModuleExports)(`${plugin.resolve}/gatsby-ssr`);

    if (pluginNodeExports.length > 0) {
      plugin.nodeAPIs = _lodash.default.intersection(pluginNodeExports, currentAPIs.node);
      badExports.node = badExports.node.concat(getBadExports(plugin, pluginNodeExports, currentAPIs.node)); // Collate any bad exports
    }

    if (pluginBrowserExports.length > 0) {
      plugin.browserAPIs = _lodash.default.intersection(pluginBrowserExports, currentAPIs.browser);
      badExports.browser = badExports.browser.concat(getBadExports(plugin, pluginBrowserExports, currentAPIs.browser)); // Collate any bad exports
    }

    if (pluginSSRExports.length > 0) {
      plugin.ssrAPIs = _lodash.default.intersection(pluginSSRExports, currentAPIs.ssr);
      badExports.ssr = badExports.ssr.concat(getBadExports(plugin, pluginSSRExports, currentAPIs.ssr)); // Collate any bad exports
    }
  });
  return {
    flattenedPlugins: flattenedPlugins,
    badExports
  };
}

const handleMultipleReplaceRenderers = ({
  flattenedPlugins
}) => {
  // multiple replaceRenderers may cause problems at build time
  const rendererPlugins = flattenedPlugins.filter(plugin => plugin.ssrAPIs.includes(`replaceRenderer`)).map(plugin => plugin.name);

  if (rendererPlugins.length > 1) {
    if (rendererPlugins.includes(`default-site-plugin`)) {
      _reporter.default.warn(`replaceRenderer API found in these plugins:`);

      _reporter.default.warn(rendererPlugins.join(`, `));

      _reporter.default.warn(`This might be an error, see: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`);
    } else {
      console.log(``);

      _reporter.default.error(`Gatsby's replaceRenderer API is implemented by multiple plugins:`);

      _reporter.default.error(rendererPlugins.join(`, `));

      _reporter.default.error(`This will break your build`);

      _reporter.default.error(`See: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`);

      if (process.env.NODE_ENV === `production`) process.exit(1);
    } // Now update plugin list so only final replaceRenderer will run


    const ignorable = rendererPlugins.slice(0, -1); // For each plugin in ignorable, set a skipSSR flag to true
    // This prevents apiRunnerSSR() from attempting to run it later

    const messages = [];
    flattenedPlugins.forEach((fp, i) => {
      if (ignorable.includes(fp.name)) {
        messages.push(`Duplicate replaceRenderer found, skipping gatsby-ssr.js for plugin: ${fp.name}`);
        flattenedPlugins[i].skipSSR = true;
      }
    });

    if (messages.length > 0) {
      console.log(``);
      messages.forEach(m => _reporter.default.warn(m));
      console.log(``);
    }
  }

  return flattenedPlugins;
};

exports.handleMultipleReplaceRenderers = handleMultipleReplaceRenderers;

function warnOnIncompatiblePeerDependency(name, packageJSON) {
  // Note: In the future the peer dependency should be enforced for all plugins.
  const gatsbyPeerDependency = _lodash.default.get(packageJSON, `peerDependencies.gatsby`);

  if (!_gatsbyWorker.isWorker && gatsbyPeerDependency && !semver.satisfies(_package.version, gatsbyPeerDependency, {
    includePrerelease: true
  })) {
    _reporter.default.warn(`Plugin ${name} is not compatible with your gatsby version ${_package.version} - It requires gatsby@${gatsbyPeerDependency}`);
  }
}
//# sourceMappingURL=validate.js.map