Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better syntax for optional / required dependencies #4

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ const walker = new Walker(modulePath);
Returns `Promise<Module[]>`

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.
36 changes: 19 additions & 17 deletions src/Walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -13,7 +13,7 @@ export interface PackageJSON {
}
export interface Module {
path: string;
depType: DepType;
relationship: DepRelationship;
name: string;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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}"
Expand All @@ -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;
}
Expand All @@ -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
Expand All @@ -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)),
);
}
}
Expand All @@ -142,7 +144,7 @@ export class Walker {
await this.walkDependenciesForModuleInModule(
moduleName,
modulePath,
childDepType(depType, DepType.OPTIONAL),
new DepRelationship(childDepType, childRequired(relationship.getRequired(), DepRequireState.OPTIONAL)),
);
}
}
Expand All @@ -154,7 +156,7 @@ export class Walker {
this.cache = new Promise<Module[]>(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;
Expand Down
82 changes: 36 additions & 46 deletions src/depTypes.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -46,19 +44,15 @@ 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;
}
case DepType.ROOT:
switch (newType) {
case DepType.ROOT:
case DepType.PROD:
case DepType.OPTIONAL:
case DepType.DEV:
case DepType.DEV_OPTIONAL:
default:
return false;
}
Expand All @@ -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;
}
}
27 changes: 16 additions & 11 deletions test/Walker_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
});
Loading