Skip to content

Commit

Permalink
feat(northbrook): build a dependency graph for more correct tooling
Browse files Browse the repository at this point in the history
Northbrook will now traverse all of your packages dependencies, building a
graph of inter-dependencies between *only* packages being managed by Northbrook.
This is done to sort your packages in a more correct order that will allow tools
that do building or bundling of some kind (maybe others!) that deal with these
inter-dependencies directly or indirectly. In the case of circular dependencies
being detected an error will be thrown informing you to add configuration to
eliminate the errors you are having. The package identified in the error message
will from then on be excluded from `northbrook link` and continue to use NPM installed
packages (or however you chose to setup your projects) to avoid impossible Cycles 🔥

AFFECTS: northbrook
  • Loading branch information
TylorS committed Jan 16, 2017
1 parent 5609b7e commit c2ed7e3
Show file tree
Hide file tree
Showing 26 changed files with 240 additions and 63 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@types/semver": "^5.3.30",
"app-module-path": "^2.1.0",
"buba": "^4.0.2",
"dependency-graph": "^0.5.0",
"findup-sync": "^0.4.3",
"mkdirp": "^0.5.1",
"ramda": "^0.22.1",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NorthbrookConfig, Stdio } from './types';
import { DepGraph } from './northbrook';

import {
CommandFlags,
Expand All @@ -13,6 +14,7 @@ declare module 'reginn' {
args: Array<string>;
options: any;
config: NorthbrookConfig;
depGraph: DepGraph;
directory: string;
}

Expand Down
Empty file.
5 changes: 5 additions & 0 deletions src/northbrook/northbrook/__test__/a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "a-testpackage",
"version": "0.0.0",
"dependencies": {}
}
Empty file.
7 changes: 7 additions & 0 deletions src/northbrook/northbrook/__test__/b/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "b-testpackage",
"version": "0.0.0",
"dependencies": {
"c-testpackage": "latest"
}
}
Empty file.
7 changes: 7 additions & 0 deletions src/northbrook/northbrook/__test__/c/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "c-testpackage",
"version": "0.0.0",
"dependencies": {
"a-testpackage": "latest"
}
}
Empty file.
7 changes: 7 additions & 0 deletions src/northbrook/northbrook/__test__/d/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "d-testpackage",
"version": "0.0.0",
"dependencies": {
"c-testpackage": "latest"
}
}
Empty file.
7 changes: 7 additions & 0 deletions src/northbrook/northbrook/__test__/e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "e-testpackage",
"version": "0.0.0",
"dependencies": {
"f-testpackage": "latest"
}
}
Empty file.
7 changes: 7 additions & 0 deletions src/northbrook/northbrook/__test__/f/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "f-testpackage",
"version": "0.0.0",
"dependencies": {
"e-testpackage": "latest"
}
}
42 changes: 42 additions & 0 deletions src/northbrook/northbrook/buildDependencyGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as assert from 'assert';
import { join } from 'path';
import { buildDependencyGraph } from './buildDependencyGraph';

const pkg = (name: string) => join(__dirname, '__test__/' + name);

const packageA = pkg('a');
const packageB = pkg('b');
const packageC = pkg('c');
const packageD = pkg('d');
const packageE = pkg('e');
const packageF = pkg('f');

describe('buildDependencyGraph', () => {
describe('given packages as input', () => {
it('builds a dependency graph', () => {
const packages = [ packageA, packageB, packageC, packageD ];

const graph = buildDependencyGraph(packages);

assert.deepEqual(graph.paths(), [packageA, packageC, packageB, packageD]);
});

it('throws an error when there is a circular dependency', () => {
const packages = [ packageE, packageF ];
const graph = buildDependencyGraph(packages);

assert.throws(() => {
graph.paths();
});
});

it('does not throw an error when given circular configuration', () => {
const packages = [ packageE, packageF ];
const graph = buildDependencyGraph(packages, [ 'e-testpackage' ]);

const paths = graph.paths();

assert.deepEqual(paths, [packageF, packageE]);
});
});
});
108 changes: 108 additions & 0 deletions src/northbrook/northbrook/buildDependencyGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { join } from 'path';
import { EOL } from 'os';
import { union, clone } from 'ramda';
import { Pkg } from '../../types';

const DependencyGraph = require('dependency-graph').DepGraph;

export function buildDependencyGraph(packagePaths: Array<string>, circular: Array<string> = []) {
const configs: { [packageName: string]: Pkg } = {};
const graph: { [pacakgename: string]: Array<Pkg> } = {};

packagePaths.forEach(packageFinder(configs));

const depGraph = new DependencyGraph();
const packages = Object.keys(configs).map(key => configs[key]);
const packageNames: Array<string> = packages.map(pkg => pkg.name);

packages.forEach(({ name, config }) => {
const devDependencies: Array<string> =
findDependencies(config.devDependencies, packageNames);

const peerDependencies: Array<string> =
findDependencies(config.peerDependencies, packageNames);

const dependencies: Array <string> =
findDependencies(config.dependencies, packageNames);

const allDependencies: Array<string> =
union(union(devDependencies, peerDependencies), dependencies);

depGraph.addNode(name, configs[name]);

graph[name] = allDependencies.map(depName => configs[depName]);
});

packageNames.forEach(name => {
if (circular.indexOf(name) === -1) {
const deps = graph[name];

deps.forEach(dep => {
if (circular.indexOf(dep.name) === -1)
depGraph.addDependency(name, dep.name);
});
}
});

return new DepGraph(depGraph, configs, circular);
}

export class DepGraph {
constructor (
private depGraph: any,
private configs: { [key: string]: Pkg },
private circular: Array<string>) {}

public dependenciesOf (packageName: string) {
return this.depGraph.dependenciesOf(packageName);
}

public configOf(packageName: string): Pkg {
return clone(this.depGraph.getNodeData(packageName));
}

public paths(): Array<string> {
return this.packages().map(pkg => pkg.path);
}

public packages(): Array<Pkg> {
const configs = this.configs;

return this.packageNames().map(name => configs[name]);
}

public packageNames(): Array<string> {
const { depGraph, circular } = this;

try {
return depGraph.overallOrder()
.filter((name: string) => circular.indexOf(name) === -1)
.concat(circular);
} catch (e) {
const circularPackage = e.message.split(':')[1].split('->')[0].trim();

throw new Error(EOL + EOL + `${e.message}` + EOL + EOL +
`Circular dependencies are an advanced use-case and must be handled explicitly.` + EOL +
`To handle circular dependencies it is required to create a ` + EOL +
`Northbrook configuration file` + `containing at least:` + EOL + EOL +
` module.exports = {` + EOL +
` circular: [ '${circularPackage}' ]` + EOL +
` }` + EOL);
}
}
}

function packageFinder(configs: any) {
return function (path: string) {
const pkgJson = clone(require(join(path, 'package.json')));

configs[pkgJson.name] = { path, name: pkgJson.name, config: pkgJson };
};
}

function findDependencies(dependencies: any, packageNames: Array<String>): Array<string> {
if (!dependencies) return [];

return Object.keys(dependencies)
.filter(name => packageNames.indexOf(name) > -1);
};
1 change: 1 addition & 0 deletions src/northbrook/northbrook/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './northbrook';
export * from './buildDependencyGraph';
7 changes: 5 additions & 2 deletions src/northbrook/northbrook/northbrook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cyan } from 'typed-colors';
import { resolvePlugins } from './resolvePlugins';
import { resolvePackages } from './resolvePackages';
import { northrookRun } from './run';
import { buildDependencyGraph } from './buildDependencyGraph';
import { isFile, defaultStdio } from '../../helpers';
import { NorthbrookConfig, STDIO, Stdio, Plugin } from '../../types';

Expand Down Expand Up @@ -59,9 +60,11 @@ export function northbrook(
if (packages.length === 0)
packages = isFile(join(cwd, 'package.json')) ? [cwd] : [];

config.packages = packages;
const dependencyGraph = buildDependencyGraph(packages, config.circular);

const run = northrookRun(clone(config), cwd, stdio as Stdio);
config.packages = dependencyGraph.paths();

const run = northrookRun(clone(config), dependencyGraph, cwd, stdio as Stdio);

return {
plugins: plugins.slice(0),
Expand Down
9 changes: 8 additions & 1 deletion src/northbrook/northbrook/run/callCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getCommandFlags } from 'reginn/lib/commonjs/run/getCommandFlags';
import { tail } from 'ramda';
import { deepMerge } from '../../../helpers';
import { NorthbrookConfig, Stdio } from '../../../types';
import { DepGraph } from '../buildDependencyGraph';

export function callCommand(
argv: string[],
Expand All @@ -12,6 +13,7 @@ export function callCommand(
config: NorthbrookConfig,
directory: string,
stdio: Stdio,
depGraph: DepGraph,
) {
return function (command: Command) {
if (!command.handler) return;
Expand All @@ -27,20 +29,23 @@ export function callCommand(
filter,
config,
directory,
depGraph,
),
stdio,
);
} else if (command.aliases && command.aliases.length > 0) {
command.handler({
config,
directory,
depGraph,
args: tail(parsedArgs),
options: optionsToCamelCase(filter(command)),
}, stdio);
} else {
command.handler({
config,
directory,
depGraph,
args: parsedArgs,
options: optionsToCamelCase(filter(command)),
}, stdio);
Expand Down Expand Up @@ -68,7 +73,8 @@ function createSubApplication(
commandFlags: CommandFlags, command: Command,
filter: (command: Command) => CommandFlags,
config: NorthbrookConfig,
directory: string): HandlerApp
directory: string,
depGraph: DepGraph): HandlerApp
{
const flags = argv.filter(arg => parsedArgs.indexOf(arg) === -1);
const args = tail(parsedArgs).concat(flags);
Expand All @@ -81,5 +87,6 @@ function createSubApplication(
flags: commandFlags,
config,
directory,
depGraph,
};
}
14 changes: 11 additions & 3 deletions src/northbrook/northbrook/run/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ import { forEach, ifElse } from 'ramda';
import { parseArguments, splitArguments } from 'reginn/lib/commonjs/run/parseArguments';

import { EOL } from 'os';
import { DepGraph } from '../buildDependencyGraph';
import { callCommand } from './callCommand';
import { cross } from 'typed-figures';
import { deepMerge } from '../../../helpers';
import { filterOptions } from 'reginn/lib/commonjs/run/filterOptions';
import { getCommandFlags } from 'reginn/lib/commonjs/run/getCommandFlags';
import { matchCommands } from 'reginn/lib/commonjs/run/matchCommands';

export function northrookRun(config: NorthbrookConfig, directory: string, stdio: Stdio) {
export function northrookRun(
config: NorthbrookConfig,
depGraph: DepGraph,
directory: string,
stdio: Stdio)
{
return function run(
argv: Array<string>, app: App): Promise<HandlerApp>
{
Expand All @@ -37,14 +43,15 @@ export function northrookRun(config: NorthbrookConfig, directory: string, stdio:
(parsedArguments._[0] ? red(bold(`${parsedArguments._[0]}`)) : ''));
}

return execute(argv, app, config, directory, stdio, matchedCommands, parsedArguments);
return execute(argv, app, config, depGraph, directory, stdio, matchedCommands, parsedArguments);
};
}

function execute(
argv: string[],
app: App,
config: NorthbrookConfig,
depGraph: DepGraph,
directory: string,
stdio: Stdio,
matchedCommands: Array<Command>,
Expand All @@ -58,7 +65,7 @@ function execute(

const filterCommandOptions = filterOptions(options, app.flags, argv);
const commandCall =
callCommand(argv, args, commandFlags, filterCommandOptions, config, directory, stdio);
callCommand(argv, args, commandFlags, filterCommandOptions, config, directory, stdio, depGraph);

if ((parsedArguments as any).help === true) {
stdout.write(green(bold(`Northbrook`)) + EOL + EOL +
Expand All @@ -74,6 +81,7 @@ function execute(

return Promise.resolve<HandlerApp>({
config,
depGraph,
directory,
type: 'app',
flags: commandFlags,
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/exec/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ each(plugin, function ({ pkg, args }, io) {

const cmd = args.shift() as string;

io.stdout.write(`Running '${cmd} ${args.join(' ')}' in ${pkg.name}...`);
io.stdout.write(`Running '${`${cmd} ${args.join(' ')}`.trim()}' in ${pkg.name}...`);

m.addPath(path);
m.addPath(join(path, 'node_modules'));

return execute(cmd, args, io, path)
.then(() => io.stdout.write(
`Completed running '${cmd} ${args.join(' ')}' in ${pkg.name}` + EOL))
`Completed running '${`${cmd} ${args.join(' ')}`.trim()}' in ${pkg.name}` + EOL))
.catch(logError(io.stdout, io.stderr));
})
.catch(() => {
Expand Down
Loading

0 comments on commit c2ed7e3

Please sign in to comment.