rdme
Version:
ReadMe's official CLI and GitHub Action.
136 lines (135 loc) • 5.61 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import { Ajv } from 'ajv';
import _addFormats from 'ajv-formats';
import grayMatter from 'gray-matter';
// workaround from here: https://github.com/ajv-validator/ajv-formats/issues/85#issuecomment-2262652443
const addFormats = _addFormats;
/**
* Validates the frontmatter data, fixes any issues, and returns the results.
*/
export function fix(
/** frontmatter data to be validated */
data,
/** schema to validate against */
schema,
/**
* mappings of object IDs to slugs
* (e.g., category IDs to category URIs)
*/
mappings) {
if (!Object.keys(data).length) {
this.debug('no frontmatter attributes found, skipping validation');
return { fixableErrorCount: 0, errors: [], hasIssues: false, unfixableErrors: [], updatedData: data };
}
const ajv = new Ajv({ allErrors: true, strictTypes: false, strictTuples: false });
addFormats(ajv);
const ajvValidate = ajv.compile(schema);
ajvValidate(data);
let errors;
if (ajvValidate?.errors) {
errors = ajvValidate.errors;
this.debug(`${errors.length} errors found: ${JSON.stringify(errors)}`);
// if one of the errors is that the category uri doesn't match the pattern,
// we can ignore it since we normalize it later
const categoryUriPatternErrorIndex = ajvValidate.errors.findIndex(error => error.instancePath === '/category/uri' && error.keyword === 'pattern');
if (categoryUriPatternErrorIndex >= 0) {
this.debug('removing category uri pattern error');
errors.splice(categoryUriPatternErrorIndex, 1);
}
// also do the same for the parent uri
const parentUriPatternErrorIndex = ajvValidate.errors.findIndex(error => error.instancePath === '/parent/uri' && error.keyword === 'pattern');
if (parentUriPatternErrorIndex >= 0) {
this.debug('removing category uri pattern error');
errors.splice(parentUriPatternErrorIndex, 1);
}
}
let fixableErrorCount = 0;
const unfixableErrors = [];
const updatedData = structuredClone(data);
if (typeof errors === 'undefined' || !errors.length) {
return { errors: [], fixableErrorCount, hasIssues: false, unfixableErrors, updatedData };
}
errors.forEach(error => {
if (error.instancePath === '/category' && error.keyword === 'type') {
const uri = mappings.categories[data.category];
updatedData.category = {
uri: uri || `uri-that-does-not-map-to-${data.category}`,
};
fixableErrorCount += 1;
}
else if (error.keyword === 'additionalProperties') {
const badKey = error.params.additionalProperty;
const extractedValue = data[badKey];
if (error.schemaPath === '#/additionalProperties') {
// if the bad property is at the root level, delete it
delete updatedData[badKey];
fixableErrorCount += 1;
if (badKey === 'excerpt') {
// if the `content` object exists, add to it. otherwise, create it
if (typeof updatedData.content === 'object' && updatedData.content) {
updatedData.content.excerpt = extractedValue;
}
else {
updatedData.content = {
excerpt: extractedValue,
};
}
}
else if (badKey === 'categorySlug') {
updatedData.category = {
uri: extractedValue,
};
}
else if (badKey === 'parentDoc') {
const uri = mappings.parentPages[extractedValue];
if (uri) {
updatedData.parent = {
uri,
};
}
}
else if (badKey === 'parentDocSlug') {
updatedData.parent = {
uri: extractedValue,
};
}
else if (badKey === 'hidden') {
const hidden = typeof extractedValue === 'boolean' ? extractedValue : extractedValue === 'true';
updatedData.privacy = { view: hidden ? 'anyone_with_link' : 'public' };
}
else if (badKey === 'order') {
updatedData.position = extractedValue;
}
}
else {
unfixableErrors.push(error);
}
}
else {
unfixableErrors.push(error);
}
});
return { errors, fixableErrorCount, hasIssues: true, unfixableErrors, updatedData };
}
export function writeFixes(
/** all metadata for the page that will be written to */
metadata,
/** frontmatter changes to be applied */
updatedData,
/** output directory to write to */
outputDirArg) {
const result = grayMatter.stringify(metadata.content, updatedData);
const outputPath = outputDirArg ? path.join(outputDirArg, metadata.filePath) : metadata.filePath;
this.debug(`writing fixes to ${outputPath}`);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputPath, result, { encoding: 'utf-8' });
const updatedMetadata = {
...metadata,
data: updatedData,
};
return updatedMetadata;
}