Extending Featurevisor
Custom parsers
Definitions are YAML by default. Switch the whole project to JSON, or give it a parser for any other format.
YAML#
YAML needs no configuration. Every example in these docs is YAML, and the YAML example project is a complete setup.
JSON#
Set parser to json in the configuration:
module.exports = { parser: 'json',}Every definition is then a .json file. The checkout feature from the Quick start becomes:
{ "description": "Checkout redesign", "bucketBy": "userId", "rules": [ { "key": "netherlands", "conditions": "netherlands", "percentage": 100 }, { "key": "everyone", "conditions": "*", "percentage": 25 } ]}The JSON example project is a complete setup.
Another format#
A custom parser is an object with three properties, and two optional ones for formatting:
| Property | Purpose |
|---|---|
extension | The file extension, such as toml |
parse(content, filePath?) | Turns a file's text into a definition object |
stringify(content, filePath?) | Turns a definition object back into text |
format(source, layout) | Optional. Lays out a file for fmt |
listComments(source) | Optional. Lists a file's comments for fmt |
Both functions are synchronous. The file path is there so a parser can say where an error is. Featurevisor checks that both exist when it loads the configuration. It writes definitions with stringify, for example during promote --apply, so a parser must write as well as read.
For TOML, smol-toml does both:
npm install --save-dev smol-tomlconst toml = require('smol-toml')module.exports = { parser: { extension: 'toml', parse: (content) => toml.parse(content), stringify: (content) => toml.stringify(content), },}The TOML example project is a complete setup.
Pick a format that can hold every value your definitions use. TOML has no null, for example. Commands that write definitions keep the lines they do not change, but a file they cannot merge a change into is written whole through stringify. That can change its formatting and drop comments, so check the Git diff after a command writes definitions.
Format files#
featurevisor fmt lays out a file with its parser's format function. The built-in YAML parser applies the whole house style, and the JSON parser all that JSON can express: key order, indentation and blank lines. A custom parser without format keeps its files as they are, because only the parser knows what its format can keep, such as comments.
format receives the file's text and a layout for its kind of definition, and returns the new text:
layout property | Gives |
|---|---|
indent | Spaces per level of nesting |
orderKeys(path, keys) | The keys of the map at path, in Featurevisor's order |
spaceEntries(path) | Whether the map at path has blank lines between entries |
spaceItems(path) | Whether the list at path has blank lines between items |
keepBlankLines | Whether a blank line the author wrote between two entries or items stays, one at most |
orderValue(value) | A parsed definition with every map's keys in order |
formatText(path, text) | The text of the string at path in house style, such as an expression in conditions, or undefined to keep it |
options | Featurevisor's choices for your file format, when it has some for that extension, such as YAML's quotes. Empty for other formats |
formatText is there in projects with condition expressions on. A parser that writes from values, as this TOML one does, can pass a value through applyFormatText(value, layout.formatText) from @featurevisor/parsers, which applies it to every string.
Apply what your format can express and ignore the rest, since later versions may add to layout. Return undefined to leave a file as it is, and fmt reports it as left by the parser. smol-toml writes values but not comments, so this format orders keys only in a file without a #:
const toml = require('smol-toml')module.exports = { parser: { extension: 'toml', parse: (content) => toml.parse(content), stringify: (content) => toml.stringify(content), format: (source, layout) => source.includes('#') ? undefined : toml.stringify(layout.orderValue(toml.parse(source))), },}A checkout written with its keys out of order:
bucketBy = "userId"description = "Checkout redesign"[[rules]]percentage = 100key = "netherlands"conditions = "netherlands"[[rules]]key = "everyone"percentage = 25conditions = "*"reads like this after npx featurevisor fmt:
description = "Checkout redesign"bucketBy = "userId"[[rules]]key = "netherlands"conditions = "netherlands"percentage = 100[[rules]]key = "everyone"conditions = "*"percentage = 25fmt keeps each file's line endings and ends it with one newline, so format does not need to. It also leaves a file with # featurevisor-ignore at its top as it is, without calling format. In a format with comments, keep a line that ends with # featurevisor-ignore-line as written. @featurevisor/parsers exports both comments as IGNORE_FILE_COMMENT and IGNORE_LINE_COMMENT. It checks that the result parses to the same values and stays the same when formatted again. Give the parser listComments, returning every comment in a file, and fmt checks that none is lost as well. A file that fails a check is left as it is.
format is for fmt, and for convert, which formats a file it rewrites when the file was formatted before. Promote and other commands that write definitions use stringify alone.
Related#
- Configuration
- Projects
- Datasource, to read definitions from somewhere other than files

