Skip to content

Commit

Permalink
See #58. Add Dependency Manager, related classes, and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Glavin001 committed Mar 22, 2018
1 parent bbf6337 commit 99f758e
Show file tree
Hide file tree
Showing 10 changed files with 456 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 4
"editor.tabSize": 2
}
83 changes: 83 additions & 0 deletions src/DependencyManager/Dependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { SemVer } from "semver";

export abstract class Dependency {
private _isInstalled: boolean = false;
private _version?: SemVer;

constructor(protected options: DependencyOptions) {}

public load(): Promise<boolean> {
if (this.isInstalled) {
return Promise.resolve(this.isInstalled);
}
return this.reload();
}

public reload(): Promise<boolean> {
return this.loadVersion()
.then(version => {
this._version = this.parseVersion(version);
return (this._isInstalled = true);
})
.catch(error => {
this._version = undefined;
return (this._isInstalled = false);
});
}

private parseVersion(version: string): SemVer {
const { parseVersion } = this.options;
return new SemVer(parseVersion ? parseVersion(version) : version, true);
}

protected abstract loadVersion(): Promise<string>;

public get isInstalled(): boolean {
return this._isInstalled;
}
public get name(): string {
return this.options.name;
}

public get version(): SemVer | undefined {
return this._version;
}

public get required(): boolean {
return !Boolean(this.options.optional);
}
}

export interface BaseDependencyOptions {
// tslint:disable-next-line:no-reserved-keywords
type: DependencyType;
name: string;
parseVersion?(text: string): string;
optional?: boolean;
}

export enum DependencyType {
Node = "node",
Executable = "exec"
}

export interface NodeDependencyOptions extends BaseDependencyOptions {
// tslint:disable-next-line:no-reserved-keywords
type: DependencyType.Node;
// tslint:disable-next-line:no-reserved-keywords
package: string;
}

export interface ExecutableDependencyOptions extends BaseDependencyOptions {
// tslint:disable-next-line:no-reserved-keywords
type: DependencyType.Executable;
program: string;
versionArgs?: string[];
docker?: {
image: string;
};
}

export type DependencyOptions =
| NodeDependencyOptions
| ExecutableDependencyOptions;
27 changes: 27 additions & 0 deletions src/DependencyManager/DependencyFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
Dependency,
DependencyType,
NodeDependencyOptions,
ExecutableDependencyOptions,
DependencyOptions
} from "./Dependency";
import { NodeDependency } from "./NodeDependency";
import { ExecutableDependency } from "./ExecutableDependency";

export class DependencyFactory {
constructor(private options: DependencyOptions) {}

public dependency(): Dependency {
const { options } = this;
switch (options.type) {
case DependencyType.Node:
return new NodeDependency(options);
case DependencyType.Executable:
return new ExecutableDependency(options);
default:
throw new Error(
`Dependency type not found for: ${JSON.stringify(options)}`
);
}
}
}
48 changes: 48 additions & 0 deletions src/DependencyManager/DependencyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// tslint:disable:no-reserved-keywords
import { SemVer } from "semver";
import { DependencyFactory } from "./DependencyFactory";
import { Dependency, DependencyOptions } from "./Dependency";

export class DependencyManager {
private readonly dependencies: Dependency[];
private readonly lookup: { [name: string]: Dependency };

constructor(dependencies: DependencyOptions[]) {
this.dependencies = dependencies.map(dependency =>
new DependencyFactory(dependency).dependency()
);
this.lookup = this.dependencies.reduce(
(lookup, dep) => ({
...lookup,
[dep.name]: dep
}),
{}
);
}

public has(name: string): boolean {
return Boolean(this.get(name));
}

public get<T extends Dependency>(name: string): T | undefined {
return this.lookup[name] as T;
}

public load(): Promise<any> {
return Promise.all(
this.dependencies.map(dep => {
return dep.load().then(isInstalled => {
if (dep.required && !isInstalled) {
throw new Error(
`Dependency "${dep.name}" is required and not installed.`
);
}
});
})
);
}
}

export interface DependencyMap {
[name: string]: Dependency;
}
41 changes: 41 additions & 0 deletions src/DependencyManager/ExecutableDependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Dependency,
BaseDependencyOptions,
DependencyType,
ExecutableDependencyOptions
} from "./Dependency";

export class ExecutableDependency extends Dependency {
constructor(protected options: ExecutableDependencyOptions) {
super(options);
}

protected loadVersion() {
return this.run(this.versionArgs).then(({ stdout }) => stdout);
}

private get versionArgs(): string[] {
return this.options.versionArgs || ["--version"];
}

public run(args: RunArg[], options: RunOptions = {}): Promise<RunResponse> {
// return Promise.reject(new Error("test"));
return Promise.resolve({
exitCode: 0,
stderr: "",
stdout: "test"
});
}
}

export type RunArg = string | Promise<string> | undefined | null;

export interface RunOptions {
cwd?: string;
}

export interface RunResponse {
stdout: string;
stderr: string;
exitCode: number;
}
60 changes: 60 additions & 0 deletions src/DependencyManager/NodeDependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Dependency,
BaseDependencyOptions,
DependencyType,
NodeDependencyOptions
} from "./Dependency";

// tslint:disable-next-line:no-require-imports no-var-requires
const requireg = require("requireg");

export class NodeDependency extends Dependency {
private cachedPackage: any;

constructor(protected options: NodeDependencyOptions) {
super(options);
}

protected loadVersion() {
try {
return Promise.resolve(this.require("package.json").version);
} catch (error) {
return Promise.reject(error);
}
}

// tslint:disable-next-line:no-reserved-keywords
public get package() {
return this.require();
}

// tslint:disable-next-line:no-reserved-keywords
private require(id?: string): any {
// tslint:disable-next-line:no-require-imports non-literal-require
return require(this.resolve(id));
}

private resolve(file?: string): string {
const path = this.fullPath(file);
return this.resolveLocal(path) || this.resolveGlobal(path);
}

private resolveLocal(path: string): string {
return require.resolve(path);
}

private resolveGlobal(path: string): string {
return requireg.resolve(path);
}

private fullPath(filePath?: string): string {
if (filePath) {
return `${this.packageName}/${filePath}`;
}
return this.packageName;
}

private get packageName(): string {
return this.options.package;
}
}
3 changes: 3 additions & 0 deletions src/DependencyManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./Dependency";
export * from "./DependencyManager";
export * from "./DependencyFactory";
Loading

0 comments on commit 99f758e

Please sign in to comment.