Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing tests of config parse - Rebuilt #8

Open
wants to merge 4 commits into
base: additional-config-files
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
#
# This build use extensive use of parameters
# https://circleci.com/docs/reusing-config/#using-the-parameters-declaration
#
# And Conditional Steps
# https://circleci.com/docs/reusing-config/#defining-conditional-steps

version: 2.1

orbs:
node: circleci/node@5.2.0

commands:
project-setup:
parameters:
working-dir:
type: string
node-version:
type: string
windows:
type: boolean
default: false
steps:
- checkout:
path: << parameters.working-dir >>
- when:
condition:
equal: [ true, << parameters.windows >> ]
steps:
- run:
name: nvm-install
command: choco install nvm -y
- run:
name: node-install
command: |
Start-Process powershell -verb runAs -Args "-start GeneralProfile"
nvm install << parameters.node-version >>
nvm use << parameters.node-version >>
- run:
name: npm-install
command: npm ci
- when:
condition:
equal: [ false, << parameters.windows >> ]
steps:
- node/install:
node-version: << parameters.node-version >>
install-yarn: false
- node/install-packages:
check-cache: always
pkg-manager: npm
with-cache: false

lint-test:
steps:
- run:
command: npm run posttest

unit-test:
steps:
- run:
command: npm run test


executors:
linux: # a Linux VM running Ubuntu 20.04
docker:
- image: cimg/base:2024.01
working_directory: /home/circleci/project/c8
macos: # macos executor running Xcode
macos:
xcode: 14.2.0
working_directory: /Users/distiller/project/c8
win:
machine:
image: 'windows-server-2019-vs2019:2023.10.1'
resource_class: windows.medium
shell: powershell.exe -ExecutionPolicy Bypass
working_directory: C:\Users\circleci\project\c8

jobs:
# Refactor to a command
test:
parameters:
os:
type: string
node-version:
type: string
executor: << parameters.os >>
steps:
- when:
condition:
equal: [ linux, << parameters.os >> ]
steps:
- project-setup:
node-version: << parameters.node-version >>
working-dir: /home/circleci/project/c8
- when:
condition:
equal: [ win , << parameters.os >> ]
steps:
- project-setup:
node-version: << parameters.node-version >>
working-dir: C:\Users\circleci\project\c8
windows: true
- when:
condition:
equal: [ macos, << parameters.os >> ]
steps:
- project-setup:
node-version: << parameters.node-version >>
working-dir: /Users/distiller/project/c8
- lint-test
- unit-test

workflows:
matrix-test:
jobs:
- test:
matrix:
parameters:
os: [win, linux, macos]
node-version: ["14.21.3", "16.20.2", "18.19.0", "20.11.0"]
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,25 @@ The above example will output coverage metrics for `foo.js`.

## CLI Options / Configuration

c8 can be configured via command-line flags, a `c8` section in `package.json`, or a JSON configuration file on disk.

A configuration file can be specified by passing its path on the command line with `--config` or `-c`. If no config option is provided, c8 searches for files named `.c8rc`, `.c8rc.json`, `.nycrc`, or `.nycrc.json`, starting from
`cwd` and walking up the filesystem tree.
c8 can be configured via command-line flags, a `c8` section in `package.json`, or a configuration file on disk.

When using `package.json` configuration or a dedicated configuration file, omit the `--` prefix from the long-form of the desired command-line option.

A configuration file can be specified by passing its path on the command line with `--config` or `-c`. If no config option is provided, c8 searches for files named in the table below starting from `cwd` and walking up the filesystem tree.

A robust configuration file naming convention is available in an effort to stay compatible with nyc configuration options and ensure dynamic configuration.

| File name | File Association |
|-----------------------------------------------------------------------------------------------------|--------------------|
| `.c8rc`, `.c8rc.json` | JSON |
| `.c8rc.yml`, `.c8rc.yaml` | YAML |
| `.c8rc.js`, `.c8rc.cjs`, `.c8.config.js`, `.c8.config.cjs`, `c8.config.js`, `c8.config.cjs` | CommonJS export* |
| `.nycrc`, `.nycrc.json` | JSON |
| `.nycrc.yaml`, `.nycrc.yml` | YAML |
| `.nycrc.js`, `.nycrc.cjs`, `nyc.config.js`, `nyc.config.cjs`, `.nyc.config.js`, `.nyc.config.cjs` | CommonJS export* |

For packages written in ESM module syntax, a static configuration option is supported in JSON or YAML syntax. A dynamic configuration is also supported. These configuration files must be written in CommonJS utilizing one of the .cjs file options in the table above. At the moment ESM syntax is not supported for writing c8 configuration files. This may change in the future, but please note, C8 is written in CommonJS syntax.

Here is a list of common options. Run `c8 --help` for the full list and documentation.

| Option | Description | Type | Default |
Expand Down
69 changes: 69 additions & 0 deletions lib/error-reporting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* TODO: Refactor:
*
* Not sure if this the name we want to use for this class
* In the parameters it supports an error that has already
* been thrown, then appends it's own error message
* Is there a more general name we can use like
* AppendableError or MultiError? Is this a good
* Candidate for the decorator design pattern?
*
*
* Note: this is different than javascript Decorators
*/
/**
* To be thrown when there's a problem parsing a configuration file.
*
* More often than not the errors from the parsing engines are opaque and
* unhelpful, so this gives us the opportunity to provide better information
* to the user.
*/
class ConfigParsingError extends Error {
/**
* Constructs the error, given the path and error hints.
*
* @param {string} path The path to the file that had a parsing problem.
* @param {string} errorHints Any user-helpful hints.
* @param {unknown} [originalError] Optional: The original error thrown by the underlying parser.
*/
constructor (path, errorHints, originalError) {
const originalErrorMessage =
originalError instanceof Error
? ` Original error: ${originalError.message}`
: ''

super(
`Error loading configuration from file "${path}": ${errorHints}${originalErrorMessage}`
)

// this.name = ConfigParsingError.name

if (originalError instanceof Error) {
this.stack = originalError.stack
}
}
}

/**
* To be thrown when a file is loaded that is not one of the supported file
* types, especially when the file type is determined from the file extension.
*/
class UnsupportedFileTypeError extends Error {
/**
* Constructs the error, given the path and supported file types.
*
* @param {string} path The path to the file that is not supported.
* @param {string[]} supportedFileTypes An array of supported file types that will help the user understand when they need to do.
*/
constructor (path, supportedFileTypes) {
const types = supportedFileTypes.join(', ')
super(
`Unsupported file type while reading file "${path}". Please make sure your file of one of the following file types: ${types}`
)
// this.name = UnsupportedFileTypeError.name
}
}

module.exports = {
ConfigParsingError,
UnsupportedFileTypeError
}
134 changes: 134 additions & 0 deletions lib/load-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const { readFileSync } = require('fs')
const { extname, basename } = require('path')
const JsYaml = require('js-yaml')

const {
UnsupportedFileTypeError,
ConfigParsingError
} = require('./error-reporting')

const CONFIG_FILE_NAMES = Object.freeze([
'.c8rc',
'.c8rc.json',
'.c8rc.yml',
'.c8rc.yaml',
'.c8rc.js',
'.c8rc.cjs',
'.c8.config.js',
'.c8.config.cjs',
'c8.config.js',
'c8.config.cjs',
'.nycrc',
'.nycrc.json',
'.nycrc.yml',
'.nycrc.yaml',
'.nyc.config.js',
'.nyc.config.cjs',
'nyc.config.js',
'nyc.config.cjs'
])

const JS_EXTS = Object.freeze(['.js', '.cjs'])
const JSON_EXTS = Object.freeze(['.json'])
const YAML_EXTS = Object.freeze(['.yml', '.yaml'])

const NO_EXPORTS = Symbol('no exports')

/**
* Loads a configuration file of whatever format from the given path.
*
* @param {string} path The path to load the configuration from.
* @param {readFile(path: string) => string, readJs(path: string) => C8Config} [_di] For test suite use only. Do not use.
* @returns {object} An object containing
* @throws {UnsupportedFileTypeError} When the given configuration file is of a type that is unsupported.
* @throws {ConfigParsingError} When the configuration file fails to be read. E.g. a syntax error, such as not using quoted keys in JSON.
*/
function loadConfigFile (path, _di) {
const di = {
// A variation of the Dependency Injection pattern that allows the test suites to overide any of these functions with mocks.
readFile: (path) => readFileSync(path, 'utf8'),
readJs: (path) => require(path),
..._di // Note that making the DI argument a hidden argument by using the `arguments` array isn't a viable option in TypeScript, so this has been written in a way that is compatible with that.
}

let config

const fileName = basename(path)
const ext = extname(path).toLowerCase()

if (YAML_EXTS.includes(ext)) {
try {
// TODO: add YAML schema so that we get better errors for YAML users.
config = JsYaml.load(di.readFile(path))
} catch (error) {
if (error instanceof JsYaml.YAMLException) {
throw new ConfigParsingError(
path,
'must contain a valid c8 configuration object.',
error
)
}

throw error
}

if (!config) {
// TODO: remove this check once we get YAML schema validation.
throw new ConfigParsingError(path, 'invalid configuration')
}
} else if (JS_EXTS.includes(ext)) {
// Add a loader that allows us to check to see if anything was ever exported.
// Thank you to https://stackoverflow.com/a/70999950 for the inspiration. Please note that this code won't port to TypeScript nicely.
const extensions = module.constructor._extensions
const cjsLoader = extensions['.cjs']
const jsLoader = extensions['.js']
extensions['.cjs'] = extensions['.js'] = (module, filename) => {
module.exports[NO_EXPORTS] = filename
jsLoader(module, filename)
}
try {
config = di.readJs(path)
} finally {
// Undo the global state mutation, even if an error was thrown.
extensions['.cjs'] = cjsLoader
extensions['.js'] = jsLoader
}
if (NO_EXPORTS in config) {
throw new ConfigParsingError(
path,
'does not export a c8 configuration object.'
)
}
} else if (JSON_EXTS.includes(ext) || CONFIG_FILE_NAMES.includes(fileName)) {
try {
config = JSON.parse(di.readFile(path))
} catch (error) {
if (error instanceof SyntaxError) {
throw new ConfigParsingError(
path,
'must contain a valid c8 configuration object.',
error
)
}

throw error
}
} else {
// If the user supplied a bad configuration file that we can't figure out how to read, then it's on them to solve it.
// Attempting to use some other config, even a default one, will result in unexpected behavior: aka ignoring the config that was explicitly specified is not intuitive.
throw new UnsupportedFileTypeError(path, [
...JSON_EXTS,
...JS_EXTS,
...YAML_EXTS
])
}

// TODO: validate the object schema so that we get validation of JS-like configs. Might want to refactor the above test cases so that the YAML isn't being validated twice.

return config
}

module.exports = {
CONFIG_FILE_NAMES,
loadConfigFile
}
9 changes: 5 additions & 4 deletions lib/parse-args.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
const defaultExclude = require('@istanbuljs/schema/default-exclude')
const defaultExtension = require('@istanbuljs/schema/default-extension')
const findUp = require('find-up')
const { readFileSync } = require('fs')
const Yargs = require('yargs/yargs')
const { applyExtends } = require('yargs/helpers')
const parser = require('yargs-parser')
const { resolve } = require('path')

const { CONFIG_FILE_NAMES, loadConfigFile } = require('./load-config')

function buildYargs (withCommands = false) {
const yargs = Yargs([])
.usage('$0 [opts] [script] [opts]')
.options('config', {
alias: 'c',
config: true,
describe: 'path to JSON configuration file',
describe: 'path to configuration file',
configParser: (path) => {
const config = JSON.parse(readFileSync(path))
const config = loadConfigFile(path)
return applyExtends(config, process.cwd(), true)
},
default: () => findUp.sync(['.c8rc', '.c8rc.json', '.nycrc', '.nycrc.json'])
default: () => findUp.sync(CONFIG_FILE_NAMES)
})
.option('reporter', {
alias: 'r',
Expand Down
Loading