@netlify/content-engine
Version:
348 lines • 16.3 kB
JavaScript
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
;