Skip to content

Commit

Permalink
fix(jsii-diff): don't fail on new packages (#502)
Browse files Browse the repository at this point in the history
When a new package is created, jsii-diff will try to download the
previous package from NPM and then fail.

Remember that the package doesn't exist upstream and fail silently
in that case.
  • Loading branch information
rix0rrr authored May 8, 2019
1 parent 7ba1aab commit d1d0633
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 18 deletions.
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

0 comments on commit d1d0633

Please sign in to comment.