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

fix(jsii-diff): don't fail on new packages #502

Merged
merged 1 commit into from
May 8, 2019
Merged
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
50 changes: 37 additions & 13 deletions packages/jsii-diff/bin/jsii-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import spec = require('jsii-spec');
import log4js = require('log4js');
import yargs = require('yargs');
import { compareAssemblies } from '../lib';
import { downloadNpmPackage } from '../lib/util';
import { DownloadFailure, downloadNpmPackage, showDownloadFailure } from '../lib/util';
import { VERSION } from '../lib/version';

const LOG = log4js.getLogger('jsii-diff');
Expand Down Expand Up @@ -33,13 +33,24 @@ async function main(): Promise<number> {
configureLog4js(argv.verbose);

LOG.debug(`Loading original assembly from ${(argv as any).original}`);
const original = await loadAssembly((argv as any).original);
const loadOriginal = await loadAssembly((argv as any).original);
if (!loadOriginal.success) {
process.stderr.write(`Could not load '${loadOriginal.resolved}': ${showDownloadFailure(loadOriginal.reason)}. Skipping analysis\n`);
return 0;
}

LOG.debug(`Loading updated assembly from ${(argv as any).updated}`);
const updated = await loadAssembly((argv as any).updated);
const loadUpdated = await loadAssembly((argv as any).updated);
if (!loadUpdated.success) {
process.stderr.write(`Could not load '${loadUpdated.resolved}': ${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...`);
process.stderr.write(`Look like different assemblies: '${original.name}' vs '${updated.name}'. Comparing is probably pointless...\n`);
}

LOG.info(`Starting analysis`);
Expand All @@ -66,27 +77,37 @@ async function main(): Promise<number> {
// Allow both npm:<package> (legacy) and npm://<package> (looks better)
const NPM_REGEX = /^npm:(\/\/)?/;

async function loadAssembly(name: string) {
/**
* Load the indicated assembly from the given name
*
* Supports downloading from NPM as well as from file or directory.
*/
async function loadAssembly(requested: string): Promise<LoadAssemblyResult> {
let resolved = requested;
try {
if (name.match(NPM_REGEX)) {
let pkg = name.replace(NPM_REGEX, '');
if (requested.match(NPM_REGEX)) {
let pkg = requested.replace(NPM_REGEX, '');
if (!pkg) { pkg = await loadPackageNameFromAssembly(); }

// Put 'pkg' back into 'name' so any errors loading the assembly get a good source description
name = `npm://${pkg}`;
if (pkg.indexOf('@', 1) === -1) { name += '@latest'; }
resolved = `npm://${pkg}`;
if (pkg.indexOf('@', 1) === -1) { resolved += '@latest'; }

return await downloadNpmPackage(pkg, loadFromFilesystem);
const download = await downloadNpmPackage(pkg, loadFromFilesystem);
if (download.success) {
return { requested, resolved, success: true, assembly: download.result };
}
return { requested, resolved, success: false, reason: download.reason };
} else {
return await loadFromFilesystem(name);
// We don't accept failure loading from the filesystem
return { requested, resolved, success: true, assembly: await loadFromFilesystem(requested) };
}
} 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 '${name}': ${e.message}`;
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) {
Expand All @@ -96,6 +117,9 @@ async function loadAssembly(name: string) {
}
}

type LoadAssemblyResult = { requested: string; resolved: string }
& ({ success: true; assembly: reflect.Assembly } | { success: false; reason: DownloadFailure });

async function loadPackageNameFromAssembly(): Promise<string> {
const JSII_ASSEMBLY_FILE = '.jsii';
if (!await fs.pathExists(JSII_ASSEMBLY_FILE)) {
Expand Down
48 changes: 43 additions & 5 deletions packages/jsii-diff/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,57 @@ export async function inTempDir<T>(block: () => T | Promise<T>): Promise<T> {
}
}

export async function downloadNpmPackage<T>(pkg: string, block: (dir: string) => Promise<T>): Promise<T> {
export type DownloadFailure = 'no_such_package';

export type NpmDownloadResult<T> = { success: true; result: T } | { success: false; reason: DownloadFailure };

export function showDownloadFailure(f: DownloadFailure) {
switch (f) {
case 'no_such_package': return 'NPM package does not exist';
}
}

export async function downloadNpmPackage<T>(pkg: string, block: (dir: string) => Promise<T>): Promise<NpmDownloadResult<T>> {
return await inTempDir(async () => {
LOG.info(`Fetching NPM package ${pkg}`);

// Need to install package and dependencies in order for jsii-reflect
// to not bork when it can find the dependencies.
await exec(`npm install --silent --prefix . ${pkg}`);
try {
// Need to install package and dependencies in order for jsii-reflect
// to not bork when it can find the dependencies.
await exec(`npm install --silent --prefix . ${pkg}`);
} catch (e) {
// If this fails, might be because the package doesn't exist
if (!isSubprocesFailedError(e)) { throw e; }
if (await npmPackageExists(pkg)) {
throw new Error(`NPM fetch failed: ${e}. Please try again.`);
}
LOG.warn(`NPM package ${pkg} does not exist.`);
return { success: false, reason: 'no_such_package' } as NpmDownloadResult<T>;
}

const pkgDir = trimVersionString(pkg);
return await block(path.join(process.cwd(), 'node_modules', pkgDir));
return {
success: true,
result: await block(path.join(process.cwd(), 'node_modules', pkgDir))
} as NpmDownloadResult<T>;
});
}

function isSubprocesFailedError(e: any) {
return e.code !== undefined && e.cmd !== undefined;
}

async function npmPackageExists(pkg: string): Promise<boolean> {
try {
LOG.info(`Checking existence of ${pkg}`);
await exec(`npm show --silent ${pkg}`);
return true;
} catch (e) {
if (!isSubprocesFailedError(e)) { throw e; }
return false;
}
}

/**
* Trim an optional version string from an NPM package name
*/
Expand Down