Skip to content

Commit

Permalink
Merge pull request #8 from jalal246/dev
Browse files Browse the repository at this point in the history
Introduce better algorithm to solve dependencies
  • Loading branch information
jalal246 authored Mar 5, 2020
2 parents 0ae74a0 + 765e20b commit 35dbbe8
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 88 deletions.
122 changes: 102 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,76 @@
# Package Sorter

> A function used for monorepos production build.
> A function sorts a group of packages that depends on each other :nerd_face:
When you have projects depend on each other. You have to build core first, then
the project depends on it and so on. You probably want this step to be automated
so you can use: `package-sorter(unsortedPackages[], coreDependency)`.

_If `coreDependency` is not passed, `package-sorter` will extract it as `@coreDep/`_
the project depends on it, and so on. You probably want this step to be automated
so you can use `package-sorter(unsortedPackages[], coreDependency)`.

```bash
npm install package-sorter
```

### Example:
## API

```js
/**
* @param {Array} [packages=[]] - packages in workspace.
* @param {string} coreDependency - core package that other packages depends on it.
*
* @returns {Object} result
* @returns {Array} result.sorted
* @returns {Array} result.unSorted
*/
const { sorted, unSorted } = packageSorter(packages, coreDependency);
```

If `coreDependency` is not passed, `package-sorter` will extract it following
monorepo naming pattern as: `@coreDep/`

> Why returns `unSorted`?
> Just in case, packages are missing the main dependency will be added to
> unSorted. Then you can figure out what's missing before production.
### Example (1) - All Sorted

```js
import packageSorter from "package-sorter";

// packages in your monorepo:
// input packages:
const pkg0 = {
name: "@folo/withcontext",
dependencies: {}
};

const pkg1 = {
name: "@folo/values",
dependencies: {
"@folo/withcontext": "^0.1.5"
}
};

const pkg2 = {
name: "@folo/layout",
dependencies: {
"@folo/values": "^0.1.5"
}
};

const packages = [pkg2, pkg1, pkg0];

// our core dependency in this case is: @folo.
const { sorted, unSorted } = sortPackages(packages, "@folo");

// sorted: [pkg0, pkg1, pkg2];
// unSorted: []
```

### Example (2) - Mixed Packages

```js
import packageSorter from "package-sorter";

// input packages:
const pkg0 = {
name: "@folo/withcontext",
dependencies: {}
Expand All @@ -32,39 +84,69 @@ const pkg1 = {
};

const pkg2 = {
name: "@folo/utils",
name: "unrelated",
dependencies: {}
};

const pkg3 = {
name: "@folo/layout",
const packages = [pkg2, pkg1, pkg0];

// let's the function get core dependency.
const { sorted } = sortPackages(packages);

// sorted: [pkg2, pkg0, pkg1]
// unSorted: []
```

### Example (3) - Some Unsorted

```js
import packageSorter from "package-sorter";

// input packages:
const pkg0 = {
name: "@folo/withcontext",
dependencies: {}
};

const pkg1 = {
name: "@folo/values",
dependencies: {
"@folo/withcontext": "^0.1.5"
}
};

const pkg4 = {
name: "@folo/forms",
const pkg2 = {
name: "@folo/unsortable",
dependencies: {
"@folo/layout": "^0.1.4",
"@folo/values": "^0.1.4"
"@folo/missing": "^0.1.5"
}
};

const unsortedPackages = [pkg1, pkg2, pkg3, pkg0, pkg4];
const packages = [pkg2, pkg1, pkg0];

// our core dependency in this case is: @folo.
const sorted = sortPackages(unsortedPackages, "@folo");
const { sorted } = sortPackages(packages);

//=> [pkg2, pkg0, pkg1, pkg4, pkg3]
// sorted: [pkg0, pkg1]
// unSorted: [pkg2]
```

## Tests
### Related projects

- [move-position](https://github.com/jalal246/move-position) - Moves element
index in given array from position A to B.

- [builderz](https://github.com/jalal246/builderz) - Building your project with zero config.

- [corename](https://github.com/jalal246/corename) - Extracts package name.

- [get-info](https://github.com/jalal246/get-info) - Utility functions for projects production.

### Test

```sh
npm test
```

## License
### License

This project is licensed under the [GPL-3.0 License](https://github.com/jalal246/packageSorter/blob/master/LICENSE)
129 changes: 89 additions & 40 deletions src/packageSorter.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
/* eslint-disable strict */

"use strict";

const getCoreName = require("corename");

let sorted;
let unSorted;
let coreDep;
let elemAdded;

/**
* Checks if targeted dependency is already added to sorted array.
*
* @param {string} dep - name of targeted dependency.
* @returns {boolean}
*/
function isAddedToSorted(dep) {
function isDepInSorted(dep) {
/**
* Check if dependency already added, using name package matching.
*/
Expand All @@ -18,91 +24,134 @@ function isAddedToSorted(dep) {
}

/**
* Checks if given package should be added to sorted array.
*
* If package dependencies doesn't matched coreDep, then returns true. It
* should be added because it's neutral.
*
* Otherwise, checks, if it exists before, then true to add it, if not false.
* Because we should add the essential dependency first.
* Checks if given package has decency in coreDep.
*
* @param {Object} packageDeps
* @returns {boolean}
*
* @returns {Object} result
* @returns {boolean} result.hasCoreDep
* @returns {Object} result.dep
*/
function isAddPackage(packageDeps) {
let isAdd = true;
function isPackageNeedCoreDep(packageDeps) {
let hasCoreDep = false;
let dep;

const packageDepsArr = Object.keys(packageDeps);

for (let i = 0; i < packageDepsArr.length; i += 1) {
const dep = packageDepsArr[i];
dep = packageDepsArr[i];

if (dep.includes(coreDep)) {
isAdd = isAddedToSorted(dep);
hasCoreDep = dep.includes(coreDep);

if (isAdd) break;
}
if (hasCoreDep) break;
}

return isAdd;
return { hasCoreDep, dep };
}

/**
* Adds package at(index) to sorted or inSorted.
*
* @param {Array} packages - packages in workspace.
* @param {number} at - index
* @param {boolean} isSorted -
*/
function addTo(packages, at, isSorted) {
const target = isSorted ? sorted : unSorted;

target.push(packages[at]);

/**
* remove it from packages so it won't be checked next time.
*/
packages.splice(at, 1);

elemAdded += 1;
}

/**
* Loop into packages. Add package that don't require coreDep first, then add
* coreDep, then other packages.
*
* @param {Array} packages - packages in workspace.
*/
function sort(packages) {
let noChange = false;
packages.forEach(({ dependencies = {} }, i) => {
let isAddToSorted = false;

let hasCoreDep = false;
let dep = {};

for (let i = 0; i < packages.length; i += 1) {
const pkg = packages[i];

const { dependencies } = pkg;

({ hasCoreDep, dep } = isPackageNeedCoreDep(dependencies));

/**
* Checks if this package is already existed in sorted array or even has
* coreDep
* When to add package to sorted?
* - Neutral. Doesn't have hasCoreDep, then add it to sorted.
* - Not natural, but its core dep is already added.
*/
const isAdd = isAddPackage(dependencies, coreDep);

if (isAdd) {
sorted.push(packages[i]);
isAddToSorted = !hasCoreDep || isDepInSorted(dep);

/**
* remove it from unsorted
*/
packages.splice(i, 1);
if (isAddToSorted) {
addTo(packages, i, true);

noChange = true;
break;
}
});
}

return noChange;
/**
* Has hasCoreDep but couldn't add it.
* - Add it to unsorted.
* - remove it form packages.
*/
if (!isAddToSorted && hasCoreDep) {
addTo(packages, 0, false);
}
}

/**
* Sorting packages. Package with no deps will come first, then package that
* depending of package that is built. This is essential for monorepo build.
*
* @param {Array} packages - contains dependencies for each package in workspace.
* @param {*} [packages=[]] - packages in workspace.
* @param {string} coreDependency - core package that other packages depend on.
* @returns {Array} - Sorted Array.
*
* @returns {Object} result
* @returns {Array} result.sorted
* @returns {Array} result.unSorted
*/
function packageSorter(packages = [], coreDependency) {
unSorted = [];

/**
* Nothing to sort when:
* 1- have only one package.
* 2- can't discover the coreDep (which may be due to packages not depending
* on each other aka already sorted)
*/
if (packages.length <= 1) return packages;
if (packages.length <= 1) return { sorted: packages, unSorted };

coreDep = coreDependency || getCoreName(packages);

if (!coreDep) return packages;
if (!coreDep) return { sorted: packages, unSorted };

const totalLength = packages.length;
sorted = [];
while (packages.length > 0) {
const noChange = sort(packages);
if (!noChange) break;

elemAdded = 0;

while (sorted.length < totalLength) {
sort(packages);

if (elemAdded === totalLength) {
break;
}
}

return sorted;
return { sorted, unSorted };
}

module.exports = packageSorter;
Loading

0 comments on commit 35dbbe8

Please sign in to comment.