From 38009900e717c024d3be069b8f82d65737004830 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 8 Apr 2018 02:41:32 +1000 Subject: [PATCH] Update logic to allow for opt/req and prod/dev/root --- README.md | 2 +- src/Walker.ts | 36 ++++++++-------- src/depTypes.ts | 82 ++++++++++++++++--------------------- test/Walker_spec.ts | 27 +++++++----- test/depTypes_spec.ts | 95 ++++++++++++++++++------------------------- 5 files changed, 111 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 074dc60..b28768e 100644 --- a/README.md +++ b/README.md @@ -39,5 +39,5 @@ const walker = new Walker(modulePath); Returns `Promise` Will walk your entire node_modules tree reporting back an array of "modules", each -module has a "path", "name" and "depType". See the typescript definition file +module has a "path", "name" and "relationship". See the typescript definition file for more information. \ No newline at end of file diff --git a/src/Walker.ts b/src/Walker.ts index 89b7758..f5b69a1 100644 --- a/src/Walker.ts +++ b/src/Walker.ts @@ -2,7 +2,7 @@ import * as debug from 'debug'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { DepType, depTypeGreater, childDepType } from './depTypes'; +import { DepType, DepRequireState, depRelationshipGreater, childRequired, DepRelationship } from './depTypes'; export type VersionRange = string; export interface PackageJSON { @@ -13,7 +13,7 @@ export interface PackageJSON { } export interface Module { path: string; - depType: DepType; + relationship: DepRelationship; name: string; } @@ -48,7 +48,7 @@ export class Walker { return null; } - private async walkDependenciesForModuleInModule(moduleName: string, modulePath: string, depType: DepType) { + private async walkDependenciesForModuleInModule(moduleName: string, modulePath: string, relationship: DepRelationship) { let testPath = modulePath; let discoveredPath: string | null = null; let lastRelative: string | null = null; @@ -65,7 +65,7 @@ export class Walker { } } // If we can't find it the install is probably buggered - if (!discoveredPath && depType !== DepType.OPTIONAL && depType !== DepType.DEV_OPTIONAL) { + if (!discoveredPath && relationship.getRequired() !== DepRequireState.OPTIONAL) { throw new Error( `Failed to locate module "${moduleName}" from "${modulePath}" @@ -74,22 +74,22 @@ export class Walker { } // If we can find it let's do the same thing for that module if (discoveredPath) { - await this.walkDependenciesForModule(discoveredPath, depType); + await this.walkDependenciesForModule(discoveredPath, relationship); } } - private async walkDependenciesForModule(modulePath: string, depType: DepType) { - d('walk reached:', modulePath, ' Type is:', DepType[depType]); + private async walkDependenciesForModule(modulePath: string, relationship: DepRelationship) { + d('walk reached:', modulePath, ' Type is:', relationship.toString()); // We have already traversed this module if (this.walkHistory.has(modulePath)) { d('already walked this route'); // Find the existing module reference const existingModule = this.modules.find(module => module.path === modulePath) as Module; - // If the depType we are traversing with now is higher than the + // If the relationship we are traversing with now is higher than the // last traversal then update it (prod superseeds dev for instance) - if (depTypeGreater(depType, existingModule.depType)) { - d(`existing module has a type of "${existingModule.depType}", new module type would be "${depType}" therefore updating`); - existingModule.depType = depType; + if (depRelationshipGreater(relationship, existingModule.relationship)) { + d(`existing module has a type of "${existingModule.relationship.toString()}", new module type would be "${relationship.toString()}" therefore updating`); + existingModule.relationship = relationship; } return; } @@ -105,11 +105,13 @@ export class Walker { // Record this module as being traversed this.walkHistory.add(modulePath); this.modules.push({ - depType, + relationship, path: modulePath, name: pJ.name, }); + const childDepType = relationship.getType() === DepType.DEV ? DepType.DEV : DepType.PROD; + // For every prod dep for (const moduleName in pJ.dependencies) { // npm decides it's a funny thing to put optional dependencies in the "dependencies" section @@ -121,18 +123,18 @@ export class Walker { await this.walkDependenciesForModuleInModule( moduleName, modulePath, - childDepType(depType, DepType.PROD), + new DepRelationship(childDepType, childRequired(relationship.getRequired(), DepRequireState.REQUIRED)), ); } // For every dev dep, but only if we are in the root module - if (depType === DepType.ROOT) { + if (relationship.getType() === DepType.ROOT) { d('we\'re still at the beginning, walking down the dev route'); for (const moduleName in pJ.devDependencies) { await this.walkDependenciesForModuleInModule( moduleName, modulePath, - childDepType(depType, DepType.DEV), + new DepRelationship(DepType.DEV, childRequired(relationship.getRequired(), DepRequireState.REQUIRED)), ); } } @@ -142,7 +144,7 @@ export class Walker { await this.walkDependenciesForModuleInModule( moduleName, modulePath, - childDepType(depType, DepType.OPTIONAL), + new DepRelationship(childDepType, childRequired(relationship.getRequired(), DepRequireState.OPTIONAL)), ); } } @@ -154,7 +156,7 @@ export class Walker { this.cache = new Promise(async (resolve, reject) => { this.modules = []; try { - await this.walkDependenciesForModule(this.rootModule, DepType.ROOT); + await this.walkDependenciesForModule(this.rootModule, new DepRelationship(DepType.ROOT, DepRequireState.REQUIRED)); } catch (err) { reject(err); return; diff --git a/src/depTypes.ts b/src/depTypes.ts index 786d46b..9320d78 100644 --- a/src/depTypes.ts +++ b/src/depTypes.ts @@ -1,43 +1,41 @@ export enum DepType { PROD, DEV, + ROOT, +} + +export enum DepRequireState { OPTIONAL, - DEV_OPTIONAL, - ROOT + REQUIRED, +} + +export class DepRelationship { + constructor(private type: DepType, private required: DepRequireState) {} + + public getType() { return this.type; } + public getRequired() { return this.required; } + public toString() { + return `${DepType[this.getType()]}_${DepRequireState[this.getRequired()]}`; + } +} + +export const depRequireStateGreater = (newState: DepRequireState, existing: DepRequireState) => { + if (existing === DepRequireState.REQUIRED) { + return false; + } else if (newState === DepRequireState.REQUIRED) { + return true; + } + return false; } export const depTypeGreater = (newType: DepType, existing: DepType) => { switch (existing) { case DepType.DEV: switch (newType) { - case DepType.OPTIONAL: case DepType.PROD: case DepType.ROOT: return true; case DepType.DEV: - case DepType.DEV_OPTIONAL: - default: - return false; - } - case DepType.DEV_OPTIONAL: - switch (newType) { - case DepType.OPTIONAL: - case DepType.PROD: - case DepType.ROOT: - case DepType.DEV: - return true; - case DepType.DEV_OPTIONAL: - default: - return false; - } - case DepType.OPTIONAL: - switch (newType) { - case DepType.PROD: - case DepType.ROOT: - return true; - case DepType.OPTIONAL: - case DepType.DEV: - case DepType.DEV_OPTIONAL: default: return false; } @@ -46,9 +44,7 @@ export const depTypeGreater = (newType: DepType, existing: DepType) => { case DepType.ROOT: return true; case DepType.PROD: - case DepType.OPTIONAL: case DepType.DEV: - case DepType.DEV_OPTIONAL: default: return false; } @@ -56,9 +52,7 @@ export const depTypeGreater = (newType: DepType, existing: DepType) => { switch (newType) { case DepType.ROOT: case DepType.PROD: - case DepType.OPTIONAL: case DepType.DEV: - case DepType.DEV_OPTIONAL: default: return false; } @@ -67,22 +61,18 @@ export const depTypeGreater = (newType: DepType, existing: DepType) => { } } -export const childDepType = (parentType: DepType, childType: DepType) => { - if (childType === DepType.ROOT) { - throw new Error('Something went wrong, a child dependency can\'t be marked as the ROOT'); +export const depRelationshipGreater = (newRelationship: DepRelationship, existingRelationship: DepRelationship) => { + if (depRequireStateGreater(newRelationship.getRequired(), existingRelationship.getRequired())) { + return true; } - switch (parentType) { - case DepType.ROOT: - return childType; - case DepType.PROD: - if (childType === DepType.OPTIONAL) return DepType.OPTIONAL; - return DepType.PROD; - case DepType.OPTIONAL: - return DepType.OPTIONAL; - case DepType.DEV_OPTIONAL: - return DepType.DEV_OPTIONAL; - case DepType.DEV: - if (childType === DepType.OPTIONAL) return DepType.DEV_OPTIONAL; - return DepType.DEV; + return depTypeGreater(newRelationship.getType(), existingRelationship.getType()); +} + +export const childRequired = (parent: DepRequireState, child: DepRequireState) => { + switch (parent) { + case DepRequireState.OPTIONAL: + return DepRequireState.OPTIONAL; + default: + return child; } } \ No newline at end of file diff --git a/test/Walker_spec.ts b/test/Walker_spec.ts index fa582ab..62730fc 100644 --- a/test/Walker_spec.ts +++ b/test/Walker_spec.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { expect } from 'chai'; import { Module, Walker } from '../src/Walker'; -import { DepType } from '../src/depTypes'; +import { DepType, DepRequireState } from '../src/depTypes'; describe('Walker', () => { let walker: Walker; @@ -18,26 +18,31 @@ describe('Walker', () => { expect(walker.getRootModule()).to.equal(path.resolve(__dirname, '..')); }); - it('should locate top level prod deps as prod deps', () => { - expect(dep('fs-extra')).to.have.property('depType', DepType.PROD); + it('should locate top level prod deps as required prod deps', () => { + expect(dep('fs-extra').relationship.getType()).equal(DepType.PROD); + expect(dep('fs-extra').relationship.getRequired()).equal(DepRequireState.REQUIRED); }); - it('should locate top level dev deps as dev deps', () => { - expect(dep('mocha')).to.have.property('depType', DepType.DEV); + it('should locate top level dev deps as required dev deps', () => { + expect(dep('mocha').relationship.getType()).equal(DepType.DEV); + expect(dep('mocha').relationship.getRequired()).equal(DepRequireState.REQUIRED); }); - it('should locate a dep of a dev dep as a dev dep', () => { - expect(dep('commander')).to.have.property('depType', DepType.DEV); + it('should locate a dep of a dev dep as a required dev dep', () => { + expect(dep('commander').relationship.getType()).equal(DepType.DEV); + expect(dep('commander').relationship.getRequired()).equal(DepRequireState.REQUIRED); }); - it('should locate a dep of a dev dep that is also a top level prod dep as a prod dep', () => { - expect(dep('debug')).to.have.property('depType', DepType.PROD); + it('should locate a dep of a dev dep that is also a top level prod dep as a required prod dep', () => { + expect(dep('debug').relationship.getType()).equal(DepType.PROD); + expect(dep('debug').relationship.getRequired()).equal(DepRequireState.REQUIRED); }); - it('should locate a dep of a dev dep that is optional as a dev_optional dep', function () { + it('should locate a dep of a dev dep that is optional as an optional dev dep', function () { if (process.platform !== 'darwin') { this.skip(); } - expect(dep('fsevents')).to.have.property('depType', DepType.DEV_OPTIONAL); + expect(dep('fsevents').relationship.getType()).to.equal(DepType.DEV); + expect(dep('fsevents').relationship.getRequired()).to.equal(DepRequireState.OPTIONAL); }); }); diff --git a/test/depTypes_spec.ts b/test/depTypes_spec.ts index 75f9863..21c6f34 100644 --- a/test/depTypes_spec.ts +++ b/test/depTypes_spec.ts @@ -1,63 +1,45 @@ import { expect } from 'chai'; -import { DepType, childDepType, depTypeGreater } from '../src/depTypes'; +import { DepType, DepRequireState, childRequired, depTypeGreater, depRequireStateGreater } from '../src/depTypes'; describe('depTypes', () => { - describe('enum', () => { + describe('DepType enum', () => { it('should contain unique numbers', () => { expect( Object.keys(DepType) .map(key => DepType[key]) .filter(value => typeof value === 'number') .length - ).to.equal(5); + ).to.equal(3); }); }); - describe('childDepType', () => { - it('should throw an error if you try to calculate the child type of a "root" child', () => { - expect(() => childDepType(DepType.PROD, DepType.ROOT)).to.throw(); + describe('DepRequireState enum', () => { + it('should contain unique numbers', () => { + expect( + Object.keys(DepRequireState) + .map(key => DepRequireState[key]) + .filter(value => typeof value === 'number') + .length + ).to.equal(2); }); + }); + describe('childRequired', () => { it('should mark children of optional deps as optional', () => { - expect(childDepType(DepType.OPTIONAL, DepType.DEV)).to.equal(DepType.OPTIONAL); - expect(childDepType(DepType.OPTIONAL, DepType.PROD)).to.equal(DepType.OPTIONAL); - expect(childDepType(DepType.OPTIONAL, DepType.OPTIONAL)).to.equal(DepType.OPTIONAL); - }); - - it('should mark non-optional deps of prod deps as prod', () => { - expect(childDepType(DepType.PROD, DepType.DEV)).to.equal(DepType.PROD); - expect(childDepType(DepType.PROD, DepType.PROD)).to.equal(DepType.PROD); - }); - - it('should mark optional deps of prod deps as optional', () => { - expect(childDepType(DepType.PROD, DepType.OPTIONAL)).to.equal(DepType.OPTIONAL); - }); - - it('should mark non-optional deps of dev deps as dev', () => { - expect(childDepType(DepType.DEV, DepType.PROD)).to.equal(DepType.DEV); - expect(childDepType(DepType.DEV, DepType.DEV)).to.equal(DepType.DEV); + expect(childRequired(DepRequireState.OPTIONAL, DepRequireState.REQUIRED)).to.equal(DepRequireState.OPTIONAL); }); - // THIS IS REQUIRED BEHAVIOR, DO NOT CHANGE - // For future context, this is just so we don't leave around optional transitive - // deps. Using dev_optional is :ok: because it ensures that we don't keep them - it('should mark optional deps of dev deps as dev_optional deps', () => { - expect(childDepType(DepType.DEV, DepType.OPTIONAL)).to.equal(DepType.DEV_OPTIONAL); + it('should mark required children of required deps as required', () => { + expect(childRequired(DepRequireState.REQUIRED, DepRequireState.REQUIRED)).to.equal(DepRequireState.REQUIRED); }); - it('should mark deps of the root project as their native dep type', () => { - expect(childDepType(DepType.ROOT, DepType.DEV)).to.equal(DepType.DEV); - expect(childDepType(DepType.ROOT, DepType.OPTIONAL)).to.equal(DepType.OPTIONAL); - expect(childDepType(DepType.ROOT, DepType.PROD)).to.equal(DepType.PROD); + it('should mark optional children of required deps as optional', () => { + expect(childRequired(DepRequireState.REQUIRED, DepRequireState.OPTIONAL)).to.equal(DepRequireState.OPTIONAL); }); }); describe('depTypeGreater', () => { - it('should report OPTIONAL > DEV', () => { - expect(depTypeGreater(DepType.OPTIONAL, DepType.DEV)).to.equal(true); - }); - it('should report PROD > DEV', () => { expect(depTypeGreater(DepType.PROD, DepType.DEV)).to.equal(true); }); @@ -66,26 +48,10 @@ describe('depTypes', () => { expect(depTypeGreater(DepType.ROOT, DepType.DEV)).to.equal(true); }); - it('should report DEV < OPTIONAL', () => { - expect(depTypeGreater(DepType.DEV, DepType.OPTIONAL)).to.equal(false); - }); - - it('should report PROD > OPTIONAL', () => { - expect(depTypeGreater(DepType.PROD, DepType.OPTIONAL)).to.equal(true); - }); - - it('should report ROOT > OPTIONAL', () => { - expect(depTypeGreater(DepType.ROOT, DepType.OPTIONAL)).to.equal(true); - }); - it('should report DEV < PROD', () => { expect(depTypeGreater(DepType.DEV, DepType.PROD)).to.equal(false); }); - it('should report OPTIONAL < PROD', () => { - expect(depTypeGreater(DepType.OPTIONAL, DepType.PROD)).to.equal(false); - }); - it('should report ROOT > PROD', () => { expect(depTypeGreater(DepType.ROOT, DepType.PROD)).to.equal(true); }); @@ -94,12 +60,29 @@ describe('depTypes', () => { expect(depTypeGreater(DepType.DEV, DepType.ROOT)).to.equal(false); }); - it('should report OPTIONAL < ROOT', () => { - expect(depTypeGreater(DepType.OPTIONAL, DepType.ROOT)).to.equal(false); - }); - it('should report PROD < ROOT', () => { expect(depTypeGreater(DepType.PROD, DepType.ROOT)).to.equal(false); }); }); + + describe('depRequireStateGreater', () => { + it('should report REQUIRED > OPTIONAL', () => { + expect(depRequireStateGreater(DepRequireState.REQUIRED, DepRequireState.OPTIONAL)).to.equal(true); + }); + + it('should report OPTIONAL < REQUIRED', () => { + expect(depRequireStateGreater(DepRequireState.OPTIONAL, DepRequireState.REQUIRED)).to.equal(false); + }); + + /** + * These tests ensure that the method will not modify things that are identical + */ + it('should report OPTIONAL < OPTIONAL', () => { + expect(depRequireStateGreater(DepRequireState.OPTIONAL, DepRequireState.OPTIONAL)).to.equal(false); + }); + + it('should report OPTIONAL < REQUIRED', () => { + expect(depRequireStateGreater(DepRequireState.REQUIRED, DepRequireState.REQUIRED)).to.equal(false); + }); + }); });