Skip to content

Commit

Permalink
Merge pull request #5 from janbiasi/feat/dependency-cycles
Browse files Browse the repository at this point in the history
feat: 🎸 detect cyclical dependencies, via CLI and before pack
  • Loading branch information
janbiasi authored Jul 15, 2019
2 parents 5664241 + 0ff3b37 commit 25a0c7e
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 21 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion src/Packer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,14 @@ export class Packer {
const sourceExists = await fs.pathExists(this.options.source);
const sourcePkgExists = await fs.pathExists(resolve(this.options.source, 'package.json'));

return sourceExists && sourcePkgExists;
const sourcesExists = sourceExists && sourcePkgExists;
const adapterValidationResult = await this.adapter.validate();

if (sourcesExists && adapterValidationResult.valid) {
return true;
}

throw adapterValidationResult.message || 'Invalid packer configuration';
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/adapter/Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { IAnalytics, IPackerOptions, IAdapter } from '../types';
export class Adapter implements IAdapter {
constructor(protected cwd: string, protected options: IPackerOptions) {}

public async validate() {
return { valid: true };
}

public async analyze(): Promise<IAnalytics> {
throw new Error('Not implemented');
}
Expand Down
71 changes: 67 additions & 4 deletions src/adapter/Lerna.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ interface ILernaPackageInfo {
names: string[];
}

interface ILernacyclicalGraph {
[name: string]: string[];
}

interface ILernaResolvedTree {
internal: LernaPackageList;
external: DependenciesLike;
Expand Down Expand Up @@ -58,11 +62,46 @@ export class AdapterLerna extends Adapter {
try {
return await this.fetchPackage(sourcePkgPath);
} catch (err) {
throw new Error(err.message);
throw err;
}
}

// aggregation of internal dependencies
/**
* Detect possible cyclical dependencies which will lead to an error in
* the pack and/or analyze step.
*/
private async findcyclicalDependencies(): Promise<void> {
const { names, packages } = await this.getLernaPackagesInfo();
const lernaPkgDefs = await Promise.all(
packages.map(async pkg => {
return await this.fetchPackage(resolve(pkg.location, 'package.json'));
})
);
const cyclicalGraph = lernaPkgDefs.reduce(
(prev, curr) => ({
...prev,
[curr.name]: Object.keys(extractDependencies(curr.dependencies, dep => names.indexOf(dep) > -1))
}),
{} as ILernacyclicalGraph
);

Object.keys(cyclicalGraph).forEach(packageEntry => {
const internalDeps = cyclicalGraph[packageEntry];
internalDeps.forEach(internalLinkedDependency => {
if (cyclicalGraph[internalLinkedDependency]) {
if (cyclicalGraph[internalLinkedDependency].indexOf(packageEntry) > -1) {
throw new Error(
`${packageEntry} relies on ${internalLinkedDependency} and vice versa, please fix this cyclical dependency`
);
}
}
});
});
}

/**
* Recursive aggregation of internal dependencies
*/
private async resolveDependantInternals(
graph: IAnalytics['graph'] = {},
sourcePackage: Package,
Expand Down Expand Up @@ -113,7 +152,7 @@ export class AdapterLerna extends Adapter {

// build related graph from recursive modules
graph[sourcePackage.name] = {
internal: recursiveInternals.reduce(
internal: resolved.internal.reduce(
(prev, { name, version }) => ({ ...prev, [name]: version }),
{} as DependenciesLike
),
Expand All @@ -137,7 +176,31 @@ export class AdapterLerna extends Adapter {
}
}

/**
* Pre-validation process
*/
public async validate() {
try {
await this.findcyclicalDependencies();
return { valid: true };
} catch (err) {
return {
valid: false,
message: `${err}`
};
}
}

/**
* Main analytics process
*/
public async analyze(): Promise<IAnalytics> {
const { valid, message } = await this.validate();
if (!valid) {
throw new Error(message || `Invalid configuration found, check the docs for correct usage`);
}

// main analytics cycle
try {
const peer: DependenciesLike = {};
const rootGraph: IAnalytics['graph'] = {};
Expand Down Expand Up @@ -185,7 +248,7 @@ export class AdapterLerna extends Adapter {

return result;
} catch (err) {
throw new Error(`Failed to aggregate analytics: ${err.message}`);
throw err;
}
}
}
1 change: 0 additions & 1 deletion src/bin/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Packer } from '../../Packer';
import ora from 'ora';
import { displayPath } from '../../utils';

Packer;
export async function analyze(cwd: string, source: string) {
const spinner = ora('Creating packer instance');
spinner.frame();
Expand Down
1 change: 1 addition & 0 deletions src/bin/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './analyze';
export * from './pack';
export * from './validate';
29 changes: 29 additions & 0 deletions src/bin/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Packer } from '../../Packer';
import ora from 'ora';
import { displayPath } from '../../utils';

export async function validate(cwd: string, source: string) {
const spinner = ora('Creating packer instance');
spinner.frame();
spinner.start();

const packer = new Packer({
cwd,
source,
target: 'packed',
hooks: {
init: [
async () => {
spinner.succeed(`Initialized packer v${Packer.version} for ${displayPath(process.cwd(), source)}`);
}
]
}
});

try {
await packer.validate();
spinner.succeed(`Your project is ready to get packed!`);
} catch (err) {
spinner.fail(`${err || 'Project contains issues, please check the docs'}`);
}
}
18 changes: 17 additions & 1 deletion src/bin/monopacker-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { resolve } from 'path';
import * as commander from 'commander';
import { analyze, pack } from './commands';
import { analyze, pack, validate } from './commands';
import { AdapterLerna } from '../adapter/Lerna';

const program = new commander.Command();
Expand All @@ -15,6 +15,22 @@ program
.option('-v, --verbose', 'Silent mode', false)
.allowUnknownOption(false);

program
.command('validate <source>')
.alias('v')
.description('Checks if a package is packable by resolving all packages theoretically')
.option('-r, --root <dir>', 'Set a custom root directory, default: process.cwd()', process.cwd())
.action(async (source, { root, verbose = false }) => {
await validate(resolve(root), source);
})
.on('--help', () => {
console.log('');
console.log('Examples:');
console.log('');
console.log(' $ monpacker validate ./packages/main');
console.log(' $ monopacker v packages/main');
});

program
.command('analyze <source>')
.alias('a')
Expand Down
4 changes: 4 additions & 0 deletions src/types/Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { IAnalytics, IPackerOptions } from './Runtime';

export interface IAdapter {
analyze(): Promise<IAnalytics>;
validate(): Promise<{
valid: boolean;
message?: string;
}>;
}

export interface IAdapterConstructable {
Expand Down
6 changes: 4 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ export function getLernaPackages(root: string) {

if (!isDuplicate.length && pkg && pkg !== null) {
if (!pkg.name || !pkg.version) {
console.log(`Invalid package ${pkg.name || '<unknown>'} in ${ref}: name or version missing`);
throw new Error(
`Invalid package ${pkg.name || '<unknown>'} in ${ref}: name or version missing`
);
}
acc.push({
name: pkg.name,
Expand All @@ -132,7 +134,7 @@ export function getLernaPackages(root: string) {
}
} catch (err) {
/* istanbul ignore next */
console.log(err);
throw err;
}
return acc;
},
Expand Down
Loading

0 comments on commit 25a0c7e

Please sign in to comment.