@hashicorp/platform-content-conformance
v0.0.12
Published
A system to execute checks against HashiCorp's various types of content. Very similar to [ESLint](https://eslint.org/), but tailored to checking content (MDX) and data (JSON, YAML) instead of JavaScript source.
Downloads
2,209
Maintainers
Keywords
Readme
@hashicorp/platform-content-conformance
A system to execute checks against HashiCorp's various types of content. Very similar to ESLint, but tailored to checking content (MDX) and data (JSON, YAML) instead of JavaScript source.
Installation
$ npm install @hashicorp/platform-content-conformance
Usage
CLI
$ hc-content [options] [path/to/file] [path/to/file]
Options:
--cwd
--config ./path/to/conformance.config.mjs
JavaScript
The package exposes its underlying functionality through the ContentConformanceRunner
class.
import { ContentConformanceRunner } from '@hashicorp/platform-content-conformance'
async function main() {
const runner = new ContentConformanceRunner()
await runner.init()
await runner.run()
console.log(runner.report())
}
main()
Options
cwd
(string) - Specify the working directory where the content checks will be run. Defaults toprocess.cwd()
.config
(string) - Specify a path to a custom configuration file. If not provided, defaults to{cwd}/content-conformance.config.mjs
.files
(string[]) - Specify a list of filenames to match against when running checks. Acts as a filter. If not specified, all files matching the patterns provided in your configuration will be checked.reporter
(string) - Specify the reporter type to use when callingrunner.report()
. Can betext
,json
, ormarkdown
. Defaults totext
.
Configuration
On its own, the content conformance system does not know where the files to be checked are located, or what rules should be used. To configure the checker, we create a content-conformance.config.mjs
file located in your project's root directory.
export default {
root: '.',
contentFileGlobPattern: 'content/**/*.mdx',
rules: {
'no-h1': 'error',
},
}
Configuring rules
Rules can be specified with differing severity to control the check's failure state. The valid values are: error
, warn
, or off
. By default, errors will cause a run to fail, while warnings will not. Rules can also be turned off. Individual rules may also accept a configuration object. This configuration object is made available via the rule context that is passed into the executor functions.
export default {
root: '.',
contentFileGlobPattern: 'content/**/*.mdx',
partialsDirectories: ['content/partials'],
rules: {
'with-config': [
'error',
{ message: 'this will be available as context.config.message' },
],
},
}
Presets
This package includes a number of configuration presets that can be used by specifying a preset
configuration property:
export default {
preset: 'base-mdx',
rules: {
'my-rule': 'off',
},
}
When using a preset, any properties defined in the consuming configuration will take precedence over the preset. Individual rule configuration from a preset is completely overwritten if present in the consuming configuration. In the above case, if base-mdx
contains configuration for the my-rule
rule, it would be replaced with off
.
Available presets can be found in the ./src/configs directory.
Rules
The core value of the conformance checker lies in the rules that you configure. The core package ships with a number of rules and presets that can be used, and custom rules can also be created in your project. A rule is a JavaScript module that exports a specific object format.
// ./rules/my-rule.js
/** @type {import('@hashicorp/platform-content-conformance').Rule} */
export default {
id: 'my-rule',
type: 'content',
description: 'This rule performs an important check.',
executor: {
async contentFile(file, context) {},
async dataFile(file, context) {},
},
}
Rule Properties
id
(string) A unique identifier for your rule. Convention is to use the ID as the rule's filename.type
(string) The type of rule. Can becontent
,data
, orstructure
.description
(string) A short description of what the rule does.executor
(object) An object containing file "executor" functions. These functions contain the logic for the rule.contentFile
(function) Called when aContentFile
is visited.dataFile
(function) Called when aDataFile
is visited.
Writing an executor
A rule executor contains the logic for the conformance rule. Each executor type is run once for each file of the provided type
(content
, or data
), or once for the entire project if the type is structure
.
Each executor has access to two arguments:
file
(ContentFile
|DataFile
) The file being checked by the rule.context
(object) An object containing global information and areport()
method (seeConformanceRuleContext
in types.ts).
context.report()
An executor should contain logic related to the rule to determine if the given file is in violation. If a violation is detected, the executor should call context.report()
:
{
async contentFile(file, context) {
if (hasViolation) {
context.report('This rule was broken', file)
}
}
}
The context.report()
method accepts three arguments:
message
(string) The message that will be used in generated output.file
(object) The file that the report is associated with.node
(object) The node that the violation is associated with. Allows positional information to be displayed in the report. (optional)
Note An individual rule does not have a concept of severity in its implementation. That is handled in the configuration file.
ContentFile
visitors
When defining a contentFile
executor, the file
argument exposes a visit()
method. This allows the rule to interact with the Abstract Syntax Tree (AST) generated from the source file. It allows checks to be written against specific parts of a content file with minimal boilerplate or extra processing. Usage of file.visit()
is very similar to unist-util-visit
.
{
async contentFile(file, context) {
file.visit(['link'], node => {
const hasViolation = node.url.includes('example.com')
if (hasViolation) {
context.report(`Link to example.com detected: ${node.url}`, file, node)
}
})
}
}
ContentFile
frontmatter
The frontmatter of a content file can be accessed using the file.frontmatter()
method:
{
async contentFile(file, context) {
console.log(file.frontmatter())
}
}
Internally, the underlying ContentFile
class ensures that the source file is only parsed into its AST representation once.
DataFile
contents
To access the contents of a data file, use the .contents()
method:
{
async dataFile(file, context) {
console.log(file.contents())
}
}
Using remark-lint
rules
remark-lint rules are supported by default. To use a remark-lint
rule, add it to your configuration and ensure the package is installed as a development dependency in your project.
export default {
root: '.',
contentFileGlobPattern: 'content/**/*.mdx',
rules: {
'remark-lint-definition-case': 'error',
},
}
Rule Messages
Writing meaningful, actionable rule messages is important to ensure contributors can address issues on their own. Messages passed to context.report()
should be written in an active voice. For example:
❌ 'Expected frontmatter to contain: description'
✅ 'Document does not have a `description` key in its frontmatter. Add a `description` key at the top of the document.'
Internals
At its core, the content conformance checker handles loading a number of different file types and running rules against those files. Rule violations are tracked by file, and after execution rules can be passed to a reporter method to present the results.
Files that are eligible to be checked are represented as VFile
[https://github.com/vfile/vfile] instances. VFile
has a notion of messages attached to a file, along with a number of custom reporters to format and output a file's messages. We take advantage fo this by logging rule violations as a VFileMessage
on the relevant file.
Runner
The runner is responsible for handling options, loading configuration, loading rules, and setting up the environment for the engine to execute. The runner can be called directly from JavaScript, or invoked via the CLI. After calling the engine to execute the checks, the runner is responsible for reporting the results using its configured reporter.
Engine
The engine is responsible for reading files and executing rules against the files.
File
All files that are checked are represented by VFile
instances, with some additional information. The ContentFile
primitive represents files that contain documentation content. Currently, our content files are authored with MDX, with support for YAML frontmatter, out-of-the-box. The DataFile
primitive represents files that contain data used to render our pages. Currently, JSON and YAML files are supported.
Rule
This package ships with a number of rules, and they can also be defined within consuming projects. Rules can define a number of executor functions that will be run on the different file types. See Rules for more information.