Created
September 12, 2019 20:24
-
-
Save mikeholler/e9c8f946084674bb11016304b7ade7e6 to your computer and use it in GitHub Desktop.
Wrapper that allows for --author-mode argument.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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