Skip to content

Commit

Permalink
Merge pull request #477 from chris48s/wip-plugins
Browse files Browse the repository at this point in the history
plugin interface: internals
  • Loading branch information
chris48s authored Aug 4, 2024
2 parents 761f595 + 1cd9bc0 commit 900952a
Show file tree
Hide file tree
Showing 25 changed files with 733 additions and 230 deletions.
3 changes: 2 additions & 1 deletion .c8rc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
],
"exclude": [
"**/*.spec.js",
"src/test-helpers.js"
"src/test-helpers.js",
"testfiles/plugins/**"
]
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export GLOBAL_AGENT_HTTP_PROXY=http://myproxy:8888
* v8r always exits with code `97` when:
* There was an error loading a config file
* A config file was loaded but failed validation
* There was an error loading a plugin
* A plugin file was loaded but failed validation

* v8r always exits with code `98` when:
* An input glob pattern was invalid
Expand Down
18 changes: 4 additions & 14 deletions config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,17 @@
"type": "string"
},
"parser": {
"description": "A custom parser to use for files matching fileMatch instead of trying to infer the correct parser from the filename",
"type": "string",
"enum": [
"json",
"json5",
"toml",
"yaml"
]
"description": "A custom parser to use for files matching fileMatch instead of trying to infer the correct parser from the filename. 'json', 'json5', 'toml' and 'yaml' are always valid. Plugins may define additional values which are valid here.",
"type": "string"
}
}
}
}
}
},
"format": {
"description": "Output format for validation results",
"type": "string",
"enum": [
"text",
"json"
]
"description": "Output format for validation results. 'text' and 'json' are always valid. Plugins may define additional values which are valid here.",
"type": "string"
},
"ignoreErrors": {
"description": "Exit with code 0 even if an error was encountered. True means a non-zero exit code is only issued if validation could be completed successfully and one or more files were invalid",
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"bin": {
"v8r": "src/index.js"
},
"main": "src/index.js",
"exports": "./src/index.js",
"exports": "./src/public.js",
"files": [
"src/**/!(*.spec).js",
"config-schema.json",
Expand Down
100 changes: 73 additions & 27 deletions src/config.js → src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,19 @@ import { createRequire } from "module";
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
const require = createRequire(import.meta.url);

import Ajv2019 from "ajv/dist/2019.js";
import { cosmiconfig } from "cosmiconfig";
import decamelize from "decamelize";
import isUrl from "is-url";
import path from "path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
validateConfigAgainstSchema,
validateConfigDocumentParsers,
validateConfigOutputFormats,
} from "./config-validators.js";
import logger from "./logger.js";
import { logErrors } from "./output-formatters.js";

function validateConfig(configFile) {
const ajv = new Ajv2019({ allErrors: true, strict: false });
const schema = require("../config-schema.json");
const validateFn = ajv.compile(schema);
const valid = validateFn(configFile.config);
if (!valid) {
logErrors(
configFile.filepath ? configFile.filepath : "",
validateFn.errors,
);
throw new Error("Malformed config file");
}
return valid;
}
import { loadAllPlugins } from "./plugins.js";

function preProcessConfig(configFile) {
if (!configFile?.config?.customCatalog?.schemas) {
Expand All @@ -52,8 +41,6 @@ async function getCosmiConfig(cosmiconfigOptions) {
} else {
logger.info(`No config file found`);
}
validateConfig(configFile);
preProcessConfig(configFile);
return configFile;
}

Expand All @@ -72,7 +59,7 @@ function getRelativeFilePath(config) {
return path.relative(process.cwd(), config.filepath);
}

function parseArgs(argv, config) {
function parseArgs(argv, config, documentFormats, outputFormats) {
const parser = yargs(hideBin(argv));

let command = "$0 <patterns..>";
Expand All @@ -91,7 +78,7 @@ function parseArgs(argv, config) {
parser
.command(
command,
"Validate local json/yaml files against schema(s)",
`Validate local ${documentFormats.join("/")} files against schema(s)`,
(yargs) => {
yargs.positional("patterns", patternsOpts);
},
Expand Down Expand Up @@ -142,7 +129,7 @@ function parseArgs(argv, config) {
})
.option("format", {
type: "string",
choices: ["text", "json"],
choices: outputFormats,
default: "text",
describe: "Output format for validation results",
})
Expand All @@ -168,10 +155,69 @@ function parseArgs(argv, config) {
return parser.argv;
}

async function getConfig(argv, cosmiconfigOptions = {}) {
const config = await getCosmiConfig(cosmiconfigOptions);
const args = parseArgs(argv, config);
return mergeConfigs(args, config);
function getDocumentFormats(loadedPlugins) {
let documentFormats = [];
for (const plugin of loadedPlugins) {
documentFormats = documentFormats.concat(plugin.registerDocumentFormats());
}
return documentFormats;
}

function getOutputFormats(loadedPlugins) {
let outputFormats = [];
for (const plugin of loadedPlugins) {
outputFormats = outputFormats.concat(plugin.registerOutputFormats());
}
return outputFormats;
}

async function bootstrap(argv, config, cosmiconfigOptions = {}) {
if (config) {
// special case for unit testing purposes
// this allows us to inject an incomplete config and bypass the validation
const { allLoadedPlugins, loadedCorePlugins, loadedUserPlugins } =
await loadAllPlugins(config.plugins || []);
return {
config,
allLoadedPlugins,
loadedCorePlugins,
loadedUserPlugins,
};
}

// load the config file and validate it against the schema
const configFile = await getCosmiConfig(cosmiconfigOptions);
validateConfigAgainstSchema(configFile);

// load both core and user plugins
// TODO: expand config file format to allow the user to supply an array of plugins here
const plugins = [];
const { allLoadedPlugins, loadedCorePlugins, loadedUserPlugins } =
await loadAllPlugins(plugins);
const documentFormats = getDocumentFormats(allLoadedPlugins);
const outputFormats = getOutputFormats(allLoadedPlugins);

// now we have documentFormats and outputFormats
// we can finish validating and processing the config
validateConfigDocumentParsers(configFile, documentFormats);
validateConfigOutputFormats(configFile, outputFormats);
preProcessConfig(configFile);

// parse command line arguments
const args = parseArgs(argv, configFile, documentFormats, outputFormats);

return {
config: mergeConfigs(args, configFile),
allLoadedPlugins,
loadedCorePlugins,
loadedUserPlugins,
};
}

export { getConfig, parseArgs, preProcessConfig, validateConfig };
export {
bootstrap,
getDocumentFormats,
getOutputFormats,
parseArgs,
preProcessConfig,
};
Loading

0 comments on commit 900952a

Please sign in to comment.