From 7f4b61b6eca6dfa5fd8eed31a28b99633d5f9123 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:39:38 +0200 Subject: [PATCH 01/14] feat: Apply default values when using package API chore(refactor): Move config related things unrelated to CLI specifics into config.js module. Make it easier to spot differences between external user config schema and runtime config schema. --- bin/index.js | 78 +++++------------ lib/api.js | 1 + lib/main.js | 36 ++++++-- lib/model/config.js | 193 +++++++++++++++++++++++++++++++++++++++++++ lib/model/context.js | 85 +------------------ package.json | 2 +- 6 files changed, 246 insertions(+), 149 deletions(-) create mode 100644 lib/api.js create mode 100644 lib/model/config.js diff --git a/bin/index.js b/bin/index.js index 0c70ea74..aca53874 100755 --- a/bin/index.js +++ b/bin/index.js @@ -6,13 +6,13 @@ import nodeFs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import proc from "node:process"; -import { NO_BASEDIR, NO_OUTDIR, OUTDIR_IS_BASEDIR, OUTDIR_IS_BASEDIR_WITH_DROP } from "../lib/cli/messages.js"; + import { upgrade } from "../lib/cli/upgrade.js"; -import * as main from "../lib/main.js"; +import { runViaCli } from "../lib/main.js"; +import { getRunnableConfig, getDefaultConfig } from "../lib/model/config.js"; import { watch } from "chokidar"; const require_ = createRequire(import.meta.url); -const confSchema = require_("../conf/v5/schema.json"); const packageJson = require_("../package.json"); const version = packageJson.version; const CWD = proc.cwd(); @@ -72,6 +72,12 @@ const cli = { ,type: "boolean" ,default: false } + ,"noupgrade": { + alias: "" + ,description: "When used prevents entering an interactive upgrade workflow when upgrade routines are available." + ,type: "boolean" + ,default: false + } ,"shallow": { alias: "" ,description: "A JSON string for an object to be shallow-merged with the default configuration or a configuration file provided with --config. Usage: `glossarify-md --shallow \"{'baseDir': './input'}\"`. Shallow merging _replaces_ nested property values. Use --deep to deep-merge." @@ -131,14 +137,7 @@ if (argv.help || proc.argv.length === 2) { */ async function configure(argv, cwd) { - const confSchemaProps = confSchema.properties; - const confDefault = Object - .keys(confSchemaProps) - .reduce((obj, key) => { - // Set up a default config from default values in the config schema. - obj[key] = confSchemaProps[key].default; - return obj; - }, { "$schema": confSchema.$id }); + const confDefault = getDefaultConfig(); // --config let confPath = argv.config || ""; @@ -154,6 +153,7 @@ async function configure(argv, cwd) { if (!argv.noupgrade) { confUser = await upgrade(confData, confPath, confDefault); } + proc.chdir(confDir); } catch (e) { console.error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); proc.exit(1); @@ -181,48 +181,12 @@ async function configure(argv, cwd) { } } - // Merge custom conf with default conf - const conf = merge(confDefault, confUser, { - clone: false - , arrayMerge: (_default, curConf) => { - return curConf && curConf.length > 0 ? curConf : _default; - } - }); - - return { confDir, conf }; + const conf = getRunnableConfig(confUser); + return conf; } // _/ Helpers \_________________________________________________________________ -function validateConf(conf) { - - if (conf.baseDir === "") { - console.log(NO_BASEDIR); - console.log("ABORTED.\n"); - proc.exit(0); - } - - if (conf.outDir === "") { - console.log(NO_OUTDIR); - console.log("ABORTED.\n"); - proc.exit(0); - } - - console.log(`☛ Reading from: ${conf.baseDir}`); - console.log(`☛ Writing to: ${conf.outDir}\n`); - - if (conf.outDir === conf.baseDir) { - if (conf.outDirDropOld) { - console.log(OUTDIR_IS_BASEDIR_WITH_DROP); - console.log("ABORTED.\n"); - proc.exit(0); - } else if (!argv.force) { - console.log(OUTDIR_IS_BASEDIR); - console.log("ABORTED.\n"); - proc.exit(0); - } - } -} // --init function writeConf(conf, argv) { @@ -292,7 +256,7 @@ function printHelp(parameters) { // _/ Run \_____________________________________________________________________ async function run() { - const { confDir, conf } = await configure(argv, CWD); + const conf = await configure(argv, CWD); // --init if (argv.init) { @@ -300,29 +264,25 @@ async function run() { proc.exit(0); } - // Resolve baseDir relative to confDir and outDir relative to baseDir - conf.baseDir = path.resolve(confDir, conf.baseDir); - conf.outDir = path.resolve(conf.baseDir, conf.outDir); - validateConf(conf, argv); try { // --watch if (argv.watch) { - await main.run(conf); + await runViaCli(conf, argv.force); // Do not drop 'outDir' while watching. Dropping it would cause some // subsequent 3rd-party watchers on it to break (e.g. vuepress 1.x) conf.outDirDropOld = false; console.log(`Start watching ${conf.baseDir}...`); const watcher = watch(conf.baseDir, { ignoreInitial: true, interval: 200 }) - .on("add", path => { console.log(`${path} added.`); main.run(conf); }) - .on("change", path => { console.log(`${path} changed.`); main.run(conf); }) - .on("unlink", path => { console.log(`${path} deleted.`); main.run(conf); }); + .on("add", path => { console.log(`${path} added.`); runViaCli(conf, argv.force); }) + .on("change", path => { console.log(`${path} changed.`); runViaCli(conf, argv.force); }) + .on("unlink", path => { console.log(`${path} deleted.`); runViaCli(conf, argv.force); }); const stopWatching = async () => { await watcher.close(); console.log("Stopped watching."); }; process.on("SIGINT", stopWatching); } else { - await main.run(conf); + await runViaCli(conf, argv.force); } } catch (err) { console.error(err); diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 00000000..96b89771 --- /dev/null +++ b/lib/api.js @@ -0,0 +1 @@ +export {runViaApi as run, getSlugger} from "./main.js"; diff --git a/lib/main.js b/lib/main.js index 4b31e3d1..484118d3 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,10 +2,8 @@ import GitHubSlugger from "github-slugger"; import { exportGlossaries } from "./exporter.js"; import { importGlossaries } from "./importer.js"; import { newContext } from "./model/context.js"; -import { - readDocumentFiles, - readGlossaries -} from "./reader.js"; +import { validateConfig, getRunnableConfig } from "./model/config.js"; +import { readDocumentFiles, readGlossaries } from "./reader.js"; import { copyBaseDirToOutDir, writeDocumentFiles, @@ -15,8 +13,34 @@ import { writeTestOutput } from "./writer.js"; -export async function run(glossarifyMdConf) { - let context = await newContext(glossarifyMdConf); + +/** + * @param {object} confUser User-provided configuration adhering to the external configuration schema. + * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. + */ +export async function runViaApi(confUser) { + const conf = getRunnableConfig(confUser); + conf.force = false; + return run(conf); +} + + +/** + * @param {object} confUser User-provided configuration adhering to the external configuration schema. Assumed to + * have been derived from applying config.js#getRunConfig() already. + * @param {object} forceFlag A flag indicating presence of the --force CLI argument. + * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. + */ +export async function runViaCli(conf, forceFlag) { + conf.force = forceFlag; + return run(conf); +} + +async function run(conf) { + + validateConfig(conf); + + const context = await newContext(conf); await copyBaseDirToOutDir(context); await importGlossaries(context); await readGlossaries(context); diff --git a/lib/model/config.js b/lib/model/config.js new file mode 100644 index 00000000..d3c8c052 --- /dev/null +++ b/lib/model/config.js @@ -0,0 +1,193 @@ +import merge from "deepmerge"; +import path from "node:path"; +import proc from "node:process"; +import { createRequire } from "node:module"; +import { toForwardSlash } from "../path/tools.js"; +import { NO_BASEDIR, NO_OUTDIR, OUTDIR_IS_BASEDIR, OUTDIR_IS_BASEDIR_WITH_DROP } from "../lib/cli/messages.js"; + +// Init default values from external configuration schema, once. +const require_ = createRequire(import.meta.url); +const confSchema = require_("../conf/v5/schema.json"); +const confSchemaProps = confSchema.properties; +const confDefault = Object + .keys(confSchemaProps) + .reduce((obj, key) => { + // Set up a default config from default values in the config schema. + obj[key] = confSchemaProps[key].default; + return obj; + }, { "$schema": confSchema.$id }); + + +/** + * @returns {object} The default configuration derived from defaults in the + * the external configuation schema. + */ +export function getDefaultConfig() { + return confDefault; +} + + +/** + * Merges a potentially incomplete user-provided configuration with the + * default configuration. + * + * @param {object} userConf + * @returns {object} A user-provided configuration with defaults for options + * not provided by the user. + */ +export function getRunnableConfig(userConf) { + const defaultConf = getDefaultConfig(); + + // Make sure to use defaults from config schema when user + // does not provide relevant options by merging user configuration + // into default configuration. + const conf = merge(defaultConf, userConf, { + clone: false + , arrayMerge: (_default, curConf) => { + return curConf && curConf.length > 0 ? curConf : _default; + } + }); + return conf; +} + +/** + * Maps a config adhering to the external config schema onto a slightly + * modified schema optimized for accessing config values at runtime. + * Meant to be used internally, only. + * + * Initializes essential hard coded defaults as a last resort and in addition + * to sensible defaults which should have already been applied to the given + * configuration. Initializes additional default values which can not + * be provided, statically. + * + * @param {object} conf A configuration adhering to the external config schema + * @returns {object} A configuration adhering to an internal runtime-optimized config schema + */ +export function getRuntimeConfig(conf) { + + const _conf = { + ...conf + ,baseDir: toForwardSlash(conf.baseDir || "") + ,outDir: toForwardSlash(conf.outDir || "") + ,indexing: { + ...conf.indexing + ,headingDepths: arrayToMap(conf.indexing.headingDepths) + } + ,linking: { + ...conf.linking + ,headingDepths: arrayToMap(conf.linking.headingDepths) + ,limitByTermOrigin: arrayToMap(conf.linking.limitByTermOrigin) + ,pathRewrites: reverseMultiValueMap(conf.linking.pathRewrites) + ,sortAlternatives: { + by: "glossary-filename" + ,...conf.linking.sortAlternatives + } + } + }; + + _conf.baseDir = path.resolve(proc.cwd(), conf.baseDir); + _conf.outDir = path.resolve(conf.baseDir, conf.outDir); + + // limit link creation for alternative definitions + const altLinks = _conf.linking.limitByAlternatives; + if (Math.abs(altLinks) > 95) { + _conf.linking.limitByAlternatives = Math.sign(altLinks) * 95; + } + const sortAlternatives = _conf.linking.sortAlternatives; + if (sortAlternatives.by === "glossary-ref-count") { + _conf.linking.sortAlternatives = { perSectionDepth: 2, ...sortAlternatives }; + } + if (conf.generateFiles.listOfFigures) { + _conf.generateFiles.listOfFigures = { class: "figure", title: "Figures", ...conf.generateFiles.listOfFigures }; + _conf.generateFiles.listOf.push(conf.generateFiles.listOfFigures); + } + if (conf.generateFiles.listOfTables) { + _conf.generateFiles.listOfTables = { class: "table", title: "Tables", ...conf.generateFiles.listOfTables }; + _conf.generateFiles.listOf.push(conf.generateFiles.listOfTables); + } + if (_conf.unified.rcPath) { + _conf.unified.rcPath = toForwardSlash(path.resolve(conf.baseDir, conf.unified.rcPath)); + } + + return _conf; +} + +// _/ Helpers \_________________________________________________________________ + +/** + * Validates a given configuration and tests whether it is a + * runnable configuration. + * + * @param {object} conf + */ +export function validateConfig(conf) { + + if (conf.baseDir === "") { + console.log(NO_BASEDIR); + console.log("ABORTED.\n"); + proc.exit(0); + } + + if (conf.outDir === "") { + console.log(NO_OUTDIR); + console.log("ABORTED.\n"); + proc.exit(0); + } + + console.log(`☛ Reading from: ${conf.baseDir}`); + console.log(`☛ Writing to: ${conf.outDir}\n`); + + if (conf.outDir === conf.baseDir) { + if (conf.outDirDropOld) { + console.log(OUTDIR_IS_BASEDIR_WITH_DROP); + console.log("ABORTED.\n"); + proc.exit(0); + } else if (!conf.force) { + console.log(OUTDIR_IS_BASEDIR); + console.log("ABORTED.\n"); + proc.exit(0); + } + } +} + +function arrayToMap(array) { + return array.reduce((prev, curr) => { + prev[curr] = true; + return prev; + }, {}); +} + +/** + * Transforms a map + * { + * "key1": ["value1", "value2", "value3"], + * "key2": ["value1", "valueA", "valueB"], + * "key3": "value%" + * } + * to a map + * { + * "value1": "key2", + * "value2": "key", + * "value3": "key", + * "valueA": "key2", + * "valueB": "key2", + * "value%": "key3" + * } + */ +function reverseMultiValueMap(input) { + const output = {}; + for (const key in input) { + if (input[key]) { + const values = [].concat(input[key]); // [1] + for (let i = 0, len = values.length; i < len; i++) { + const value = values[i]; + output[value] = key; + } + } + } + return output; + + // ___/ Implementation Notes \___ + // [1] use .concat() to wrap non-array %value% into [ "%value%" ]. + // Won't work with something like [... input[key]]. +} diff --git a/lib/model/context.js b/lib/model/context.js index 64c34718..273c50a7 100644 --- a/lib/model/context.js +++ b/lib/model/context.js @@ -4,7 +4,7 @@ import { Glob } from "glob"; import { relativeFromTo, toForwardSlash } from "../path/tools.js"; import { init as initCollator } from "../text/collator.js"; import { Glossary } from "./glossary.js"; - +import { getRuntimeConfig } from "./config.js"; /** * @@ -31,48 +31,7 @@ class Context { * @type {VFile} */ this.writeFiles = []; - const conf_ = this.conf = { - ...conf - ,baseDir: toForwardSlash(conf.baseDir || "") - ,outDir: toForwardSlash(conf.outDir || "") - ,indexing: { - ...conf.indexing - // Excluding certain headingDepths in (cross-)linking - ,headingDepths: arrayToMap(conf.indexing.headingDepths) - } - ,linking: { - ...conf.linking - ,headingDepths: arrayToMap(conf.linking.headingDepths) - ,limitByTermOrigin: arrayToMap(conf.linking.limitByTermOrigin) - ,pathRewrites: reverseMultiValueMap(conf.linking.pathRewrites) - ,sortAlternatives: { - by: "glossary-filename" - ,...conf.linking.sortAlternatives - } - } - }; - - // limit link creation for alternative definitions - const altLinks = conf.linking.limitByAlternatives; - if (Math.abs(altLinks) > 95) { - conf_.linking.limitByAlternatives = Math.sign(altLinks) * 95; - } - const sortAlternatives = conf_.linking.sortAlternatives; - if (sortAlternatives.by === "glossary-ref-count") { - conf_.linking.sortAlternatives = { perSectionDepth: 2, ...sortAlternatives }; - } - if (conf.generateFiles.listOfFigures) { - conf_.generateFiles.listOfFigures = { class: "figure", title: "Figures", ...conf.generateFiles.listOfFigures }; - conf_.generateFiles.listOf.push(conf.generateFiles.listOfFigures); - } - if (conf.generateFiles.listOfTables) { - conf_.generateFiles.listOfTables = { class: "table", title: "Tables", ...conf.generateFiles.listOfTables }; - conf_.generateFiles.listOf.push(conf.generateFiles.listOfTables); - } - if (conf_.unified.rcPath) { - conf_.unified.rcPath = toForwardSlash(path.resolve(conf.baseDir, conf.unified.rcPath)); - } - + this.conf = getRuntimeConfig(conf); } resolvePath(relativePath) { @@ -85,46 +44,6 @@ class Context { } } -function arrayToMap(array) { - return array.reduce((prev, curr) => { - prev[curr] = true; - return prev; - }, {}); -} - -/** - * Transforms a map - * { - * "key1": ["value1", "value2", "value3"], - * "key2": ["value1", "valueA", "valueB"], - * "key3": "value%" - * } - * to a map - * { - * "value1": "key2", - * "value2": "key", - * "value3": "key", - * "valueA": "key2", - * "valueB": "key2", - * "value%": "key3" - * } - */ -function reverseMultiValueMap(input) { - const output = {}; - for (const key in input) { - if (input[key]) { - const values = [].concat(input[key]); // [1] - for (let i = 0, len = values.length; i < len; i++) { - const value = values[i]; - output[value] = key; - } - } - } - return output; - - // ___/ Implementation Notes \___ - // [1] use .concat() to wrap %value% into [ "%value%" ] prior to forEach() -} async function unglobGlossaries(context) { const { baseDir, glossaries, excludeFiles } = context.conf; diff --git a/package.json b/package.json index 3b093904..4ec3d44b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "license": "MIT", "exports": { - "import": "./lib/main.js" + "import": "./lib/api.js" }, "type": "module", "engines": { From ce1d1e7bb10936de6847ce890ad061361c15c5c1 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:05:22 +0200 Subject: [PATCH 02/14] Improve on CLI testability. --- bin/index.js | 292 +----------------------------------------------- lib/cli/main.js | 291 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 289 deletions(-) create mode 100644 lib/cli/main.js diff --git a/bin/index.js b/bin/index.js index aca53874..624e61cf 100755 --- a/bin/index.js +++ b/bin/index.js @@ -1,292 +1,6 @@ #!/usr/bin/env node -import merge from "deepmerge"; -import fs from "fs-extra"; -import minimist from "minimist"; -import nodeFs from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; import proc from "node:process"; +import { main } from "../lib/cli/main.js"; -import { upgrade } from "../lib/cli/upgrade.js"; -import { runViaCli } from "../lib/main.js"; -import { getRunnableConfig, getDefaultConfig } from "../lib/model/config.js"; -import { watch } from "chokidar"; - -const require_ = createRequire(import.meta.url); -const packageJson = require_("../package.json"); -const version = packageJson.version; -const CWD = proc.cwd(); -const banner = -`┌──────────────────────────┐ -│ glossarify-md v${version} │ -└──────────────────────────┘ -`; - -// _/ CLI \_____________________________________________________________________ -const cli = { - "config": { - alias: "c" - ,description: "Path to config file, e.g. './glossarify-md.conf.json'." - ,type: "string" - ,default: "./glossarify-md.conf.json" - } - ,"deep": { - alias: "" - ,description: "Deeply merge the given JSON configuration string with the configuration file or default configuration. This will _extend_ nested arrays and replace only those keys exactly matching with the given structure. Use --shallow to shallow-merge." - ,type: "string" - ,default: "" - } - ,"help": { - alias: "h" - ,description: "Show this help." - ,type: "boolean" - ,default: false - } - ,"init": { - alias: "" - ,description: "Generate a configuration file with default values. Usage: 'glossarify-md --init > glossarify-md.conf.json'" - ,type: "boolean" - ,default: false - } - ,"local": { - alias: "" - ,description: "When used with --init generates a configuration using a local node_modules path to the config schema." - ,type: "boolean" - ,default: false - } - ,"logfile": { - alias: "" - ,description: "Where to write console logs into. Used for testing." - ,type: "string" - ,default: "" - } - ,"more": { - alias: "" - ,description: "When used with --init generates an extended configuration with default values otherwise applied in the background." - ,type: "boolean" - ,default: false - } - ,"new": { - alias: "" - ,description: "When used with --init generates a file ./docs/glossary.md" - ,type: "boolean" - ,default: false - } - ,"noupgrade": { - alias: "" - ,description: "When used prevents entering an interactive upgrade workflow when upgrade routines are available." - ,type: "boolean" - ,default: false - } - ,"shallow": { - alias: "" - ,description: "A JSON string for an object to be shallow-merged with the default configuration or a configuration file provided with --config. Usage: `glossarify-md --shallow \"{'baseDir': './input'}\"`. Shallow merging _replaces_ nested property values. Use --deep to deep-merge." - ,type: "string" - ,default: "" - } - ,"watch": { - alias: "w" - ,description: "Watch the base directory" - ,type: "boolean" - ,default: false - } -}; -const argv = minimist(proc.argv.slice(2), cli); - -// --logfile -if (argv.logfile) { - try { - nodeFs.unlinkSync(argv.logfile); - } catch (err) { - /* ignore */ - } - nodeFs.mkdirSync(path.dirname(argv.logfile), { recursive: true }); - const logfile = path.resolve(argv.logfile); - const logError = console.error; - const logger = (txt) => { - try { - nodeFs.writeFileSync(logfile, `${txt}\n`, { flag: "a"}); - } catch (err) { - logError(err); - } - }; - console.log = logger; - console.warn = logger; - console.error = logger; - console.info = logger; -} - -// --init -// Show banner only in absence of --init; Prevents writing banner -// to file for 'glossarify-md --init >> glossarify-md.conf.json' -if (!argv.init) { - console.log(banner); -} - -// --help (or no args at all) -if (argv.help || proc.argv.length === 2) { - printHelp(cli); - proc.exit(0); -} - -/** - * - * @param {*} argv key value map of CLI args - * @param {*} cwd current working directory - * @returns - */ -async function configure(argv, cwd) { - - const confDefault = getDefaultConfig(); - - // --config - let confPath = argv.config || ""; - let confDir = cwd; - let confUser = {}; - if (confPath) { - try { - confPath = path.resolve(cwd, confPath); - confDir = path.dirname(confPath); - const confFile = await fs.readFile(confPath); - const confData = JSON.parse(confFile); - // --noupgrade - if (!argv.noupgrade) { - confUser = await upgrade(confData, confPath, confDefault); - } - proc.chdir(confDir); - } catch (e) { - console.error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); - proc.exit(1); - } - } - - // --deep - if (argv.deep) { - try { - const confUserCli = JSON.parse(argv.deep.replace(/'/g, "\"")); - confUser = merge(confUser, confUserCli); - } catch (e) { - console.error(`Failed to parse value for --deep.\nReason:\n ${e.message}\n`); - proc.exit(1); - } - } - // --shallow - if (argv.shallow) { - try { - const confUserCli = JSON.parse(argv.shallow.replace(/'/g, "\"")); - confUser = Object.assign(confUser, confUserCli); - } catch (e) { - console.error(`Failed to parse value for --shallow.\nReason:\n ${e.message}\n`); - proc.exit(1); - } - } - - const conf = getRunnableConfig(confUser); - return conf; -} - - -// _/ Helpers \_________________________________________________________________ - -// --init -function writeConf(conf, argv) { - - let fileOpts = null; - let replacer = null; - - // --local - if (argv.local) { - // append version path segment from schema URI to local path - conf.$schema = `./node_modules/glossarify-md/conf/${conf.$schema.split("/conf/")[1]}`; - } else { - conf.$schema = conf.$schema.replace(/\/v(\.?\d){3}\/conf/, `/v${version}/conf`); - } - // --more - if (argv.more) { - delete conf.dev; - fileOpts = { spaces: 2 }; - } else { - // generate a minimal configuration - replacer = (that, keyVal) => { - if (typeof keyVal === "object") { - const {$schema, baseDir, outDir} = keyVal; - return {$schema, baseDir, outDir}; - } else { - return keyVal; - } - }; - fileOpts = { spaces: 2, replacer }; - } - - // --new - if (argv.new) { - const glossaryFile = path.resolve(conf.baseDir, "glossary.md"); - const configFile = path.resolve(conf.baseDir, "../glossarify-md.conf.json"); - if (fs.pathExistsSync(glossaryFile)) { - console.log(`⚠ Warning: ${glossaryFile} already exists. Nothing written.`); - } else { - fs.outputFileSync(glossaryFile, "# Glossary", "utf8"); - } - if (fs.pathExistsSync(configFile)) { - console.log(`⚠ Warning: ${configFile} already exists. Nothing written.`); - } else { - fs.writeJsonSync(configFile, conf, fileOpts); - } - } else { - console.log(JSON.stringify(conf, replacer, 2)); - } -} - -// --help -function printHelp(parameters) { - console.log("Options:\n"); - console.log( - Object - .keys(parameters) - .filter(key => key !== "dev") - .sort((a, b) => a.localeCompare(b, "en")) - .map(key => { - const {alias, type, description, default:_default} = parameters[key]; - return `--${key}${alias ? ", --" + alias : ""} (${type})\n Default: ${JSON.stringify(_default)}\n\n${description}\n\n`; - }) - .join("") - ); -} - -// _/ Run \_____________________________________________________________________ -async function run() { - - const conf = await configure(argv, CWD); - - // --init - if (argv.init) { - writeConf(conf, argv); - proc.exit(0); - } - - try { - // --watch - if (argv.watch) { - await runViaCli(conf, argv.force); - // Do not drop 'outDir' while watching. Dropping it would cause some - // subsequent 3rd-party watchers on it to break (e.g. vuepress 1.x) - conf.outDirDropOld = false; - console.log(`Start watching ${conf.baseDir}...`); - const watcher = watch(conf.baseDir, { ignoreInitial: true, interval: 200 }) - .on("add", path => { console.log(`${path} added.`); runViaCli(conf, argv.force); }) - .on("change", path => { console.log(`${path} changed.`); runViaCli(conf, argv.force); }) - .on("unlink", path => { console.log(`${path} deleted.`); runViaCli(conf, argv.force); }); - const stopWatching = async () => { - await watcher.close(); - console.log("Stopped watching."); - }; - process.on("SIGINT", stopWatching); - } else { - await runViaCli(conf, argv.force); - } - } catch (err) { - console.error(err); - proc.exit(1); - } -} -run(); +const cliArgv = proc.argv.slice(2); +main(cliArgv); \ No newline at end of file diff --git a/lib/cli/main.js b/lib/cli/main.js new file mode 100644 index 00000000..17616dbd --- /dev/null +++ b/lib/cli/main.js @@ -0,0 +1,291 @@ +import merge from "deepmerge"; +import fs from "fs-extra"; +import minimist from "minimist"; +import nodeFs from "node:fs"; +import path from "node:path"; +import proc from "node:process"; + +import { createRequire } from "node:module"; +import { upgrade } from "./upgrade.js"; +import { runViaCli } from "../main.js"; +import { getRunnableConfig, getDefaultConfig } from "../model/config.js"; +import { watch } from "chokidar"; + +const require_ = createRequire(import.meta.url); +const packageJson = require_("../../package.json"); +const version = packageJson.version; +const CWD = proc.cwd(); +const banner = +`┌──────────────────────────┐ +│ glossarify-md v${version} │ +└──────────────────────────┘ +`; + +// _/ CLI \_____________________________________________________________________ +const cli = { + "config": { + alias: "c" + ,description: "Path to config file, e.g. './glossarify-md.conf.json'." + ,type: "string" + ,default: "./glossarify-md.conf.json" + } + ,"deep": { + alias: "" + ,description: "Deeply merge the given JSON configuration string with the configuration file or default configuration. This will _extend_ nested arrays and replace only those keys exactly matching with the given structure. Use --shallow to shallow-merge." + ,type: "string" + ,default: "" + } + ,"help": { + alias: "h" + ,description: "Show this help." + ,type: "boolean" + ,default: false + } + ,"init": { + alias: "" + ,description: "Generate a configuration file with default values. Usage: 'glossarify-md --init > glossarify-md.conf.json'" + ,type: "boolean" + ,default: false + } + ,"local": { + alias: "" + ,description: "When used with --init generates a configuration using a local node_modules path to the config schema." + ,type: "boolean" + ,default: false + } + ,"logfile": { + alias: "" + ,description: "Where to write console logs into. Used for testing." + ,type: "string" + ,default: "" + } + ,"more": { + alias: "" + ,description: "When used with --init generates an extended configuration with default values otherwise applied in the background." + ,type: "boolean" + ,default: false + } + ,"new": { + alias: "" + ,description: "When used with --init generates a file ./docs/glossary.md" + ,type: "boolean" + ,default: false + } + ,"noupgrade": { + alias: "" + ,description: "When used prevents entering an interactive upgrade workflow when upgrade routines are available." + ,type: "boolean" + ,default: false + } + ,"shallow": { + alias: "" + ,description: "A JSON string for an object to be shallow-merged with the default configuration or a configuration file provided with --config. Usage: `glossarify-md --shallow \"{'baseDir': './input'}\"`. Shallow merging _replaces_ nested property values. Use --deep to deep-merge." + ,type: "string" + ,default: "" + } + ,"watch": { + alias: "w" + ,description: "Watch the base directory" + ,type: "boolean" + ,default: false + } +}; + +export async function main(args) { + const argv = minimist(args, cli); + + // --logfile + if (argv.logfile) { + try { + nodeFs.unlinkSync(argv.logfile); + } catch (err) { + /* ignore */ + } + nodeFs.mkdirSync(path.dirname(argv.logfile), { recursive: true }); + const logfile = path.resolve(argv.logfile); + const logError = console.error; + const logger = (txt) => { + try { + nodeFs.writeFileSync(logfile, `${txt}\n`, { flag: "a"}); + } catch (err) { + logError(err); + } + }; + console.log = logger; + console.warn = logger; + console.error = logger; + console.info = logger; + } + + // --init + // Show banner only in absence of --init; Prevents writing banner + // to file for 'glossarify-md --init >> glossarify-md.conf.json' + if (!argv.init) { + console.log(banner); + } + + // --help (or no args at all) + if (argv.help || args.length === 0) { + printHelp(cli); + proc.exit(0); + } + + const conf = await configure(argv, CWD); + + // --init + if (argv.init) { + writeConf(conf, argv); + proc.exit(0); + } + + try { + // --watch + if (argv.watch) { + await runViaCli(conf, argv.force); + // Do not drop 'outDir' while watching. Dropping it would cause some + // subsequent 3rd-party watchers on it to break (e.g. vuepress 1.x) + conf.outDirDropOld = false; + console.log(`Start watching ${conf.baseDir}...`); + const watcher = watch(conf.baseDir, { ignoreInitial: true, interval: 200 }) + .on("add", path => { console.log(`${path} added.`); runViaCli(conf, argv.force); }) + .on("change", path => { console.log(`${path} changed.`); runViaCli(conf, argv.force); }) + .on("unlink", path => { console.log(`${path} deleted.`); runViaCli(conf, argv.force); }); + const stopWatching = async () => { + await watcher.close(); + console.log("Stopped watching."); + }; + process.on("SIGINT", stopWatching); + } else { + await runViaCli(conf, argv.force); + } + } catch (err) { + console.error(err); + proc.exit(1); + } +} + +/** + * + * @param {*} argv key value map of CLI args + * @param {*} cwd current working directory + * @returns + */ +async function configure(argv, cwd) { + + const confDefault = getDefaultConfig(); + + // --config + let confPath = argv.config || ""; + let confDir = cwd; + let confUser = {}; + if (confPath) { + try { + confPath = path.resolve(cwd, confPath); + confDir = path.dirname(confPath); + const confFile = await fs.readFile(confPath); + const confData = JSON.parse(confFile); + // --noupgrade + if (!argv.noupgrade) { + confUser = await upgrade(confData, confPath, confDefault); + } + proc.chdir(confDir); + } catch (e) { + console.error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); + proc.exit(1); + } + } + + // --deep + if (argv.deep) { + try { + const confUserCli = JSON.parse(argv.deep.replace(/'/g, "\"")); + confUser = merge(confUser, confUserCli); + } catch (e) { + console.error(`Failed to parse value for --deep.\nReason:\n ${e.message}\n`); + proc.exit(1); + } + } + // --shallow + if (argv.shallow) { + try { + const confUserCli = JSON.parse(argv.shallow.replace(/'/g, "\"")); + confUser = Object.assign(confUser, confUserCli); + } catch (e) { + console.error(`Failed to parse value for --shallow.\nReason:\n ${e.message}\n`); + proc.exit(1); + } + } + + const conf = getRunnableConfig(confUser); + return conf; +} + + +// _/ Helpers \_________________________________________________________________ + +// --init +function writeConf(conf, argv) { + + let fileOpts = null; + let replacer = null; + + // --local + if (argv.local) { + // append version path segment from schema URI to local path + conf.$schema = `./node_modules/glossarify-md/conf/${conf.$schema.split("/conf/")[1]}`; + } else { + conf.$schema = conf.$schema.replace(/\/v(\.?\d){3}\/conf/, `/v${version}/conf`); + } + // --more + if (argv.more) { + delete conf.dev; + fileOpts = { spaces: 2 }; + } else { + // generate a minimal configuration + replacer = (that, keyVal) => { + if (typeof keyVal === "object") { + const {$schema, baseDir, outDir} = keyVal; + return {$schema, baseDir, outDir}; + } else { + return keyVal; + } + }; + fileOpts = { spaces: 2, replacer }; + } + + // --new + if (argv.new) { + const glossaryFile = path.resolve(conf.baseDir, "glossary.md"); + const configFile = path.resolve(conf.baseDir, "../glossarify-md.conf.json"); + if (fs.pathExistsSync(glossaryFile)) { + console.log(`⚠ Warning: ${glossaryFile} already exists. Nothing written.`); + } else { + fs.outputFileSync(glossaryFile, "# Glossary", "utf8"); + } + if (fs.pathExistsSync(configFile)) { + console.log(`⚠ Warning: ${configFile} already exists. Nothing written.`); + } else { + fs.writeJsonSync(configFile, conf, fileOpts); + } + } else { + console.log(JSON.stringify(conf, replacer, 2)); + } +} + +// --help +function printHelp(parameters) { + console.log("Options:\n"); + console.log( + Object + .keys(parameters) + .filter(key => key !== "dev") + .sort((a, b) => a.localeCompare(b, "en")) + .map(key => { + const {alias, type, description, default:_default} = parameters[key]; + return `--${key}${alias ? ", --" + alias : ""} (${type})\n Default: ${JSON.stringify(_default)}\n\n${description}\n\n`; + }) + .join("") + ); +} + + From 239fe76d0c2370baca7fd792ef6bd77af4d5da77 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Thu, 24 Aug 2023 09:52:30 +0200 Subject: [PATCH 03/14] Improve error handling. Apply suggestions on use of proc.exit(1) --- lib/cli/main.js | 32 +++++++++++++++++++----------- lib/main.js | 47 +++++++++++++++++++++++---------------------- lib/model/config.js | 31 +++++++++++++----------------- lib/model/errors.js | 20 +++++++++++++++++++ 4 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 lib/model/errors.js diff --git a/lib/cli/main.js b/lib/cli/main.js index 17616dbd..9f7cf525 100644 --- a/lib/cli/main.js +++ b/lib/cli/main.js @@ -10,6 +10,7 @@ import { upgrade } from "./upgrade.js"; import { runViaCli } from "../main.js"; import { getRunnableConfig, getDefaultConfig } from "../model/config.js"; import { watch } from "chokidar"; +import { ConfigError } from "../model/errors.js"; const require_ = createRequire(import.meta.url); const packageJson = require_("../../package.json"); @@ -127,21 +128,26 @@ export async function main(args) { // --help (or no args at all) if (argv.help || args.length === 0) { printHelp(cli); - proc.exit(0); + return; } const conf = await configure(argv, CWD); + // --force + if (argv.force) { + conf.force = true; + } + // --init if (argv.init) { writeConf(conf, argv); - proc.exit(0); + return; } try { // --watch if (argv.watch) { - await runViaCli(conf, argv.force); + await runViaCli(conf); // Do not drop 'outDir' while watching. Dropping it would cause some // subsequent 3rd-party watchers on it to break (e.g. vuepress 1.x) conf.outDirDropOld = false; @@ -159,8 +165,15 @@ export async function main(args) { await runViaCli(conf, argv.force); } } catch (err) { - console.error(err); - proc.exit(1); + if (err instanceof ConfigError) { + console.log(err.message); + console.log("ABORTED.\n"); + proc.exitCode = 0; + } else { + console.error(err); + proc.exitCode = 1; + } + return; } } @@ -190,8 +203,7 @@ async function configure(argv, cwd) { } proc.chdir(confDir); } catch (e) { - console.error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); - proc.exit(1); + throw new Error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); } } @@ -201,8 +213,7 @@ async function configure(argv, cwd) { const confUserCli = JSON.parse(argv.deep.replace(/'/g, "\"")); confUser = merge(confUser, confUserCli); } catch (e) { - console.error(`Failed to parse value for --deep.\nReason:\n ${e.message}\n`); - proc.exit(1); + throw new Error(`Failed to parse value for --deep.\nReason:\n ${e.message}\n`); } } // --shallow @@ -211,8 +222,7 @@ async function configure(argv, cwd) { const confUserCli = JSON.parse(argv.shallow.replace(/'/g, "\"")); confUser = Object.assign(confUser, confUserCli); } catch (e) { - console.error(`Failed to parse value for --shallow.\nReason:\n ${e.message}\n`); - proc.exit(1); + throw new Error(`Failed to parse value for --shallow.\nReason:\n ${e.message}\n`); } } diff --git a/lib/main.js b/lib/main.js index 484118d3..c157606b 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,7 +2,7 @@ import GitHubSlugger from "github-slugger"; import { exportGlossaries } from "./exporter.js"; import { importGlossaries } from "./importer.js"; import { newContext } from "./model/context.js"; -import { validateConfig, getRunnableConfig } from "./model/config.js"; +import { getRunnableConfig } from "./model/config.js"; import { readDocumentFiles, readGlossaries } from "./reader.js"; import { copyBaseDirToOutDir, @@ -12,34 +12,16 @@ import { writeReport, writeTestOutput } from "./writer.js"; - - -/** - * @param {object} confUser User-provided configuration adhering to the external configuration schema. - * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. - */ -export async function runViaApi(confUser) { - const conf = getRunnableConfig(confUser); - conf.force = false; - return run(conf); -} - +import { ConfigError } from "./model/errors.js"; /** - * @param {object} confUser User-provided configuration adhering to the external configuration schema. Assumed to - * have been derived from applying config.js#getRunConfig() already. + * @param {object} conf Configuration adhering to the external configuration schema. + * Assumed to have been initialized with default values for options not provided by + * users themselves. * @param {object} forceFlag A flag indicating presence of the --force CLI argument. * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. */ -export async function runViaCli(conf, forceFlag) { - conf.force = forceFlag; - return run(conf); -} - async function run(conf) { - - validateConfig(conf); - const context = await newContext(conf); await copyBaseDirToOutDir(context); await importGlossaries(context); @@ -58,6 +40,25 @@ async function run(conf) { return context; } +/** + * @param {object} confUser User-provided configuration adhering to the external configuration schema. + * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. + */ +export async function runViaApi(confUser) { + const conf = getRunnableConfig(confUser); + return run(conf); +} + +/** + * Call when running from CLI and when configuration schema defaults have already + * been applied on a user-provided configuration. To be used internally. Not meant to be + * exported as part of the programming API. + * + * @see config.js#getRunnableConfig + * @see #runViaApi + */ +export const runViaCli = run; + /** * Provide internally used slugifier to allow for better integration with vuepress * See also https://github.com/about-code/glossarify-md/issues/27. diff --git a/lib/model/config.js b/lib/model/config.js index d3c8c052..b1c7ffb6 100644 --- a/lib/model/config.js +++ b/lib/model/config.js @@ -3,11 +3,12 @@ import path from "node:path"; import proc from "node:process"; import { createRequire } from "node:module"; import { toForwardSlash } from "../path/tools.js"; -import { NO_BASEDIR, NO_OUTDIR, OUTDIR_IS_BASEDIR, OUTDIR_IS_BASEDIR_WITH_DROP } from "../lib/cli/messages.js"; +import { NO_BASEDIR, NO_OUTDIR, OUTDIR_IS_BASEDIR, OUTDIR_IS_BASEDIR_WITH_DROP } from "../cli/messages.js"; +import { ConfigError } from "./errors.js"; // Init default values from external configuration schema, once. const require_ = createRequire(import.meta.url); -const confSchema = require_("../conf/v5/schema.json"); +const confSchema = require_("../../conf/v5/schema.json"); const confSchemaProps = confSchema.properties; const confDefault = Object .keys(confSchemaProps) @@ -88,6 +89,9 @@ export function getRuntimeConfig(conf) { _conf.baseDir = path.resolve(proc.cwd(), conf.baseDir); _conf.outDir = path.resolve(conf.baseDir, conf.outDir); + console.log(`☛ Reading from: ${_conf.baseDir}`); + console.log(`☛ Writing to: ${_conf.outDir}\n`); + // limit link creation for alternative definitions const altLinks = _conf.linking.limitByAlternatives; if (Math.abs(altLinks) > 95) { @@ -109,6 +113,8 @@ export function getRuntimeConfig(conf) { _conf.unified.rcPath = toForwardSlash(path.resolve(conf.baseDir, conf.unified.rcPath)); } + validateRuntimeConfig(_conf); + return _conf; } @@ -120,32 +126,21 @@ export function getRuntimeConfig(conf) { * * @param {object} conf */ -export function validateConfig(conf) { +function validateRuntimeConfig(conf) { if (conf.baseDir === "") { - console.log(NO_BASEDIR); - console.log("ABORTED.\n"); - proc.exit(0); + throw new ConfigError(NO_BASEDIR); } if (conf.outDir === "") { - console.log(NO_OUTDIR); - console.log("ABORTED.\n"); - proc.exit(0); + throw new ConfigError(NO_OUTDIR); } - console.log(`☛ Reading from: ${conf.baseDir}`); - console.log(`☛ Writing to: ${conf.outDir}\n`); - if (conf.outDir === conf.baseDir) { if (conf.outDirDropOld) { - console.log(OUTDIR_IS_BASEDIR_WITH_DROP); - console.log("ABORTED.\n"); - proc.exit(0); + throw new ConfigError(OUTDIR_IS_BASEDIR_WITH_DROP); } else if (!conf.force) { - console.log(OUTDIR_IS_BASEDIR); - console.log("ABORTED.\n"); - proc.exit(0); + throw new ConfigError(OUTDIR_IS_BASEDIR); } } } diff --git a/lib/model/errors.js b/lib/model/errors.js new file mode 100644 index 00000000..c568f8d0 --- /dev/null +++ b/lib/model/errors.js @@ -0,0 +1,20 @@ + +/** + * Error being thrown when an invalid configuration was detected. + * May be handled like a validation error, so may not need to + * terminate process abnormaly with process.exit(1) but could + * be handled to terminate process with process.exit(0). + * + * @implements Error + */ +export class ConfigError extends Error { + + constructor(message) { + super(message); + } + + get code() { + return ConfigError.ERR_CONF; + } +} +ConfigError.code = "ERR_CONF"; From e88976aeab0300d18576942133c0cd452c5da9a1 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Fri, 25 Aug 2023 11:10:48 +0200 Subject: [PATCH 04/14] Fix linter errors. --- bin/index.js | 2 +- lib/cli/main.js | 1 - lib/main.js | 1 - lib/model/errors.js | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/bin/index.js b/bin/index.js index 624e61cf..9e3da9b4 100755 --- a/bin/index.js +++ b/bin/index.js @@ -3,4 +3,4 @@ import proc from "node:process"; import { main } from "../lib/cli/main.js"; const cliArgv = proc.argv.slice(2); -main(cliArgv); \ No newline at end of file +main(cliArgv); diff --git a/lib/cli/main.js b/lib/cli/main.js index 9f7cf525..d287a250 100644 --- a/lib/cli/main.js +++ b/lib/cli/main.js @@ -298,4 +298,3 @@ function printHelp(parameters) { ); } - diff --git a/lib/main.js b/lib/main.js index c157606b..d18f42f0 100644 --- a/lib/main.js +++ b/lib/main.js @@ -12,7 +12,6 @@ import { writeReport, writeTestOutput } from "./writer.js"; -import { ConfigError } from "./model/errors.js"; /** * @param {object} conf Configuration adhering to the external configuration schema. diff --git a/lib/model/errors.js b/lib/model/errors.js index c568f8d0..245defaa 100644 --- a/lib/model/errors.js +++ b/lib/model/errors.js @@ -1,4 +1,3 @@ - /** * Error being thrown when an invalid configuration was detected. * May be handled like a validation error, so may not need to From dbdd338e4b659ab58d3d435a6065bb7c529a1851 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Fri, 25 Aug 2023 12:21:05 +0200 Subject: [PATCH 05/14] Correctly set baseDir from config file srcDir. --- lib/cli/main.js | 32 +++++++++++++++++++------------- lib/main.js | 5 ++++- lib/model/config.js | 11 +++++++++-- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/cli/main.js b/lib/cli/main.js index d287a250..8fa278f5 100644 --- a/lib/cli/main.js +++ b/lib/cli/main.js @@ -138,31 +138,30 @@ export async function main(args) { conf.force = true; } - // --init - if (argv.init) { - writeConf(conf, argv); - return; - } - try { // --watch if (argv.watch) { + + // Run once prior to watching... await runViaCli(conf); + // Do not drop 'outDir' while watching. Dropping it would cause some // subsequent 3rd-party watchers on it to break (e.g. vuepress 1.x) conf.outDirDropOld = false; - console.log(`Start watching ${conf.baseDir}...`); - const watcher = watch(conf.baseDir, { ignoreInitial: true, interval: 200 }) - .on("add", path => { console.log(`${path} added.`); runViaCli(conf, argv.force); }) - .on("change", path => { console.log(`${path} changed.`); runViaCli(conf, argv.force); }) - .on("unlink", path => { console.log(`${path} deleted.`); runViaCli(conf, argv.force); }); + + const watchDir = path.resolve(conf.srcDir, conf.baseDir); + console.log(`Start watching ${watchDir}...`); + const watcher = watch(watchDir, { ignoreInitial: true, interval: 200 }) + .on("add", path => { console.log(`${path} added.`); runViaCli(conf); }) + .on("change", path => { console.log(`${path} changed.`); runViaCli(conf); }) + .on("unlink", path => { console.log(`${path} deleted.`); runViaCli(conf); }); const stopWatching = async () => { await watcher.close(); console.log("Stopped watching."); }; process.on("SIGINT", stopWatching); } else { - await runViaCli(conf, argv.force); + await runViaCli(conf); } } catch (err) { if (err instanceof ConfigError) { @@ -201,7 +200,6 @@ async function configure(argv, cwd) { if (!argv.noupgrade) { confUser = await upgrade(confData, confPath, confDefault); } - proc.chdir(confDir); } catch (e) { throw new Error(`Failed to read config '${confPath}'.\nReason:\n ${e.message}\n`); } @@ -227,6 +225,14 @@ async function configure(argv, cwd) { } const conf = getRunnableConfig(confUser); + + // --init + if (argv.init) { + writeConf(conf, argv); + proc.exit(0); + } + + conf.srcDir = confDir; return conf; } diff --git a/lib/main.js b/lib/main.js index d18f42f0..b540e1b6 100644 --- a/lib/main.js +++ b/lib/main.js @@ -16,7 +16,10 @@ import { /** * @param {object} conf Configuration adhering to the external configuration schema. * Assumed to have been initialized with default values for options not provided by - * users themselves. + * users themselves. When the configuration object was loaded from a file it must + * have been augmented with a 'srcDir' property denoting the directory of the config + * file. If 'srcDir' is missing 'baseDir' will be resolved relative to the process's + * current working directory. * @param {object} forceFlag A flag indicating presence of the --force CLI argument. * @returns {Promise} a promise from an async execution resolving to an object representing the execution context. */ diff --git a/lib/model/config.js b/lib/model/config.js index b1c7ffb6..53e1d8b7 100644 --- a/lib/model/config.js +++ b/lib/model/config.js @@ -86,8 +86,15 @@ export function getRuntimeConfig(conf) { } }; - _conf.baseDir = path.resolve(proc.cwd(), conf.baseDir); - _conf.outDir = path.resolve(conf.baseDir, conf.outDir); + // When a configuration has been loaded from a file then srcDir + // is meant to be the directory of the configuration file. + if (! conf.srcDir) { + conf.srcDir = proc.cwd(); + } + // Resolve baseDir relative to config file src directory + // Resolve outDir relative to baseDir. + _conf.baseDir = path.resolve(conf.srcDir, conf.baseDir); + _conf.outDir = path.resolve(_conf.baseDir, conf.outDir); console.log(`☛ Reading from: ${_conf.baseDir}`); console.log(`☛ Writing to: ${_conf.outDir}\n`); From 0202fe87d88024e98c416a15f3ed6cd8d0158c3f Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:41:06 +0200 Subject: [PATCH 06/14] try get better error message --- lib/cli/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cli/main.js b/lib/cli/main.js index 8fa278f5..d687036f 100644 --- a/lib/cli/main.js +++ b/lib/cli/main.js @@ -159,7 +159,7 @@ export async function main(args) { await watcher.close(); console.log("Stopped watching."); }; - process.on("SIGINT", stopWatching); + proc.on("SIGINT", stopWatching); } else { await runViaCli(conf); } @@ -167,10 +167,10 @@ export async function main(args) { if (err instanceof ConfigError) { console.log(err.message); console.log("ABORTED.\n"); - proc.exitCode = 0; + proc.exit(0); } else { console.error(err); - proc.exitCode = 1; + proc.exit(1); } return; } From eb6e187d05334aa9e37fc8963c38da5bcf37dfd1 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:47:51 +0200 Subject: [PATCH 07/14] Revert "try get better error message" This reverts commit cf9a64d5cd584d56cedae41a024d9e1bcb75c16e. --- lib/cli/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cli/main.js b/lib/cli/main.js index d687036f..8fa278f5 100644 --- a/lib/cli/main.js +++ b/lib/cli/main.js @@ -159,7 +159,7 @@ export async function main(args) { await watcher.close(); console.log("Stopped watching."); }; - proc.on("SIGINT", stopWatching); + process.on("SIGINT", stopWatching); } else { await runViaCli(conf); } @@ -167,10 +167,10 @@ export async function main(args) { if (err instanceof ConfigError) { console.log(err.message); console.log("ABORTED.\n"); - proc.exit(0); + proc.exitCode = 0; } else { console.error(err); - proc.exit(1); + proc.exitCode = 1; } return; } From d8b8f0cf3e7652df63465a98594fe723c065f7b0 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 26 Aug 2023 20:04:07 +0200 Subject: [PATCH 08/14] Fix path shenanigans --- lib/model/config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/model/config.js b/lib/model/config.js index 53e1d8b7..e4e86b10 100644 --- a/lib/model/config.js +++ b/lib/model/config.js @@ -93,8 +93,8 @@ export function getRuntimeConfig(conf) { } // Resolve baseDir relative to config file src directory // Resolve outDir relative to baseDir. - _conf.baseDir = path.resolve(conf.srcDir, conf.baseDir); - _conf.outDir = path.resolve(_conf.baseDir, conf.outDir); + _conf.baseDir = toForwardSlash(path.resolve(conf.srcDir, conf.baseDir)); + _conf.outDir = toForwardSlash(path.resolve(_conf.baseDir, conf.outDir)); console.log(`☛ Reading from: ${_conf.baseDir}`); console.log(`☛ Writing to: ${_conf.outDir}\n`); @@ -117,7 +117,7 @@ export function getRuntimeConfig(conf) { _conf.generateFiles.listOf.push(conf.generateFiles.listOfTables); } if (_conf.unified.rcPath) { - _conf.unified.rcPath = toForwardSlash(path.resolve(conf.baseDir, conf.unified.rcPath)); + _conf.unified.rcPath = toForwardSlash(path.resolve(_conf.baseDir, conf.unified.rcPath)); } validateRuntimeConfig(_conf); From e37f32fb38958135d5404b162165840812e8f684 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 26 Aug 2023 20:31:10 +0200 Subject: [PATCH 09/14] Write srcDir to effective configuration. --- lib/writer.js | 1 + .../config-cli/deep-defaults/glossarify-md-effective.conf.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/writer.js b/lib/writer.js index 3680e951..d79741fe 100644 --- a/lib/writer.js +++ b/lib/writer.js @@ -211,6 +211,7 @@ export async function writeTestOutput(context) { const snapshot = Object.assign({}, conf); snapshot.baseDir = toReproducablePath(CWD, conf.baseDir, "{CWD}"); snapshot.outDir = toReproducablePath(CWD, conf.outDir, "{CWD}"); + snapshot.srcDir = toReproducablePath(CWD, conf.srcDir, "{CWD}"); promises.push(writeTestFile(context, effectiveConfFile, JSON.stringify(snapshot, null, 2))); } if (termsFile && indexedTerms) { diff --git a/test/output-expected/config-cli/deep-defaults/glossarify-md-effective.conf.json b/test/output-expected/config-cli/deep-defaults/glossarify-md-effective.conf.json index 410a2381..ec520d08 100644 --- a/test/output-expected/config-cli/deep-defaults/glossarify-md-effective.conf.json +++ b/test/output-expected/config-cli/deep-defaults/glossarify-md-effective.conf.json @@ -67,5 +67,6 @@ "unified": {}, "dev": { "effectiveConfFile": "./glossarify-md-effective.conf.json" - } + }, + "srcDir": "{CWD}/input/config-cli/deep-defaults" } From 87221f44be5720c1e9535294eeaa8c1635653014 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:40:58 +0200 Subject: [PATCH 10/14] Run tests on node 20.x --- .github/workflows/tests-functional.yml | 2 +- .github/workflows/tests-latest.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-functional.yml b/.github/workflows/tests-functional.yml index e65e0abe..1b68c720 100644 --- a/.github/workflows/tests-functional.yml +++ b/.github/workflows/tests-functional.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - node-version: [16.x, 18.x, 19.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/tests-latest.yml b/.github/workflows/tests-latest.yml index e76b4fc1..39cc5a74 100644 --- a/.github/workflows/tests-latest.yml +++ b/.github/workflows/tests-latest.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - node-version: [16.x, 18.x, 19.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v2 with: From fb097b80107646f08391d9c1fa406b118bbef829 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:59:20 +0100 Subject: [PATCH 11/14] Refactor: move version helpers --- lib/cli/upgrade.js | 17 +---------------- lib/cli/upgrade/version.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/cli/upgrade.js b/lib/cli/upgrade.js index 4cd49a94..596779d4 100644 --- a/lib/cli/upgrade.js +++ b/lib/cli/upgrade.js @@ -1,25 +1,10 @@ import fs from "fs-extra"; import { INQUIRER_REQUIRED, UPGRADE_REQUIRED } from "./messages.js"; import { v4To5 } from "./upgrade/v4to5.js"; -import { getConfigFormatVersion, getConfigReleaseVersion } from "./upgrade/version.js"; +import { hasUpdate, needsUpgrade, getConfigFormatVersion } from "./upgrade/version.js"; const upgrades = [v4To5]; -function needsUpgrade(confData, confDataDefault) { - const defaultConfFormatVersion = getConfigFormatVersion(confDataDefault); - const userConfFormatVersion = getConfigFormatVersion(confData); - return userConfFormatVersion !== defaultConfFormatVersion; -} - -function hasUpdate(confData, confDataDefault) { - const defaultConfReleaseVersion = getConfigReleaseVersion(confDataDefault); - const userConfReleaseVersion = getConfigReleaseVersion(confData); - if (userConfReleaseVersion !== 0) { - return userConfReleaseVersion !== defaultConfReleaseVersion; - } else { - return false; - } -} function applyUpgrades(userConfData, defaultConfData) { const targetVersion = getConfigFormatVersion(defaultConfData); diff --git a/lib/cli/upgrade/version.js b/lib/cli/upgrade/version.js index 02324ad6..ba9ed205 100644 --- a/lib/cli/upgrade/version.js +++ b/lib/cli/upgrade/version.js @@ -1,6 +1,22 @@ const searchPathVersion = /\/conf(\/v\d\/|.)schema\.json/; const searchTagVersion = /\/glossarify-md\/v([\d.]+){1,}\//; +export function needsUpgrade(givenUserConf, systemDefaultConf) { + const defaultConfFormatVersion = getConfigFormatVersion(systemDefaultConf); + const userConfFormatVersion = getConfigFormatVersion(givenUserConf); + return userConfFormatVersion !== defaultConfFormatVersion; +} + +export function hasUpdate(givenUserConf, systemDefaultConf) { + const defaultConfReleaseVersion = getConfigReleaseVersion(systemDefaultConf); + const userConfReleaseVersion = getConfigReleaseVersion(givenUserConf); + if (userConfReleaseVersion !== 0) { + return userConfReleaseVersion !== defaultConfReleaseVersion; + } else { + return false; + } +} + /** * Lift the configuration *release* version to make a config use newer options. * Does not lift the config *format* version and would not lift the given From a6a068351d894efa34c8778ebbc62722e0d7a513 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:56:02 +0100 Subject: [PATCH 12/14] test: New test case. --- test/lib/config/version.spec.js | 63 +++++++++++++++++++++++++++++++++ test/main.spec.js | 7 ++-- 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/lib/config/version.spec.js diff --git a/test/lib/config/version.spec.js b/test/lib/config/version.spec.js new file mode 100644 index 00000000..3b42b889 --- /dev/null +++ b/test/lib/config/version.spec.js @@ -0,0 +1,63 @@ +import { strictEqual } from "node:assert"; +import { hasUpdate, needsUpgrade } from "../../../lib/cli/upgrade/version.js"; +import { getDefaultConfig } from "../../../lib/model/config.js"; + +/* +(function it_should_fail_when_schema_format_or_format_path_changes() { + const actualFormatPath = currentFormatPath; + const expectedFormatPath = "/conf/v5/schema.json"; + + // const expectedSchemaUrl = "https://raw.githubusercontent.com/about-code/glossarify-md/0/conf/v0/schema.json"; + strictEqual(actualFormatPath, expectedFormatPath, "Changing the config schema format or format path is considered to be a BREAKING CHANGE. Make sure to have an upgrade procedure in place. Make sure to keep the old schema file, schema repository path and raw.githubusercontent.com/about-code/glossarify-md/ file URL available."); +})();*/ + +(function it_should_detect_update_available() { + const config = (version) => { + return { "$schema": version }; + }; + + const oldReleaseVersion = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.0.0/conf/v5/schema.json"); + const newFormatVersion = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.0.0/conf/v6/schema.json"); + const newReleaseVersionBug = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.0.1/conf/v5/schema.json"); + const newReleaseVersionFeat = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.1.0/conf/v5/schema.json"); + const newReleaseVersionMaj = config("https://raw.githubusercontent.com/about-code/glossarify-md/v2.0.0/conf/v5/schema.json"); + const sameOldReleaseVersion = oldReleaseVersion; + + strictEqual(hasUpdate(oldReleaseVersion, newReleaseVersionBug), true, "It should detect newer bugfix versions available."); + strictEqual(hasUpdate(oldReleaseVersion, newReleaseVersionFeat), true, "It should detect newer feature versions available."); + strictEqual(hasUpdate(oldReleaseVersion, newReleaseVersionMaj), true, "It should detect newer major versions available."); + strictEqual(hasUpdate(oldReleaseVersion, sameOldReleaseVersion), false, "It should not detect updates when versions match."); + strictEqual(hasUpdate(newReleaseVersionMaj, oldReleaseVersion), true, "It should suggest updating config when given release version is newer."); + strictEqual(hasUpdate(newReleaseVersionBug, oldReleaseVersion), true, "It should suggest updating config when given release version is newer."); + strictEqual(hasUpdate(newReleaseVersionFeat, oldReleaseVersion), true, "It should suggest updating config when given release version is newer."); + strictEqual(hasUpdate(oldReleaseVersion, newFormatVersion), false, "It should not detect updates when release version did not change but format version changed)."); + strictEqual(hasUpdate(newFormatVersion, oldReleaseVersion), false, "It should not detect updates when release version did not change but format version changed)."); +})(); + +(function it_should_detect_upgrade_available() { + const config = (version) => { + return { + "$schema": version + ,"baseDir": "." + ,"outDir": "/tmp" + }; + }; + + const defaultVersion = getDefaultConfig(); + const sameDefaultVersion = defaultVersion; + + const olderFormatVersionRemote = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.0.0/conf/v1/schema.json"); + const newerFormatVersionRemote = config("https://raw.githubusercontent.com/about-code/glossarify-md/v1.0.0/conf/v10000/schema.json"); + + const olderFormatVersionLocal = config("./node_modules/glossarify-md/conf/v0/schema.json"); + const newerFormatVersionLocal = config("./node_modules/glossarify-md/conf/v10000/schema.json"); + + strictEqual(needsUpgrade(olderFormatVersionRemote, defaultVersion), true); + strictEqual(needsUpgrade(olderFormatVersionLocal, defaultVersion), true); + strictEqual(needsUpgrade(sameDefaultVersion, defaultVersion), false); + strictEqual(needsUpgrade(newerFormatVersionLocal, defaultVersion), true); + strictEqual(needsUpgrade(newerFormatVersionRemote, defaultVersion), true); +})(); + +// (function it_should_fail_when_format_version_greater_release_version() { +// })() diff --git a/test/main.spec.js b/test/main.spec.js index 5eb4a49f..9aeec4ab 100644 --- a/test/main.spec.js +++ b/test/main.spec.js @@ -1,3 +1,4 @@ -export * from "./lib/path/tools.spec.js"; -export * from "./lib/text/tools.spec.js"; -export * from "./lib/model/histogram.spec.js"; +import "./lib/path/tools.spec.js"; +import "./lib/text/tools.spec.js"; +import "./lib/model/histogram.spec.js"; +import "./lib/config/version.spec.js"; \ No newline at end of file From d53822d1c64a12ca46401b282f79141802ae1081 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:57:45 +0100 Subject: [PATCH 13/14] Update test-suite version in package-lock.json --- test/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/package-lock.json b/test/package-lock.json index 4b8033a2..f2a22a99 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "glossarify-md-testsuite", - "version": "6.0.0", + "version": "7.0.0", "license": "MIT", "devDependencies": { "npm-run-all": "^4.1.5", From 1472623fb691d33c75b455598fd4a8331c6dc878 Mon Sep 17 00:00:00 2001 From: about-code <6525873+about-code@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:02:41 +0100 Subject: [PATCH 14/14] Fix linter errors --- test/main.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/main.spec.js b/test/main.spec.js index 9aeec4ab..15fdd5c7 100644 --- a/test/main.spec.js +++ b/test/main.spec.js @@ -1,4 +1,4 @@ import "./lib/path/tools.spec.js"; import "./lib/text/tools.spec.js"; import "./lib/model/histogram.spec.js"; -import "./lib/config/version.spec.js"; \ No newline at end of file +import "./lib/config/version.spec.js";