-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
See #58. Add Dependency Manager, related classes, and tests
- Loading branch information
Showing
10 changed files
with
456 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"editor.insertSpaces": true, | ||
"editor.tabSize": 4 | ||
"editor.tabSize": 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}` | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./Dependency"; | ||
export * from "./DependencyManager"; | ||
export * from "./DependencyFactory"; |
Oops, something went wrong.