UNPKG

@netlify/content-engine

Version:
348 lines 16.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handleBadExports = handleBadExports; exports.validateConfigPluginsOptions = validateConfigPluginsOptions; exports.collatePluginAPIs = collatePluginAPIs; exports.warnOnIncompatiblePeerDependency = warnOnIncompatiblePeerDependency; const dynamic_import_1 = require("../../../lib/dynamic-import"); const node_fs_1 = __importDefault(require("node:fs")); const lodash_topairs_1 = __importDefault(require("lodash.topairs")); const lodash_difference_1 = __importDefault(require("lodash.difference")); const lodash_intersection_1 = __importDefault(require("lodash.intersection")); const lodash_get_1 = __importDefault(require("lodash.get")); const path_1 = __importDefault(require("path")); const semver = __importStar(require("semver")); const stringSimilarity = __importStar(require("string-similarity")); const reporter_1 = __importDefault(require("../../reporter")); const plugin_utils_1 = require("../../plugin-utils"); const common_tags_1 = require("common-tags"); const pkg_dir_1 = __importDefault(require("pkg-dir")); // import { trackCli } from "gatsby-telemetry" const resolve_module_exports_1 = require("../resolve-module-exports"); const get_latest_apis_1 = require("../../utils/get-latest-apis"); const resolve_plugin_1 = require("./resolve-plugin"); const prefer_default_1 = require("../prefer-default"); const import_gatsby_plugin_1 = require("../../utils/import-gatsby-plugin"); const resolve_js_file_path_1 = require("../resolve-js-file-path"); const { version: gatsbyVersion } = JSON.parse(node_fs_1.default.readFileSync(path_1.default.join(pkg_dir_1.default.sync(__dirname), "package.json"), "utf8")); 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((0, lodash_difference_1.default)(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, 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, get_latest_apis_1.getLatestAPIs)(); // Output error messages for all bad exports (0, lodash_topairs_1.default)(badExports).forEach((badItem) => { const [exportType, entries] = badItem; if (entries.length > 0) { const context = getErrorContext(entries, exportType, currentAPIs, latestAPIs); reporter_1.default.error({ id: `11329`, context, }); } }); } } const addModuleImportAndValidateOptions = (rootDir, incErrors) => async (value) => { for (const plugin of value) { if (plugin.modulePath) { const importedModule = await (0, dynamic_import_1.dynamicImport)((0, resolve_js_file_path_1.maybeAddFileProtocol)(plugin.modulePath)); const pluginModule = (0, prefer_default_1.preferDefault)(importedModule); plugin.module = pluginModule; } } const { errors: subErrors, plugins: subPlugins } = await validatePluginsOptions(value, rootDir); incErrors(subErrors); return subPlugins; }; async function validatePluginsOptions(plugins, rootDir) { let errors = 0; const newPlugins = await Promise.all(plugins.map(async (plugin) => { if ( // @ts-ignore plugin === `default-site-plugin` || plugin.resolve === `default-site-plugin`) { return plugin; } let gatsbyNode; try { const resolvedPlugin = (0, resolve_plugin_1.resolvePlugin)(plugin, rootDir); gatsbyNode = await (0, import_gatsby_plugin_1.importGatsbyPlugin)(resolvedPlugin, `gatsby-node`); } catch (err) { gatsbyNode = {}; } if (!gatsbyNode.pluginOptionsSchema) return plugin; const subPluginPaths = new Set(); let optionsSchema = gatsbyNode.pluginOptionsSchema({ Joi: plugin_utils_1.Joi.extend((joi) => { return { type: `subPlugins`, base: joi .array() .items(joi.alternatives(joi.string(), joi.object({ resolve: plugin_utils_1.Joi.string(), options: plugin_utils_1.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, resolve_plugin_1.resolvePlugin)(value, rootDir); const modulePath = require.resolve(`${resolvedPlugin.resolve}${entry ? `/${entry}` : ``}`); value.modulePath = 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) { reporter_1.default.error(err); } return value; }); }, `Gatsby specific subplugin validation`) .default([]) .external(addModuleImportAndValidateOptions(rootDir, (inc) => { errors += inc; }), `add module key to subplugin`), args: (schema, args) => { if (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_1.default.relative(rootDir, plugin.parentDir)) || null; if (!plugin_utils_1.Joi.isSchema(optionsSchema) || optionsSchema.type !== `object`) { // Validate correct usage of pluginOptionsSchema reporter_1.default.warn(`Plugin "${plugin.resolve}" has an invalid options schema so we cannot verify your configuration for it.`); return plugin; } try { 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: plugin_utils_1.Joi.array().length(0), }); } const { value, warning } = await (0, plugin_utils_1.validateOptionsSchema)(optionsSchema, plugin.options || {}); plugin.options = value; // Handle unknown key warnings const validationWarnings = warning?.details; if (validationWarnings?.length > 0) { reporter_1.default.warn((0, common_tags_1.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. `)); // 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 they weren't handled already if (!subPluginPaths.has(`plugins`) && 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 plugin_utils_1.Joi.ValidationError) { const validationErrors = error.details; if (validationErrors.length > 0) { reporter_1.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 */ async function collatePluginAPIs({ currentAPIs, flattenedPlugins, rootDir, }) { // Get a list of bad exports const badExports = { node: [], }; for (const plugin of flattenedPlugins) { plugin.nodeAPIs = []; // 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 = await (0, resolve_module_exports_1.resolveModuleExports)(plugin.resolvedCompiledGatsbyNode ?? `${plugin.resolve}/gatsby-node`, { rootDir, }); if (pluginNodeExports.length > 0) { plugin.nodeAPIs = (0, lodash_intersection_1.default)(pluginNodeExports, currentAPIs.node); badExports.node = badExports.node.concat(getBadExports(plugin, pluginNodeExports, currentAPIs.node)); // Collate any bad exports } } return { flattenedPlugins: flattenedPlugins, badExports, }; } function warnOnIncompatiblePeerDependency(name, packageJSON) { // Note: In the future the peer dependency should be enforced for all plugins. const gatsbyPeerDependency = (0, lodash_get_1.default)(packageJSON, [ "peerDependencies", "@netlify/content-engine", ]); if (gatsbyPeerDependency && !semver.satisfies(gatsbyVersion, gatsbyPeerDependency, { includePrerelease: true, })) { reporter_1.default.warn(`Plugin ${name} is not compatible with your gatsby version ${gatsbyVersion} - It requires @netlify/content-engine@${gatsbyPeerDependency}`); } } //# sourceMappingURL=validate.js.map