Skip to content

Commit

Permalink
feat: richer return API
Browse files Browse the repository at this point in the history
  • Loading branch information
James Zetlen committed Jan 29, 2020
1 parent 5dda68e commit a7eb4fa
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 15 deletions.
20 changes: 10 additions & 10 deletions src/ExplicitDependency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ export default class ExplicitDependency {
* Real path on disk of this module.
*/
private modulePath: string;

/**
* Cache storage to prevent unnecessary recalculation of the same list.
* The same subject should result in the same list while this object exists.
* Name of this package; for use in dependency detection.
*/
private pertainCache: Map<string, string | boolean>;
get name() {
return this.pkg.name;
}

constructor(modulePath: string) {
this.modulePath = modulePath;
this.pertainCache = new Map();
this.pkg = new PackageJson(modulePath);
}

Expand Down Expand Up @@ -61,32 +62,31 @@ export default class ExplicitDependency {
* `package.json`must have a top level `pwa` object with a `build` property.
*/
pertains(subject: string) {
const { pertainCache } = this;
if (pertainCache.has(subject)) {
return pertainCache.get(subject);
}
const pertaining = PackageJson.lookup(this.pkg, subject);

// Only strings can be file paths, so anything else does not pertain
if (!pertaining || typeof pertaining !== 'string') {
debug(
'%s: Subject %s resolved to %s',
this.pkg.name,
subject,
pertaining
);
pertainCache.set(subject, false);
return false;
}

debug(
'found declaration of "%s" as "%s" in "%s"',
subject,
pertaining,
this.pkg.name
);

// This is the indicated path which would pertain; is it requireable?
const subscriber = path.resolve(this.modulePath, pertaining);
try {
const pertainingModule = require.resolve(subscriber);
debug('found runnable module at %s', pertainingModule);
pertainCache.set(subject, pertainingModule);
return pertainingModule;
} catch (e) {
throw new PertainError(
Expand Down
28 changes: 25 additions & 3 deletions src/ExplicitDependencySet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@ import { Resolver } from './resolver';
import ExplicitDependency from './ExplicitDependency';
import topologicalSort from './topologicalSort';

/**
* A set of modules that can be queried for pertaining to a given subject.
*/
export default class ExplicitDependencySet {
private resolve: Resolver;
private dependencies: ExplicitDependency[];

/**
* Cache storage to prevent unnecessary recalculation of the same list.
* The same subject should result in the same list while this object exists.
*/
private sortedBySubject: Map<string, ExplicitDependency[]>;

constructor(resolve: Resolver, names: string[]) {
this.resolve = resolve;
this.dependencies = [];
this.sortedBySubject = new Map();
names.forEach(name => this.add(name));
}

/**
* Only add dependencies which can be resolved in the first place. If there
* is a problem finding them, just skip them; they don't pertain!
*/
private add(name: string) {
const modulePath = this.resolve(name);
if (modulePath) {
Expand All @@ -21,18 +34,27 @@ export default class ExplicitDependencySet {
}
}

/**
* Gets a list of ExplicitDependencies in this set which pertain to the
* subject (that is, their `package.json` has a valid key for the subject
* that indicates a requireable file). Detects dependencies between the
* packages that pertain and sorts the list in dependency order.
*/
pertaining(subject: string) {
let sorted = this.sortedBySubject.get(subject);
if (!sorted) {
const pertaining: ExplicitDependency[] = this.dependencies.filter(
dependency => dependency.pertains(subject)
);
sorted = topologicalSort(pertaining, dependency =>
// Returns a list of dependents (not dependencies) of the supplied
// dependency. This is the data structure needed for an efficient
// topological sort.
const getOutgoingEdges = (dependency: ExplicitDependency) =>
pertaining.filter(
dependent =>
dependent !== dependency && dependent.dependsOn(dependency.name)
)
);
);
sorted = topologicalSort(pertaining, getOutgoingEdges);
this.sortedBySubject.set(subject, sorted);
}
return sorted;
Expand Down
9 changes: 9 additions & 0 deletions src/PackageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ export interface JsonMap<T> {
[key: string]: T;
}

/**
* Lazy-loading proxy for package.json. A subset of properties are available,
* and a static method
* `PackageJson.lookup(package: PackageJson, dotPath: string)`
* exists for looking up other arbitrary properties.
*
* This speeds up dependency gathering; it won't be necessary to read all
* package files from disk.
*/
export default class PackageJson {
/**
* Look up a custom dot-path on a package.json instance. Abstracting this
Expand Down
5 changes: 5 additions & 0 deletions src/Resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import resolvePkg from 'resolve-pkg';

export type Resolver = (modulePath: string) => string | undefined;

/**
* Returns a function which resolves filesystem base directories, given module
* names. The returned function will resolve all modules starting from the `cwd`
* passed to the resolver factory.
*/
export default function resolver(cwd: string): Resolver {
return modulePath => resolvePkg(modulePath, { cwd });
}
54 changes: 52 additions & 2 deletions src/pertain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,75 @@ import PackageJson from './PackageJson';

const debug = makeDebug('pertain:main');

/**
* A Node package which declares in its `package.json` file that it contains
* a script module which pertains to a particular subject. Use this object
* to require the pertaining module and/or report the name of the subscriber.
*/
export interface Pertaining {
/**
* The name of the package..
* @example "@org/package-name"
*/
name: string;
/**
* The filesystem path of the package's root directory.
* @example "/var/www/venia/node_modules/@org/package-name"
*/
path: string;
/**
* The subject that was originally passed to `pertain()` to produce this
* object.
* @example "pwa.webpack"
*/
subject: string;
}

/**
* Caches the ExplicitDependencySet calculated for a given project root.
* It should not change during the lifetime of this process, so there's no
* point in recalculating it.
*/
const dependencySetCache = new Map<string, ExplicitDependencySet>();

function pertain(rootDir: string, subject: string): string[] {
/**
* Query the direct dependencies of the Node project at `rootDir` for all
* packages which have a particular `package.json` property. Return them in
* peerDependency order.
*/
function pertain(rootDir: string, subject: string): Pertaining[] {
const absRoot = path.resolve(rootDir);
let depSet = dependencySetCache.get(absRoot);
if (!depSet) {
debug('no cached depset for %s', absRoot);
// A convenience function which can be replaced with an alternate resolver
// algorithm.
const resolve = resolver(absRoot);
const { dependencies, devDependencies } = new PackageJson(absRoot);

// Merging the two dependency sets that we look at will dedupe them.
// We don't care whether it comes from devDependencies or dependencies.
// Both are relevant, because many applications with a build step compile
// code from devDependencies.
const allDependencyNames = Object.keys(
Object.assign({}, dependencies, devDependencies)
);
debug('%s allDependencyNames %s', absRoot, allDependencyNames);
depSet = new ExplicitDependencySet(resolve, allDependencyNames);
dependencySetCache.set(absRoot, depSet);
}
return depSet.pertaining(subject).map(dep => dep.pertains(subject) as string);
return depSet.pertaining(subject).map(dep => ({
name: dep.name,
path: dep.pertains(subject) as string,
subject
}));
}

/**
* Clear out the cache of dependencies that have already been detected and
* loaded. Use if the dependency graph changes and you want to "hot reload"
* functionality. Or, for testing.
*/
pertain.clearCache = () => dependencySetCache.clear();

export default pertain;
9 changes: 9 additions & 0 deletions src/topologicalSort.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import PertainError from './PertainError';

/**
* Topologically sort a list of items of any type, given a list of items and
* a map of outward relations between those items.
*
* Credit to https://github.com/marcelklehr/toposort/ for a great
* implementation I had to adapt to be faster for our particular use case.
*
* That code appears here courtesy of the MIT license.
*/
export default function topologicalSort<T>(
nodes: T[],
outgoingFrom: (node: T) => T[]
Expand Down

0 comments on commit a7eb4fa

Please sign in to comment.