jsii-diff
Version:
Assembly comparison for jsii
237 lines • 8.44 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("@jsii/check-node/run");
const spec = require("@jsii/spec");
const fs = require("fs-extra");
const reflect = require("jsii-reflect");
const log4js = require("log4js");
const yargs = require("yargs");
const lib_1 = require("../lib");
const diagnostics_1 = require("../lib/diagnostics");
const util_1 = require("../lib/util");
const version_1 = require("../lib/version");
const LOG = log4js.getLogger('jsii-diff');
async function main() {
const argv = await yargs
.env('JSII_DIFF')
.option('verbose', {
alias: 'v',
type: 'count',
desc: 'Increase the verbosity of output',
global: true,
})
.option('default-stability', {
alias: 's',
type: 'string',
choices: ['experimental', 'stable'],
desc: 'Treat unmarked APIs as',
default: 'stable',
})
.option('experimental-errors', {
alias: 'e',
type: 'boolean',
default: false,
desc: 'Error on experimental API changes',
deprecate: 'Use `--error-on` instead',
})
.option('error-on', {
type: 'string',
default: 'prod',
choices: diagnostics_1.ERROR_CLASSES,
desc: 'Which type of API changes should be treated as an error',
})
.option('ignore-file', {
alias: 'i',
type: 'string',
desc: 'Ignore API changes with keys from file (file may be missing)',
})
.option('keys', {
alias: 'k',
type: 'boolean',
default: false,
desc: 'Show diagnostic suppression keys',
})
.option('validate', {
alias: 'd',
type: 'boolean',
default: false,
desc: 'Validate the assemblies that are being loaded',
})
.usage('$0 <original> [updated]', 'Compare two JSII assemblies.', (args) => args
.positional('original', {
description: 'Original assembly (file, package or "npm:package@version")',
type: 'string',
})
.positional('updated', {
description: 'New assembly (file, package or "npm:package@version")',
type: 'string',
default: '.',
}))
.help()
.version(version_1.VERSION).argv;
configureLog4js(argv.verbose);
LOG.debug(`Loading original assembly from ${argv.original}`);
const loadOriginal = await loadAssembly(argv.original, argv);
if (!loadOriginal.success) {
process.stderr.write(`Could not load '${loadOriginal.resolved}': ${(0, util_1.showDownloadFailure)(loadOriginal.reason)}. Skipping analysis\n`);
return 0;
}
LOG.debug(`Loading updated assembly from ${argv.updated}`);
const loadUpdated = await loadAssembly(argv.updated, argv);
if (!loadUpdated.success) {
process.stderr.write(`Could not load '${loadUpdated.resolved}': ${(0, util_1.showDownloadFailure)(loadUpdated.reason)}. Skipping analysis\n`);
return 0;
}
const original = loadOriginal.assembly;
const updated = loadUpdated.assembly;
if (original.name !== updated.name) {
process.stderr.write(`Look like different assemblies: '${original.name}' vs '${updated.name}'. Comparing is probably pointless...\n`);
}
LOG.info('Starting analysis');
const mismatches = (0, lib_1.compareAssemblies)(original, updated, {
defaultExperimental: argv['default-stability'] === 'experimental',
});
LOG.info(`Found ${mismatches.count} issues`);
if (mismatches.count > 0) {
const diags = (0, diagnostics_1.classifyDiagnostics)(mismatches, (0, diagnostics_1.treatAsError)(argv['error-on'], argv['experimental-errors']), await loadFilter(argv['ignore-file']));
process.stderr.write(`Original assembly: ${original.name}@${original.version}\n`);
process.stderr.write(`Updated assembly: ${updated.name}@${updated.version}\n`);
process.stderr.write('API elements with incompatible changes:\n');
for (const diag of diags) {
process.stderr.write(`${(0, diagnostics_1.formatDiagnostic)(diag, argv.keys)}\n`);
}
return (0, diagnostics_1.hasErrors)(diags) ? 1 : 0;
}
return 0;
}
// Allow both npm:<package> (legacy) and npm://<package> (looks better)
const NPM_REGEX = /^npm:(\/\/)?/;
/**
* Load the indicated assembly from the given name
*
* Supports downloading from NPM as well as from file or directory.
*/
async function loadAssembly(requested, options) {
let resolved = requested;
try {
if (NPM_REGEX.exec(requested)) {
let pkg = requested.replace(NPM_REGEX, '');
if (!pkg) {
pkg = await loadPackageNameFromAssembly(options);
}
resolved = `npm://${pkg}`;
if (!pkg.includes('@', 1)) {
resolved += '@latest';
}
const download = await (0, util_1.downloadNpmPackage)(pkg, (f) => loadFromFilesystem(f, options));
if (download.success) {
return {
requested,
resolved,
success: true,
assembly: download.result,
};
}
return { requested, resolved, success: false, reason: download.reason };
}
// We don't accept failure loading from the filesystem
return {
requested,
resolved,
success: true,
assembly: await loadFromFilesystem(requested, options),
};
}
catch (e) {
// Prepend information about which assembly we've failed to load
//
// Look at the type of error. If it has a lot of lines (like validation errors
// tend to do) log everything to the debug log and only show a couple
const maxLines = 3;
const messageWithContext = `Error loading assembly '${resolved}': ${e.message}`;
const errorLines = messageWithContext.split('\n');
if (errorLines.length < maxLines) {
throw new Error(messageWithContext);
}
for (const line of errorLines) {
LOG.info(line);
}
throw new Error([...errorLines.slice(0, maxLines), '...'].join('\n'));
}
}
async function loadPackageNameFromAssembly(options) {
const JSII_ASSEMBLY_FILE = spec.SPEC_FILE_NAME;
if (!(await fs.pathExists(JSII_ASSEMBLY_FILE))) {
throw new Error(`No NPM package name given and no ${JSII_ASSEMBLY_FILE} file in the current directory. Please specify a package name.`);
}
const contents = await fs.readJSON(JSII_ASSEMBLY_FILE, { encoding: 'utf-8' });
const module = options.validate
? spec.validateAssembly(contents)
: contents;
if (!module.name) {
throw new Error(`Could not find package in ${JSII_ASSEMBLY_FILE}`);
}
return module.name;
}
async function loadFromFilesystem(name, options) {
const stat = await fs.stat(name);
const ts = new reflect.TypeSystem();
if (stat.isDirectory()) {
return ts.loadModule(name, options);
}
return ts.loadFile(name, options);
}
main()
.then((n) => {
process.exit(n);
})
.catch((e) => {
console.error(e);
process.exit(100);
});
function configureLog4js(verbosity) {
log4js.configure({
appenders: {
console: {
type: 'stderr',
layout: { type: 'colored' },
},
},
categories: {
default: { appenders: ['console'], level: _logLevel() },
},
});
function _logLevel() {
switch (verbosity) {
case 0:
return 'WARN';
case 1:
return 'INFO';
case 2:
return 'DEBUG';
case 3:
return 'TRACE';
default:
return 'ALL';
}
}
}
async function loadFilter(filterFilename) {
if (!filterFilename) {
return new Set();
}
try {
return new Set((await fs.readFile(filterFilename, { encoding: 'utf-8' }))
.split('\n')
.map((x) => x.trim())
.filter((x) => !x.startsWith('#')));
}
catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
LOG.debug(`No such file: ${filterFilename}`);
return new Set();
}
}
//# sourceMappingURL=jsii-diff.js.map