Skip to content

Instantly share code, notes, and snippets.

@mikeholler
Created September 12, 2019 20:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeholler/e9c8f946084674bb11016304b7ade7e6 to your computer and use it in GitHub Desktop.
Save mikeholler/e9c8f946084674bb11016304b7ade7e6 to your computer and use it in GitHub Desktop.
Wrapper that allows for --author-mode argument.
#!/usr/bin/env node
/**
* This is a wrapper to the `antora generate` command line tool that adds some
* functionality not otherwise present in antora.
*
* Before getting into that, one restriction is that the playbook.yml file MUST
* be the first argument to this command, otherwise this script will not work
* correctly. Other than that small restriction, all other uses of
* `antora generate` are supported here.
*
* This wrapper delegates to the antora executable, so it needs to know where
* to find it. It'll assume it's on the path as `antora`, but if you have it
* somewhere else make sure to set the ANTORA_EXECUTABLE environment variable
* before running this command.
*
* NOTE: This script is a preprocessor for antora, but it does not modify your
* playbook file in any way. Instead, it writes the preprocessed playbook
* to a temporary file.
*
* Additional Linting:
*
* This tool will do some additional linting on the playbook.yml file that is to
* be passed to antora. An error will be thrown whenever there is a source in
* content.sources that has an id field that is the same as another source. That
* being said, sources are not required to have ID fields.
*
* Arguments:
*
* --author-mode <id>=<path>
*
* This argument allows the user to override an entry in content.sources based on the id field.
*
* Usages:
*
* The following usages will assume a file named playbook.yml with this snippet:
*
* content:
* sources:
* - id: component-a
* url: https://gitlab.com/components/a
* branches: [master, release-2.0]
* tags: [v2.0.0, v2.1.0]
* - id: component-b
* url: https://gitlab.com/components/b
* branches: [master, release-1.1]
* tags: [v1.0.0, v1.1.0]
*
* antora-generate-wrapper.js playbook.yml --author-mode component-a=repositories/component-a/
*
* The above command will effectively replace the source with id=component-a with:
*
* id: component-a
* url: repositories/component-a/
* branches: HEAD
*
* antora-generate-wrapper.js playbook.yml --author-mode unknown-id=repositories/unknown-id/
*
* The above command will ADD a source for local writing.
*
* antora-generate-wrapper.js playbook.yml \
* --author-mode component-a=repositories/component-a/ \
* --author-mode component-b=repositories/component-b/ \
*
* The above command will replace both component-a and component-b sources so that the playbook
* that is passed along to antora will look like:
*
* content:
* sources:
* - id: component-a
* url: repositories/component-a/
* branches: HEAD
* - id: component-b
* url: repositories/component-b/
* branches: HEAD
*/
'use strict';
// js-yaml is already in use by antora, so we're guaranteed availability.
// noinspection NpmUsedModulesInstalled
const yaml = require('js-yaml');
const fs = require('fs');
const os = require('os');
const path = require('path');
const childProcess = require('child_process');
if (process.argv.length < 3) {
console.log("Must provide playbook.yml file as argument.");
process.exit(1);
}
const NODE_EXECUTABLE = process.argv[0];
const PLAYBOOK_FILE = process.argv[2];
const ARGS = process.argv.slice(3);
const ANTORA_EXECUTABLE = process.env.ANTORA_EXECUTABLE;
function checkSourceIds(playbook) {
const ids = playbook.content.sources
.map(it => it.id)
.filter(it => it !== undefined);
const uniqueIds = new Set();
const duplicateIds = new Set();
ids.forEach(function(value) {
if (uniqueIds.has(value)) {
duplicateIds.add(value);
} else {
uniqueIds.add(value);
}
});
if (duplicateIds.size > 0) {
throw `Not all source IDs are unique. Found duplicates: ${Array.from(duplicateIds)}`;
}
}
function findAuthorModeArgs(args) {
const authorModeArgs = [];
const remainingArgs = [];
let isNextAuthorModeValue = false;
args.forEach(function(value) {
if (isNextAuthorModeValue) {
if (!value.includes('=')) {
throw `--author-mode argument value must contain one equal sign id=path, found: ${value}`
}
const [id, path] = value.split('=');
authorModeArgs.push({id: id, path: path});
isNextAuthorModeValue = false;
} else if (value === '--author-mode') {
isNextAuthorModeValue = true;
} else {
remainingArgs.push(value);
}
});
if (isNextAuthorModeValue) {
throw 'Last argument is --author-mode but no value is given.'
}
return {
authorModeArgs: authorModeArgs,
remainingArgs: remainingArgs
};
}
function runAntora(playbookFile, args, listener) {
let antora;
if (ANTORA_EXECUTABLE) {
antora = childProcess.execFile(NODE_EXECUTABLE, [ANTORA_EXECUTABLE, playbookFile].concat(args));
} else {
antora = childProcess.spawn('antora', [playbookFile].concat(args));
}
antora.stdout.on('data', data => console.log(`stdout: ${data}`));
antora.stderr.on('data', data => console.log(`stderr: ${data}`));
antora.on('close', listener);
}
function preprocessPlaybook(playbook, authorModeArgs) {
const processedPlaybook = clone(playbook);
authorModeArgs.forEach(function(value) {
const index = processedPlaybook.content.sources.findIndex(it => it.id === value.id);
const authorModeSource = {
id: value.id,
url: value.path,
branches: 'HEAD'
};
if (index === -1) {
// Create a new source.
processedPlaybook.content.sources.push(authorModeSource);
} else {
// Overlay author mode settings atop the existing source.
processedPlaybook.content.sources[index] = {
...playbook.content.sources[index],
...authorModeSource
};
}
});
return processedPlaybook;
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// https://stackoverflow.com/a/1349426/1076585
function makeid(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
function tempfile(prefix, suffix) {
return path.join(os.tmpdir(), `${prefix}${makeid(20)}${suffix}`)
}
// https://stackoverflow.com/a/14861513/1076585
function onSigint(listener) {
if (process.platform === 'win32') {
let rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('SIGINT', function () {
// noinspection JSCheckFunctionSignatures
process.emit('SIGINT');
});
}
process.on('SIGINT', function () {
listener();
});
}
function main() {
const playbook = yaml.safeLoad(fs.readFileSync(PLAYBOOK_FILE, 'utf8'));
checkSourceIds(playbook);
const args = findAuthorModeArgs(ARGS);
const processedPlaybook = preprocessPlaybook(playbook, args.authorModeArgs);
const processedPlaybookFile = tempfile('playbook-', '.yml');
const processedPlaybookYaml = yaml.safeDump(processedPlaybook);
fs.writeFileSync(processedPlaybookFile, processedPlaybookYaml, 'utf8');
function cleanup() {
fs.unlinkSync(processedPlaybookFile);
}
onSigint(function() {
cleanup();
process.exit();
});
runAntora(processedPlaybookFile, args.remainingArgs, function(code) {
cleanup();
process.exit(code);
});
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment