@report-toolkit/core
Version:
See docs at [https://ibm.github.io/report-toolkit](https://ibm.github.io/report-toolkit)
692 lines (642 loc) • 23.4 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var common = require('@report-toolkit/common');
var diff$2 = require('@report-toolkit/diff');
var inspector = require('@report-toolkit/inspector');
var transformers = require('@report-toolkit/transformers');
var resolveFrom = _interopDefault(require('resolve-from'));
const {
parseConfig,
BUILTIN_CONFIGS,
RECOMMENDED_CONFIG_ALIAS
} = common.config;
const {
createRTkError,
RTKERR_INVALID_PARAMETER
} = common.error;
const {
defaultIfEmpty,
concat,
defer,
filter,
from,
fromAny,
iif,
map,
mapTo,
mergeAll,
mergeMap,
of,
pipeIf,
pluck,
share,
sort,
take,
tap,
toArray,
toObjectFromJSON
} = common.observable;
const {
SEVERITIES,
DEFAULT_SEVERITY
} = common.constants;
const debug = common.createDebugPipe('core', 'observable');
const BUILTIN_PLUGINS = ['@report-toolkit/inspector'];
const getRuleDefinitions = common._.pipe(common._.toPairs, common._.fromPairs, common._.values, common._.flatMap('rules'), common._.compact);
/**
* @type {Readonly<Partial<InspectOptions>>}
*/
const DEFAULT_INSPECT_OPTIONS = Object.freeze({
sort: true,
severity: DEFAULT_SEVERITY,
showSecretsUnsafe: false,
sortDirection: 'asc',
sortField: 'header.dumpEventTimestamp'
});
/**
* @type {Map<string,RTKPlugin>}
*/
const registeredPlugins = new Map();
/**
* @param {import('@report-toolkit/common/src/report').ReportLike|string} value - Report as JSON string or parsed report
* @param {any} [opts]
*/
function reportFrom(value, opts = {}) {
return common._.isString(value) ? defer(() => reportFromJSON(value, opts)) : defer(() => reportFromObject(value, opts));
}
/**
*
* @param {string} json - Report as JSON string
* @param {Partial<InspectOptions>} [opts]
*/
function reportFromJSON(json, opts = {}) {
return of(json).pipe(toObjectFromJSON(), toReportFromObject(opts));
}
/**
*
* @param {import('@report-toolkit/common/src/report').ReportLike} obj - Raw parsed report object
* @param {Partial<InspectOptions>} [opts]
*/
function reportFromObject(obj, opts = {}) {
return of(obj).pipe(toReportFromObject(opts));
}
/**
*
* @param {Partial<InspectOptions>} [opts]
* @returns {import('rxjs').OperatorFunction<string|object,import('@report-toolkit/common').Report>}
*/
function toReport(opts = {}) {
return observable => observable.pipe(pipeIf(
/**
* @param {any} value
*/
value => !common.isReport(value), mergeMap(value => reportFrom(value, opts))));
}
/**
*
* @param {Partial<DiffOptions>} [opts]
* @returns {import('rxjs').OperatorFunction<import('@report-toolkit/common/src/report').Report[],object>}
*/
function toReportDiff(opts = {}) {
return reports => reports.pipe(take(2), toReport(), toArray(), tap(reports => {
if (reports.length < 2) {
throw createRTkError(RTKERR_INVALID_PARAMETER, 'Two reports are required!');
}
}), diff$2.diff(opts));
}
/**
*
* @param {object} [config] - Raw rule configuration
* @hidden
* @returns {import('rxjs').OperatorFunction<import('@report-toolkit/inspector/src/rule').RuleDefinition,import('@report-toolkit/inspector/src/rule-config').RuleConfig>}
*/
function toRuleConfig(config = {}) {
const hasEnabledRules = common._.some(Boolean, config.rules || {});
return ruleDefs => ruleDefs.pipe(pipeIf(!hasEnabledRules, debug(() => 'enabling all rules by default')), pipeIf(hasEnabledRules, filter(({
id
}) => Boolean(common._.get(id, config.rules)))), map(ruleDefinition => inspector.createRule(ruleDefinition).toRuleConfig(config)));
}
/**
* Returns the difference between two reports.
*
* Example:
*
* ```js
* const {diff} = require('@report-toolkit/core').observable;
*
* const report1 = process.report.getReport();
* const report2 = process.report.getReport();
*
* diff(report1, report2, {
* filterProperties: ['header', 'javascriptStack', 'nativeStack'],
* showSecretsUnsafe: false
* }).subscribe(({op, path, newValue, oldValue}) => {
* console.log(`[${op}] <${path}> ${oldValue} => ${newValue}`);
* })
* ```
* @param {import('@report-toolkit/common/src/report').ReportLike|import('rxjs').Observable<import('@report-toolkit/common/src/report').ReportLike>} report1 - First report to diff
* @param {import('@report-toolkit/common/src/report').ReportLike|import('rxjs').Observable<import('@report-toolkit/common/src/report').ReportLike>} report2 - Second report to diff
* @param {Partial<DiffOptions>} [opts] Options
* @returns {import('rxjs').Observable<DiffResult>} Results, one per difference
* @todo support JSON reports
*/
function diff(report1, report2, opts = {}) {
return concat(fromAny(report1), fromAny(report2)).pipe(toReportDiff(opts));
}
/**
* Inspect one or more reports, running rules against each. Resolves with an array of zero or more {@link @report-toolkit/inspector.message.Message|Messages}.
*
* Example:
*
* ```js
* const {inspect} = require('@report-toolkit/core').observable
*
* const report = process.report.getReport();
* inspect(report, {
* severity: 'info',
* sort: true,
* sortDirection: 'asc',
* sortField: 'header.dumpEventTimestamp',
* showSecretsUnsafe: false,
* ruleConfig: {
* 'long-timeout': {
* timeout: '2s'
* }
* }
* }).subscribe({message, filename} => {
* console.log(`${filename}: ${message}`);
* });
* ```
* @param {import('@report-toolkit/common/src/report').ReportLike|import('rxjs').Observable<import('@report-toolkit/common/src/report').ReportLike>} reports - One or more Reports
* @param {Partial<InspectOptions>} [opts] - Options
* @returns {import('rxjs').Observable<import('@report-toolkit/inspector/src/message').Message>}
*/
function inspect(reports, opts = {}) {
const {
ruleConfig: rules,
severity,
sort: sortReports,
sortField,
sortDirection,
showSecretsUnsafe
} = common._.defaults(DEFAULT_INSPECT_OPTIONS, opts);
return fromRegisteredRuleDefinitions().pipe(toRuleConfig({
rules
}), inspector.inspectReports(fromAny(reports).pipe(toReport({
showSecretsUnsafe
}), pipeIf(sortReports, sort(sortField, sortDirection)), share())), filter(common._.pipe(common._.get('severity'), common._.get(common._.__, SEVERITIES), common._.gte(common._.__, SEVERITIES[severity]))));
}
/**
* Emits normalized config objects from raw config objects. Only a single input config should be necessary.
*
* Example:
*
* ```js
* const {loadConfig} = require('@report-toolkit/core').observable;
*
* // or require('./path/to/.rtkrc.js')
* const rawConfig = [
* 'report-toolkit:recommended',
* {
* rules: {
* 'long-timeout': {
* timeout: '2s'
* }
* }
* }
* ];
*
* loadConfig(rawConfig).subscribe(normalizedConfig => {
* // `normalizedConfig` contains contents of "recommended" settings,
* // with our override of custom rule config
* });
* ```
* @param {object} config - Raw config object
* @todo ALWAYS load builtin plugin(s)
* @todo Document config shape
* @returns {import('rxjs').Observable<Config>} Normalized config object(s)
*/
function loadConfig(config) {
return fromAny(config).pipe(defaultIfEmpty(BUILTIN_CONFIGS.get(RECOMMENDED_CONFIG_ALIAS)), debug(config => ['received raw config %O', config]), parseConfig(), pipeIf(
/**
@param {object} config
*/
config => common._.isEmpty(config.plugins), debug(() => 'no plugins specified; using default set'), map(config => ({ ...config,
plugins: BUILTIN_PLUGINS
}))), mergeMap(config => from(config.plugins).pipe(mergeMap(use), mapTo(config))), debug(config => ['final rtk config: %O', config]));
}
/**
* Given a list of transformer IDs, create an `Observable` which emits {@link TransformerBlueprint} objects. Output should be piped to {@link transform}.
*
* Example:
*
* ```js
* const {fromTransformerChain} = require('@report-toolkit/core').observable;
*
* fromTransformerChain(['filter', 'csv'], {
* transformers: {
* filter: {include: 'header'},
* csv: {flatten: true}
* }
* }); // pipe to transform()
* ```
* @param {string[]|string} transformerIds - List of Transformer IDs
* @param {Partial<Config>} [config] - Normalized config object
* @returns {import('rxjs').Observable<TransformerBlueprint>}
*/
function fromTransformerChain(transformerIds, config = {}) {
return fromAny(transformerIds).pipe(map(id => ({
id,
config: common._.merge(common._.omit('transformers', config), common._.getOr({}, `transformers.${id}`, config))
})), debug(blueprint => [`created transformer blueprint %O`, blueprint]));
}
/**
* Run `source` through chain of one or more transformers. Pipe {@link fromTransformerChain} into this.
* Performs validation before piping.
* While most other functions here will automatically convert a raw report into a `Report` instance for further processing, this one does not (since transformers don't necessarily accept them). You'll need to do this manually, as seen in the below example.
* If the final transformer does not output the desired `endType`, the `defaultTransformer` will be appended to the chain; otherwise it is ignored.
*
* Example:
*
* ```js
* const {fromTransformerChain, transform, toReportFromObject} = require('@report-toolkit/core').observable;
*
* const report$ = toReportFromObject(process.report.getReport());
* fromTransformerChain(['filter', 'csv'], {
* transformers: {
* filter: {include: 'header'},
* csv: {flatten: true}
* }
* }).pipe(transform(report$)).subscribe(line => {
* console.log(line);
* });
* ```
* @param {import('rxjs').Observable<any>} source - Source data to transform. Objects, {@link @report-toolkit/common.report.Report|Reports}, etc.
* @param {Partial<TransformOptions>} [opts] - Options for the transformation
* @returns {import('rxjs').OperatorFunction<TransformerBlueprint,any>} Result of running `source` through the transformer chains.
*/
function transform(source, opts = {}) {
return observable => observable.pipe(transformers.toTransformer(), transformers.validateTransformerChain(opts), transformers.runTransformer(source));
}
/**
* Creates a target `Observable` of {@link @report-toolkit/common.report.Report|Report} objects from a source `Observable` of plain objects (usually parsed from a JSON report).
*
* Example:
*
* ```js
* const {toReportFromObject} = require('@report-toolkit/core').observable;
*
* const json = fs.readFileSync('./report-xxxxx.json');
* toReportFromObject(json, {
* showSecretsUnsafe: false
* }).subscribe(report => {
* // `Report` instance with secrets redacted
* });
* ```
* @param {Partial<ToReportFromObjectOptions>} [opts] - Options
*/
function toReportFromObject(opts = {}) {
return inspector.toReportFromObject(opts);
}
/**
* Get a list of rule definitions contained within registered plugins.
*
* Example:
*
* ```js
* const {registeredRuleDefinitions} = require('@report-toolkit/core').observable;
*
* registeredRuleDefinitions().forEach(ruleDef => {
* console.log(ruleDef.meta.id);
* })
* ```
* @returns {import('@report-toolkit/inspector/src/rule').RuleDefinition[]}
*/
function registeredRuleDefinitions() {
// XXX: this is this way because typedoc
return getRuleDefinitions(registeredPlugins);
}
/**
* @hidden
* @todo XXX this is not the right place to load the builtins, and it's essentially redundant with {@link registeredRuleDefinitions}.
* @returns {import('rxjs').Observable<import('@report-toolkit/inspector/src/rule').RuleDefinition>}
*/
function fromRegisteredRuleDefinitions() {
return iif(() => Boolean(registeredPlugins.size), common._.pipe(common._.toPairs, common._.fromPairs, common._.values, from)(registeredPlugins), from(BUILTIN_PLUGINS).pipe(mergeMap(use))).pipe(pluck('rules'), mergeAll());
}
/**
* Register & enable a plugin.
*
* Example:
*
* ```js
* const {use} = require('@report-toolkit/core').observable;
*
* use('some-plugin-in-node_modules').subscribe();
*
* ```
* @param {string} pluginId - ID of plugin to register; a resolvable path to a module
* @returns {import('rxjs').Observable<RTKPlugin>} A plugin instance, but YAGNI.
*/
function use(pluginId) {
return iif(() => isPluginRegistered(pluginId), of(registeredPlugins.get(pluginId)), of(pluginId).pipe(debug(id => `trying to resolve plugin "${id}"`), map(id => {
try {
return require.resolve(id);
} catch (ignored) {
return resolveFrom(process.cwd(), id);
}
}), debug(pluginPath => `resolved plugin "${pluginId}" to ${pluginPath}`), map(require), tap(plugin => {
registeredPlugins.set(pluginId, plugin);
}), pipeIf(common._.has('rules'), debug(plugin => `found ${plugin.rules.length} rules within plugin "${pluginId}"`))));
}
/**
* De-register ("unload") all plugins.
*
* Example:
*
* ```js
* const {deregisterPlugins} = require('@report-toolkit/core').observable;
*
* console.log(deregisterPlugins()); // `true` or `false`, depending.
* ```
* @returns {boolean} `true` if plugins were cleared; `false` if none registered
*/
function deregisterPlugins() {
if (registeredPlugins.size) {
registeredPlugins.clear();
return true;
}
return false;
}
/**
* Returns `true` if plugin with id `pluginId` has already been registered.
*
* ```js
* const {isPluginRegistered} = require('@report-toolkit/core').observable;
*
* console.log(isPluginRegistered('my-plugin')); // `true` or `false`, depending.
* ```
* @param {string} pluginId - A unique [module ID](https://nodejs.org/api/modules.html#modules_module_id)
*/
function isPluginRegistered(pluginId) {
return registeredPlugins.has(pluginId);
}
/**
* Options for {@link transform}.
* @typedef {object} TransformOptions
* @property {string} beginWith - Begin transformer chain with this type
* @property {string} endWith - End transformer chain with this type
* @property {string} defaultTransformer - Default transformer
* @property {object} defaultTransformerConfig - Default transformer config
*/
/**
* Represents a "plugin". As of this writing, plugins may only contain rule definitions for {@link inspect}; it would make sense to add support for transformers, as well.
* @todo write plugin docs
* @typedef {object} RTKPlugin
* @property {import('@report-toolkit/inspector/src/rule').RuleDefinition[]?} rules - An array of rule definitions.
*/
/**
* Options for {@link inspect}.
* @typedef {Object} InspectOptions
* @property {boolean} sort - Whether or not to sort output when multiple reports are provided
* @property {string} severity - Filter by message severity
* @property {"asc"|"desc"} sortDirection - Ascending or descending
* @property {string} sortField - Field to sort by; keypaths are allowed
* @property {object} ruleConfig - Rule configuration object
* @property {boolean} showSecretsUnsafe - If `true`, do not redact secrets
*/
/**
* A single difference between two reports. Emitted from {@link diff}.
* @typedef {object} DiffResult
* @property {"add"|"remove"|"replace"} op - Operation
* @property {string} path - [RFC6902](https://tools.ietf.org/html/rfc6902)-style keypath
* @property {string|boolean|number|null?} value - Value from second report (where applicable)
* @property {string|boolean|number|null?} oldValue - Value from first report (where applicable)
*/
/**
* Options for {@link diff}.
* @typedef {object} DiffOptions
* @property {string[]} includeProperties - Include only these keypaths in the diff
* @property {string[]} excludeProperties - Exclude these keypaths from the diff
* @property {boolean} includeAll - Just show the whole diff if `true`
* @property {boolean} showSecretsUnsafe - If `true`, do not redact secrets
*/
/**
* A "normalized" configuration object.
* @typedef {object} Config
* @todo Describe shape
*/
/**
* A pairing of a transformer ID and a configuration of that transformer, to be
* ingested by {@link transform}.
* @typedef {object} TransformerBlueprint
* @property {string} id - Transformer ID
* @property {object?} config - Configuration for transformer
*/
/**
* Options for {@link toReportFromObject}.
* @typedef {object} ToReportFromObjectOptions
* @property {boolean} showSecretsUnsafe - If `true`, do not redact secrets
*/
var observable = /*#__PURE__*/Object.freeze({
__proto__: null,
toRuleConfig: toRuleConfig,
diff: diff,
inspect: inspect,
loadConfig: loadConfig,
fromTransformerChain: fromTransformerChain,
transform: transform,
compatibleTransformers: transformers.compatibleTransformers,
builtinTransformerIds: transformers.builtinTransformerIds,
toReportFromObject: toReportFromObject,
registeredRuleDefinitions: registeredRuleDefinitions,
fromRegisteredRuleDefinitions: fromRegisteredRuleDefinitions,
use: use,
deregisterPlugins: deregisterPlugins,
isPluginRegistered: isPluginRegistered
});
const {
fromAny: fromAny$1,
of: of$1,
toArray: toArray$1
} = common.observable;
/**
* Returns the difference between two reports.
*
* Example:
*
* ```js
* const {diff} = require('@report-toolkit/core');
*
* const report1 = process.report.getReport();
* const report2 = process.report.getReport();
* const results = await diff(report1, report2, {
* filterProperties: ['header', 'javascriptStack', 'nativeStack'],
* showSecretsUnsafe: false
* });
* results.forEach(({op, path, newValue, oldValue}) => {
* console.log(`[${op}] <${path}> ${oldValue} => ${newValue}`);
* });
*```
* @param {import('@report-toolkit/common/src/report').ReportLike} report1 - First report to diff
* @param {import('@report-toolkit/common/src/report').ReportLike} report2 - Second report to diff
* @param {Partial<import('./observable').DiffOptions>} [opts] Options
* @returns {Promise<import('./observable').DiffResult[]>} Array of results, one per difference
*/
async function diff$1(report1, report2, opts = {}) {
return diff(report1, report2, opts).pipe(toArray$1()).toPromise();
}
/**
* Inspect one or more reports, running rules against each. Resolves with an array of zero or more {@link @report-toolkit/inspector.message.Message|Messages}.
*
* Example:
*
* ```js
* const {inspect} = require('@report-toolkit/core');
*
* const report = process.report.getReport();
* const results = await inspect(report, {
* severity: 'info',
* sort: true,
* sortDirection: 'asc',
* sortField: 'header.dumpEventTimestamp',
* showSecretsUnsafe: false,
* ruleConfig: {
* 'long-timeout': {
* timeout: '2s'
* }
* }
* });
* results.forEach(({message, filename}) => {
* console.log(`${filename}: ${message}`);
* });
```
* @param {import('@report-toolkit/common/src/report').ReportLike} reports - One or more reports
* @param {Partial<import('./observable').InspectOptions>} [opts] - Options
* @returns {Promise<import('@report-toolkit/inspector/src/message').Message[]>}
*/
async function inspect$1(reports, opts = {}) {
return inspect(reports, opts).pipe(toArray$1()).toPromise();
}
/**
* Resolves with a normalized config object from a raw config object.
*
* Example:
*
* ```js
* const {loadConfig} = require('@report-toolkit/core');
*
* // or require('./path/to/.rtkrc.js')
* const rawConfig = [
* 'report-toolkit:recommended',
* {
* rules: {
* 'long-timeout': {
* timeout: '2s'
* }
* }
* }
* ];
* // `normalizedConfig` contains contents of "recommended" settings,
* // with our override of custom rule config
* const normalizedConfig = await loadConfig(rawConfig);
* ```
* @param {object} config - Raw config object
* @returns {Promise<import('./observable').Config>} A normalized config object
*/
async function loadConfig$1(config) {
return loadConfig(config).toPromise();
}
/**
* Convert a plain object (usually parsed from a JSON report generated by See [process.report.writeReport](https://nodejs.org/api/process.html#process_process_report_writereport_filename_err)) to a {@link @report-toolkit/common.report.Report|Report} instance.
*
* Example:
*
* ```js
* const {toReportFromObject} = require('@report-toolkit/core');
*
* const json = fs.readFileSync('./report-xxxxx.json');
* // `Report` instance with secrets redacted
* const report = await toReportFromObject(json, {
* showSecretsUnsafe: false
* });
* ```
* @param {object} value - Raw report
* @param {Partial<import('./observable').ToReportFromObjectOptions>} [opts]
*/
async function toReportFromObject$1(value, opts = {}) {
return of$1(value).pipe(toReportFromObject(opts)).toPromise();
}
/**
* Register & enable a plugin.
*
* Example:
*
* ```js
* const {use} = require('@report-toolkit/core');
*
* await use('some-plugin-in-node_modules');
* await use('./relative/path/to/plugin.js');
* ```
* @param {string} pluginId - ID of plugin to register; a resolvable path to a module
* @returns {Promise<import('./observable').RTKPlugin>}
*/
async function use$1(pluginId) {
return use(pluginId).toPromise();
}
/**
* Run `source` through chain of one or more transformers. Performs validation before piping.
* If the final transformer does not output the desired `endType`, the `defaultTransformer` will be appended to the chain; otherwise it is ignored.
*
* Example:
*
* ```js
* const {transform, toReportFromObject} = require('@report-toolkit/core');
*
* // by default, `report` has its secrets redacted
* const report = await toReportFromObject(process.report.getReport());
* const [header, ...data] = await transform(['filter', 'csv'], report, {
* transformers: {
* filter: {include: 'header'},
* csv: {flatten: true}
* }
* });
* ```
* @param {string | string[]} transformerIds - Unique transformer identifier(s), in order. Can be one of `csv`, `filter`, `json`, `newline`, `redact`, `stack-hash`, `table`.
* @param {object} source - Source, typically one or more {@link @report-toolkit/common.report.Report|Reports}.
* @param {Partial<import('./observable').Config>} config - As returned by {@link loadConfig}; can contain transformer-specific settings.
* @param {Partial<import('./observable').TransformOptions>} options - Optional constraints & default behavior; overrides settings in `config`, if present.
*/
async function transform$1(transformerIds, source, config = {}, options = {}) {
return fromTransformerChain(transformerIds, config).pipe(transform(fromAny$1(source), options), toArray$1()).toPromise();
}
Object.defineProperty(exports, 'constants', {
enumerable: true,
get: function () {
return common.constants;
}
});
Object.defineProperty(exports, 'builtinTransformerIds', {
enumerable: true,
get: function () {
return transformers.builtinTransformerIds;
}
});
Object.defineProperty(exports, 'compatibleTransformers', {
enumerable: true,
get: function () {
return transformers.compatibleTransformers;
}
});
exports.deregisterPlugins = deregisterPlugins;
exports.diff = diff$1;
exports.inspect = inspect$1;
exports.isPluginRegistered = isPluginRegistered;
exports.loadConfig = loadConfig$1;
exports.observable = observable;
exports.registeredRuleDefinitions = registeredRuleDefinitions;
exports.toReportFromObject = toReportFromObject$1;
exports.transform = transform$1;
exports.use = use$1;
//# sourceMappingURL=report-toolkit-core.cjs.js.map