Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load user supplied plugins #487

Merged
merged 21 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/enforce-dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ jobs:
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
fail-on-scopes: runtime
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ docs/build/
# docs: generated files
docs/.docusaurus/
docs/.cache-loader/
docs/docs/plugins/reference.mdx
docs/.tempdocs/
2 changes: 2 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
plugins:
- "./node_modules/prettier-plugin-jsdoc/dist/index.js"
9 changes: 9 additions & 0 deletions config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@
"description": "Level of verbose logging. 0 is standard, higher numbers are more verbose",
"type": "integer",
"minimum": 0
},
"plugins": {
"type": "array",
"description": "An array of strings describing v8r plugins to load",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^(package:|file:)"
}
}
}
}
70 changes: 70 additions & 0 deletions docs/build-plugin-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";

function ensureDirExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}

const tempDocsDir = "./.tempdocs";
const referenceFile = "./docs/plugins/reference.mdx";
const tempFiles = [
{ filename: path.join(tempDocsDir, "BasePlugin.mdx"), title: "BasePlugin" },
{ filename: path.join(tempDocsDir, "Document.mdx"), title: "Document" },
{
filename: path.join(tempDocsDir, "ValidationResult.mdx"),
title: "ValidationResult",
},
];

// clear files if they already exist
if (fs.existsSync(tempDocsDir)) {
fs.rmSync(tempDocsDir, { recursive: true, force: true });
}
ensureDirExists(tempDocsDir);
if (fs.existsSync(referenceFile)) {
fs.unlinkSync(referenceFile);
}

// generate from JSDoc
execSync("npx jsdoc-to-mdx -o ./.tempdocs -j jsdoc.json");

// post-processing
let combinedContent = `---
sidebar_position: 3
custom_edit_url: null
---

# Plugin API Reference

v8r exports two classes: [BasePlugin](#BasePlugin) and [Document](#Document).
v8r plugins extend the [BasePlugin](#BasePlugin) class.
Parsing a file should return a [Document](#Document) object.
Additionally, validating a document yields a [ValidationResult](#ValidationResult) object.

`;
tempFiles.forEach((file) => {
if (fs.existsSync(file.filename)) {
let content = fs.readFileSync(file.filename, "utf8");
content = content
.replace(/##/g, "###")
.replace(
/---\ncustom_edit_url: null\n---/g,
`## ${file.title} {#${file.title}}`,
)
.replace(/<br \/>/g, " ")
.replace(/:---:/g, "---")
.replace(
/\[ValidationResult\]\(ValidationResult\)/g,
"[ValidationResult](#ValidationResult)",
)
.replace(/\[Document\]\(Document\)/g, "[Document](#Document)");
combinedContent += content;
}
});

// write out result
ensureDirExists(path.dirname(tempDocsDir));
fs.writeFileSync(referenceFile, combinedContent);
11 changes: 10 additions & 1 deletion docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ v8r only searches for a config file in the current working directory.

Example yaml config file:

```yaml
```yaml title=".v8rrc.yml"
# - One or more filenames or glob patterns describing local file or files to validate
# - overridden by passing one or more positional arguments
patterns: ['*json']
Expand Down Expand Up @@ -72,6 +72,15 @@ customCatalog:
# instead of trying to infer the correct parser from the filename (optional)
# This property is specific to custom catalogs defined in v8r config files
parser: json5

# - An array of v8r plugins to load
# - Plugins can only be specified in the config file.
# They can't be loaded using command line arguments
plugins:
# Plugins installed from NPM (or JSR) must be prefixed by "package:"
- "package:v8r-plugin-emoji-output"
# Plugins in the project dir must be prefixed by "file:"
- "file:./subdir/my-local-plugin.mjs"
```

The config file format is specified more formally in a JSON Schema:
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/faq.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# FAQ
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/plugins/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"label": "Plugins",
"position": 6,
"link": {
"type": "generated-index"
}
}
21 changes: 21 additions & 0 deletions docs/docs/plugins/using-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
sidebar_position: 1
---

# Using Plugins

It is possible to extend the functionality of v8r by installing plugins.

Plugins can be packages installed from a registry like [npm](https://www.npmjs.com/) or [jsr](https://jsr.io/) or local files in your repo.

Plugins must be specified in a [config file](../configuration.md). They can't be loaded using command line arguments.

```yaml title=".v8rrc.yml"
plugins:
# Plugins installed from NPM (or JSR) must be prefixed by "package:"
- "package:v8r-plugin-emoji-output"
# Plugins in the project dir must be prefixed by "file:"
- "file:./subdir/my-local-plugin.mjs"
```

Plugins are invoked one at a time in the order they are specified in your config file.
93 changes: 93 additions & 0 deletions docs/docs/plugins/writing-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
sidebar_position: 2
---

# Writing Plugins

We can extend the functionality of v8r by writing a plugin. A plugin can be a local file contained within your project or package published to a registry like [npm](https://www.npmjs.com/) or [jsr](https://jsr.io/).

Plugins extend the [BasePlugin](../reference) class which exposes hooks that allow us customise the parsing of files and output of results. Internally, v8r's [core parsers and output formats](https://github.com/chris48s/v8r/tree/main/src/plugins) are implemented as plugins. You can use these as a reference.

## Plugin Execution

Plugins are invoked in the following sequence:

Plugins that the user has specified in the config file are run first. These are executed in the order they are specified in the config file.

v8r's core plugins run second. The order of execution for core plugins is non-configurable.

## Hook Types

There are two patterns used by v8r plugin hooks.

### Register Hooks

- `registerInputFileParsers`
- `registerOutputFormats`

These hooks return an array of strings. Any values returned by these hooks are added to the list of formats v8r can work with.

### Early Return Hooks

- `parseInputFile`
- `getSingleResultLogMessage`
- `getAllResultsLogMessage`

These hooks may optionally return or not return a value. Each plugin is run in sequence. The first plugin that returns a value "wins". That value will be used and no further plugins will be invoked. If a hook doesn't return anything, v8r will move on to the next plugin in the stack.

## Worked Example

Lets build a simple example plugin. Our plugin is going to register an output format called "emoji". If the user passes `--format emoji` (or sets `format: emoji` in their config file) the plugin will output a 👍 when the file is valid and a 👎 when the file is invalid instead of the default text log message.

```js title="./plugins/v8r-plugin-emoji-output.js"
// Our plugin extends the BasePlugin class
import { BasePlugin } from "v8r";

class EmojiOutput extends BasePlugin {

// v8r plugins must declare a name starting with "v8r-plugin-"
static name = "v8r-plugin-emoji-output";

registerOutputFormats() {
/*
Registering "emoji" as an output format here adds "emoji" to the list
of values the user may pass to the --format argument.
We could register multiple output formats here if we want,
but we're just going to register one.
*/
return ["emoji"];
}

/*
We're going to implement the getSingleResultLogMessage hook here. This
allows us to optionally return a log message to be written to stdout
after each file is validated.
*/
getSingleResultLogMessage(result, filename, format) {
/*
Check if the user has requested "emoji" output.
If the user hasn't requested emoji output, we don't want to return a value.
That will allow v8r to hand over to the next plugin in the sequence
to check for other output formats.
*/
if (format === "emoji") {
// Implement our plugin logic
if (result.valid === true) {
return "👍";
}
return "👎";
}
}
}

// Our plugin must be an ESM module
// and the plugin class must be the default export
export default EmojiOutput;
```

We can now register the plugin in our config file:

```yaml title=".v8rrc.yml"
plugins:
- "file:./plugins/v8r-plugin-emoji-output.js"
```
3 changes: 2 additions & 1 deletion docs/docs/semver.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Versioning
Expand All @@ -10,6 +10,7 @@ v8r follows [semantic versioning](https://semver.org/). For this project, the "A
- CLI exit codes
- The configuration file format
- The native JSON output format
- The `BasePlugin` class, `Document` class, and `ValidationResult` type

A "breaking change" also includes:

Expand Down
9 changes: 5 additions & 4 deletions docs/docs/usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ $ v8r feature.geojson --schema https://json.schemastore.org/geojson

Using the `--schema` flag will validate all files matched by the glob pattern against that schema. You can also define a custom [schema catalog](https://json.schemastore.org/schema-catalog.json). v8r will search any custom catalogs before falling back to [Schema Store](https://www.schemastore.org/).

```bash
$ cat > my-catalog.json <<EOF
{ "\$schema": "https://json.schemastore.org/schema-catalog.json",

```js title="my-catalog.json"
{ "$schema": "https://json.schemastore.org/schema-catalog.json",
"version": 1,
"schemas": [ { "name": "geojson",
"description": "geojson",
"url": "https://json.schemastore.org/geojson.json",
"fileMatch": ["*.geojson"] } ] }
EOF
```

```bash
$ v8r feature.geojson -c my-catalog.json
ℹ Found schema in my-catalog.json ...
ℹ Validating feature.geojson against schema from https://json.schemastore.org/geojson ...
Expand Down
6 changes: 3 additions & 3 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { themes as prismThemes } from "prism-react-renderer";

/** @type {import('@docusaurus/types').Config} */
/** @type {import("@docusaurus/types").Config} */
const config = {
title: "v8r",
tagline: "A command-line validator that's on your wavelength",
Expand Down Expand Up @@ -39,7 +39,7 @@ const config = {
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
/** @type {import("@docusaurus/preset-classic").Options} */
({
docs: {
sidebarPath: "./sidebars.js",
Expand All @@ -55,7 +55,7 @@ const config = {
],

themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
/** @type {import("@docusaurus/preset-classic").ThemeConfig} */
({
navbar: {
title: "v8r",
Expand Down
5 changes: 5 additions & 0 deletions docs/jsdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": {
"include": ["../src/plugins.js"]
}
}
Loading
Loading