Skip to content

Commit

Permalink
Merge pull request #522 from chris48s/512-config-location
Browse files Browse the repository at this point in the history
allow specifying config file location
  • Loading branch information
chris48s authored Oct 24, 2024
2 parents ec0ff20 + 54b4e72 commit 4402485
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 92 deletions.
9 changes: 7 additions & 2 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ v8r uses [CosmiConfig](https://www.npmjs.com/package/cosmiconfig) to search for
- `v8r.config.js`
- `v8r.config.cjs`

v8r only searches for a config file in the current working directory.
By default, v8r searches for a config file in the current working directory.

If you want to load a config file from another location, you can invoke v8r with the `V8R_CONFIG_FILE` environment variable. All patterns and relative paths in the config file will be resolved relative to the current working directory rather than the config file location, even if the config file is loaded from somewhere other than the current working directory.

Example yaml config file:

```yaml title=".v8rrc.yml"
# - One or more filenames or glob patterns describing local file or files to validate
# Patterns are resolved relative to the current working directory.
# - overridden by passing one or more positional arguments
patterns: ['*.json']

Expand Down Expand Up @@ -59,13 +62,14 @@ customCatalog:
description: Custom Schema # A description of the schema (optional)

# A Minimatch glob expression for matching up file names with a schema (required)
# Expressions are resolved relative to the current working directory.
fileMatch: ["*.geojson"]

# A URL or local file path for the schema location (required)
# Unlike the SchemaStore.org format, which has a `url` key,
# custom catalogs defined in v8r config files have a `location` key
# which can refer to either a URL or local file.
# Relative paths are interpreted as relative to the config file location.
# Relative paths are resolved relative to the current working directory.
location: foo/bar/geojson-schema.json

# A custom parser to use for files matching fileMatch
Expand All @@ -80,6 +84,7 @@ plugins:
# Plugins installed from NPM (or JSR) must be prefixed by "package:"
- "package:v8r-plugin-emoji-output"
# Plugins in the project dir must be prefixed by "file:"
# Relative paths are resolved relative to the current working directory.
- "file:./subdir/my-local-plugin.mjs"
```
Expand Down
44 changes: 19 additions & 25 deletions src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const require = createRequire(import.meta.url);

import { cosmiconfig } from "cosmiconfig";
import decamelize from "decamelize";
import isUrl from "is-url";
import fs from "fs";
import path from "path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
Expand All @@ -17,27 +17,28 @@ import {
import logger from "./logger.js";
import { loadAllPlugins, resolveUserPlugins } from "./plugins.js";

function preProcessConfig(configFile) {
if (!configFile?.config?.customCatalog?.schemas) {
return;
}
for (const schema of configFile.config.customCatalog.schemas) {
if (!path.isAbsolute(schema.location) && !isUrl(schema.location)) {
schema.location = path.join(
path.dirname(configFile.filepath),
schema.location,
);
async function getCosmiConfig(cosmiconfigOptions) {
let configFile;

if (process.env.V8R_CONFIG_FILE) {
if (!fs.existsSync(process.env.V8R_CONFIG_FILE)) {
throw new Error(`File ${process.env.V8R_CONFIG_FILE} does not exist.`);
}
configFile = await cosmiconfig("v8r", cosmiconfigOptions).load(
process.env.V8R_CONFIG_FILE,
);
} else {
cosmiconfigOptions.stopDir = process.cwd();
configFile = (await cosmiconfig("v8r", cosmiconfigOptions).search(
process.cwd(),
)) || { config: {} };
}
}

async function getCosmiConfig(cosmiconfigOptions) {
cosmiconfigOptions.stopDir = process.cwd();
const configFile = (await cosmiconfig("v8r", cosmiconfigOptions).search(
process.cwd(),
)) || { config: {} };
if (configFile.filepath) {
logger.info(`Loaded config file from ${getRelativeFilePath(configFile)}`);
logger.info(
`Patterns and relative paths will be resolved relative to current working directory: ${process.cwd()}`,
);
} else {
logger.info(`No config file found`);
}
Expand Down Expand Up @@ -200,7 +201,6 @@ async function bootstrap(argv, config, cosmiconfigOptions = {}) {
// we can finish validating and processing the config
validateConfigDocumentParsers(configFile, documentFormats);
validateConfigOutputFormats(configFile, outputFormats);
preProcessConfig(configFile);

// parse command line arguments
const args = parseArgs(argv, configFile, documentFormats, outputFormats);
Expand All @@ -213,10 +213,4 @@ async function bootstrap(argv, config, cosmiconfigOptions = {}) {
};
}

export {
bootstrap,
getDocumentFormats,
getOutputFormats,
parseArgs,
preProcessConfig,
};
export { bootstrap, getDocumentFormats, getOutputFormats, parseArgs };
83 changes: 19 additions & 64 deletions src/bootstrap.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import assert from "assert";
import path from "path";
import {
bootstrap,
getDocumentFormats,
getOutputFormats,
parseArgs,
preProcessConfig,
} from "./bootstrap.js";
import { loadAllPlugins } from "./plugins.js";
import { setUp, tearDown, logContainsInfo } from "./test-helpers.js";
Expand Down Expand Up @@ -149,57 +147,6 @@ describe("parseArgs", function () {
});
});

describe("preProcessConfig", function () {
it("passes through absolute paths", function () {
const configFile = {
config: {
customCatalog: { schemas: [{ location: "/foo/bar/schema.json" }] },
},
filepath: "/home/fred/.v8rrc",
};
preProcessConfig(configFile);
assert.equal(
configFile.config.customCatalog.schemas[0].location,
"/foo/bar/schema.json",
);
});

it("passes through URLs", function () {
const configFile = {
config: {
customCatalog: {
schemas: [{ location: "https://example.com/schema.json" }],
},
},
filepath: "/home/fred/.v8rrc",
};
preProcessConfig(configFile);
assert.equal(
configFile.config.customCatalog.schemas[0].location,
"https://example.com/schema.json",
);
});

it("converts relative paths to absolute", function () {
const testCases = [
["schema.json", "/home/fred/schema.json"],
["../../schema.json", "/schema.json"],
["foo/bar/schema.json", "/home/fred/foo/bar/schema.json"],
];
for (const testCase of testCases) {
const configFile = {
config: { customCatalog: { schemas: [{ location: testCase[0] }] } },
filepath: "/home/fred/.v8rrc",
};
preProcessConfig(configFile);
assert.equal(
configFile.config.customCatalog.schemas[0].location,
testCase[1],
);
}
});
});

describe("getConfig", function () {
beforeEach(function () {
setUp();
Expand All @@ -213,10 +160,7 @@ describe("getConfig", function () {
const { config } = await bootstrap(
["node", "index.js", "infile.json"],
undefined,
{
searchPlaces: ["./testfiles/does-not-exist.json"],
cache: false,
},
{ cache: false },
);
assert.equal(config.ignoreErrors, false);
assert.equal(config.cacheTtl, 600);
Expand All @@ -229,9 +173,22 @@ describe("getConfig", function () {
assert(logContainsInfo("No config file found"));
});

it("should throw if V8R_CONFIG_FILE does not exist", async function () {
process.env.V8R_CONFIG_FILE = "./testfiles/does-not-exist.json";
await assert.rejects(
bootstrap(["node", "index.js", "infile.json"], undefined, {
cache: false,
}),
{
name: "Error",
message: "File ./testfiles/does-not-exist.json does not exist.",
},
);
});

it("should read options from config file if available", async function () {
process.env.V8R_CONFIG_FILE = "./testfiles/configs/config.json";
const { config } = await bootstrap(["node", "index.js"], undefined, {
searchPlaces: ["./testfiles/configs/config.json"],
cache: false,
});
assert.equal(config.ignoreErrors, true);
Expand All @@ -246,7 +203,7 @@ describe("getConfig", function () {
{
name: "custom schema",
fileMatch: ["valid.json", "invalid.json"],
location: path.resolve("./testfiles/schemas/schema.json"),
location: "./testfiles/schemas/schema.json",
parser: "json5",
},
],
Expand All @@ -261,6 +218,7 @@ describe("getConfig", function () {
});

it("should override options from config file with args if specified", async function () {
process.env.V8R_CONFIG_FILE = "./testfiles/configs/config.json";
const { config } = await bootstrap(
[
"node",
Expand All @@ -272,10 +230,7 @@ describe("getConfig", function () {
"-vv",
],
undefined,
{
searchPlaces: ["./testfiles/configs/config.json"],
cache: false,
},
{ cache: false },
);
assert.deepStrictEqual(config.patterns, ["infile.json"]);
assert.equal(config.ignoreErrors, true);
Expand All @@ -289,7 +244,7 @@ describe("getConfig", function () {
{
name: "custom schema",
fileMatch: ["valid.json", "invalid.json"],
location: path.resolve("./testfiles/schemas/schema.json"),
location: "./testfiles/schemas/schema.json",
parser: "json5",
},
],
Expand Down
3 changes: 3 additions & 0 deletions src/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import logger from "./logger.js";
const origWriteOut = logger.writeOut;
const origWriteErr = logger.writeErr;
const testCacheName = process.env.V8R_CACHE_NAME;
const env = process.env;

function setUp() {
flatCache.clearCacheById(testCacheName);
logger.resetStdout();
logger.resetStderr();
logger.writeOut = function () {};
logger.writeErr = function () {};
process.env = { ...env };
}

function tearDown() {
Expand All @@ -19,6 +21,7 @@ function tearDown() {
logger.resetStderr();
logger.writeOut = origWriteOut;
logger.writeErr = origWriteErr;
process.env = env;
}

function isString(el) {
Expand Down
2 changes: 1 addition & 1 deletion testfiles/configs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "custom schema",
"fileMatch": ["valid.json", "invalid.json"],
"location": "../schemas/schema.json",
"location": "./testfiles/schemas/schema.json",
"parser": "json5"
}
]
Expand Down

0 comments on commit 4402485

Please sign in to comment.