feat(graph-sequencer): add package @pnpm/graph-sequencer (#7168)

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
阿豪
2023-10-16 03:28:05 +08:00
committed by GitHub
parent cf508e0680
commit 4246f41bed
21 changed files with 618 additions and 111 deletions

View File

@@ -0,0 +1,10 @@
---
"@pnpm/plugin-commands-rebuild": patch
"@pnpm/deps.graph-sequencer": major
"@pnpm/sort-packages": patch
"@pnpm/build-modules": patch
"@pnpm/core": patch
"@pnpm-private/typings": patch
---
Add package @pnpm/deps.graph-sequencer for better topological sort [#7168](https://github.com/pnpm/pnpm/pull/7168).

View File

@@ -79,29 +79,6 @@ declare module 'graceful-git' {
export = anything
}
declare module '@pnpm/graph-sequencer' {
namespace graphSequencer {
type Graph<T> = Map<T, T[]>
type Groups<T> = T[][]
interface Options<T> {
graph: Graph<T>
groups: Groups<T>
}
interface Result<T> {
safe: boolean
chunks: Groups<T>
cycles: Groups<T>
}
type GraphSequencer = <T>(opts: Options<T>) => Result<T>
}
const graphSequencer: graphSequencer.GraphSequencer
export = graphSequencer
}
declare module 'is-inner-link' {
const anything: any
export = anything

30
deps/graph-sequencer/README.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# @pnpm/deps.graph-sequencer
> Sort items in a graph using a topological sort
## Install
```
pnpm add @pnpm/deps.graph-sequencer
```
## Usage
```ts
expect(graphSequencer(new Map([
[0, [1]],
[1, [2]],
[2, [3]],
[3, [0]],
]), [0, 1, 2, 3])).toStrictEqual(
{
safe: false,
chunks: [[0, 1, 2, 3]],
cycles: [[0, 1, 2, 3]],
}
)
```
## License
[MIT](LICENSE)

1
deps/graph-sequencer/jest.config.js vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config.js')

41
deps/graph-sequencer/package.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "@pnpm/deps.graph-sequencer",
"version": "0.0.0",
"description": "Sort items in a graph using a topological sort",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=16.14"
},
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer",
"keywords": [
"pnpm8",
"pnpm",
"graph"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer#readme",
"dependencies": {
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/deps.graph-sequencer": "workspace:*"
},
"exports": {
".": "./lib/index.js"
}
}

116
deps/graph-sequencer/src/index.ts vendored Normal file
View File

@@ -0,0 +1,116 @@
export type Graph<T> = Map<T, T[]>
export type Groups<T> = T[][]
export interface Options<T> {
graph: Graph<T>
groups: Groups<T>
}
export interface Result<T> {
safe: boolean
chunks: Groups<T>
cycles: Groups<T>
}
/**
* Performs topological sorting on a graph while supporting node restrictions.
*
* @param {Graph<T>} graph - The graph represented as a Map where keys are nodes and values are their outgoing edges.
* @param {T[]} includedNodes - An array of nodes that should be included in the sorting process. Other nodes will be ignored.
* @returns {Result<T>} An object containing the result of the sorting, including safe, chunks, and cycles.
*/
export function graphSequencer<T> (graph: Graph<T>, includedNodes: T[] = [...graph.keys()]): Result<T> {
// Initialize reverseGraph with empty arrays for all nodes.
const reverseGraph = new Map<T, T[]>()
for (const key of graph.keys()) {
reverseGraph.set(key, [])
}
// Calculate outDegree and reverseGraph for the included nodes.
const nodes = new Set<T>(includedNodes)
const visited = new Set<T>()
const outDegree = new Map<T, number>()
for (const [from, edges] of graph.entries()) {
outDegree.set(from, 0)
for (const to of edges) {
if (nodes.has(from) && nodes.has(to)) {
changeOutDegree(from, 1)
reverseGraph.get(to)!.push(from)
}
}
if (!nodes.has(from)) {
visited.add(from)
}
}
const chunks: T[][] = []
const cycles: T[][] = []
let safe = true
while (nodes.size) {
const chunk: T[] = []
let minDegree = Number.MAX_SAFE_INTEGER
for (const node of nodes) {
const degree = outDegree.get(node)!
if (degree === 0) {
chunk.push(node)
}
minDegree = Math.min(minDegree, degree)
}
if (minDegree === 0) {
chunk.forEach(removeNode)
chunks.push(chunk)
} else {
const cycleNodes: T[] = []
for (const node of nodes) {
const cycle = findCycle(node)
if (cycle.length) {
cycles.push(cycle)
cycle.forEach(removeNode)
cycleNodes.push(...cycle)
if (cycle.length > 1) {
safe = false
}
}
}
chunks.push(cycleNodes)
}
}
return { safe, chunks, cycles }
// Function to update the outDegree of a node.
function changeOutDegree (node: T, value: number) {
const degree = outDegree.get(node) ?? 0
outDegree.set(node, degree + value)
}
// Function to remove a node from the graph.
function removeNode (node: T) {
for (const from of reverseGraph.get(node)!) {
changeOutDegree(from, -1)
}
visited.add(node)
nodes.delete(node)
}
function findCycle (startNode: T): T[] {
const queue: Array<[T, T[]]> = [[startNode, [startNode]]]
const cycleVisited = new Set<T>()
while (queue.length) {
const [id, cycle] = queue.shift()!
for (const to of graph.get(id)!) {
if (to === startNode) {
return cycle
}
if (visited.has(to) || cycleVisited.has(to)) {
continue
}
cycleVisited.add(to)
queue.push([to, [...cycle, to]])
}
}
return []
}
}

351
deps/graph-sequencer/test/index.ts vendored Normal file
View File

@@ -0,0 +1,351 @@
import { graphSequencer } from '../src'
test('graph with three independent self-cycles', () => {
expect(graphSequencer(new Map([
['a', ['a']],
['b', ['b']],
['c', ['c']],
]
))).toStrictEqual(
{
safe: true,
chunks: [['a', 'b', 'c']],
cycles: [
['a'], ['b'], ['c'],
],
}
)
})
test('graph with self-cycle. Sequencing a subgraph', () => {
expect(graphSequencer(new Map([
['a', ['a']],
['b', ['b']],
['c', ['c']],
]), ['a', 'b'])).toStrictEqual(
{
safe: true,
chunks: [['a', 'b']],
cycles: [['a'], ['b']],
}
)
})
test('graph with two self-cycles and an edge linking them', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', ['b']],
['c', ['b', 'c']]]
))).toStrictEqual(
{
safe: true,
chunks: [['b', 'c'], ['a']],
cycles: [
['b'], ['c'],
],
}
)
})
test('graph with nodes connected to each other sequentially without forming a cycle', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', []],
['c', ['b']]]
))).toStrictEqual(
{
safe: true,
chunks: [['b'], ['c'], ['a']],
cycles: [],
}
)
})
test('graph sequencing with a subset of 3 nodes, ignoring 2 nodes, in a 5-node graph', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', []],
['c', []],
['d', ['a']],
['e', ['a', 'b', 'c']]]
), ['a', 'd', 'e'])).toStrictEqual(
{
safe: true,
chunks: [['a'], ['d', 'e']],
cycles: [],
}
)
})
test('graph with no edges', () => {
expect(graphSequencer(new Map([
['a', []],
['b', []],
['c', []],
['d', []],
]))).toStrictEqual(
{
safe: true,
chunks: [['a', 'b', 'c', 'd']],
cycles: [],
}
)
})
test('graph of isolated nodes with no edges, sequencing a subgraph of selected nodes', () => {
expect(graphSequencer(new Map([
['a', []],
['b', []],
['c', []],
['d', []],
]), ['a', 'b', 'c'])).toStrictEqual(
{
safe: true,
chunks: [['a', 'b', 'c']],
cycles: [],
}
)
})
test('graph with multiple dependencies on one item', () => {
expect(graphSequencer(new Map([
['a', ['d']],
['b', ['d']],
['c', []],
['d', []],
]))).toStrictEqual(
{
safe: true,
chunks: [['c', 'd'], ['a', 'b']],
cycles: [],
}
)
})
test('graph with resolved cycle', () => {
expect(graphSequencer(new Map([
['a', ['b']],
['b', ['c']],
['c', ['d']],
['d', ['a']],
]))).toStrictEqual(
{
safe: false,
chunks: [['a', 'b', 'c', 'd']],
cycles: [['a', 'b', 'c', 'd']],
}
)
})
test('graph with a cycle, but sequencing a subgraph that avoids the cycle', () => {
expect(graphSequencer(new Map([
['a', ['b']],
['b', ['c']],
['c', ['d']],
['d', ['a']],
]), ['a', 'b', 'c'])).toStrictEqual(
{
safe: true,
chunks: [['c'], ['b'], ['a']],
cycles: [],
}
)
})
test('graph with resolved cycle with multiple unblocked deps', () => {
expect(graphSequencer(new Map([
['a', ['d']],
['b', ['d']],
['c', ['d']],
['d', ['a']],
]))).toStrictEqual(
{
safe: false,
chunks: [
['a', 'd'],
['b', 'c'],
],
cycles: [['a', 'd']],
}
)
})
test('graph with resolved cycle with multiple unblocked deps subgraph', () => {
expect(graphSequencer(new Map([
['a', ['d']],
['b', ['d']],
['c', ['d']],
['d', ['a']],
]), ['a', 'b', 'c'])).toStrictEqual(
{
safe: true,
chunks: [
['a', 'b', 'c'],
],
cycles: [],
}
)
})
test('graph with two cycles', () => {
expect(graphSequencer(new Map([
['a', ['b']],
['b', ['a']],
['c', ['d']],
['d', ['c']],
]))).toStrictEqual(
{
safe: false,
chunks: [['a', 'b', 'c', 'd']],
cycles: [
['a', 'b'],
['c', 'd'],
],
}
)
})
test('graph with multiple cycles. case 1', () => {
expect(graphSequencer(new Map([
['a', ['c']],
['b', ['a', 'd']],
['c', ['b']],
['d', ['c', 'e']],
['e', []],
]))).toStrictEqual(
{
safe: false,
chunks: [['e'], ['a', 'c', 'b'], ['d']],
cycles: [['a', 'c', 'b']],
}
)
})
test('graph with multiple cycles. case 2', () => {
expect(graphSequencer(new Map([
['a', ['b']],
['b', ['d']],
['c', []],
['d', ['b', 'c']],
]))).toStrictEqual(
{
safe: false,
chunks: [['c'], ['b', 'd'], ['a']],
cycles: [['b', 'd']],
}
)
})
test('graph with fully connected subgraph and additional connected node', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c', 'd']],
['b', ['a', 'c', 'd']],
['c', ['a', 'b', 'd']],
['d', ['a', 'b', 'c']],
['e', ['b']],
]))).toStrictEqual(
{
safe: false,
chunks: [['a', 'b', 'c', 'd'], ['e']],
cycles: [
['a', 'b'],
['c', 'd'],
],
}
)
})
test('graph with fully connected subgraph. case 1', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c', 'd']],
['b', ['a', 'c', 'd']],
['c', ['a', 'b', 'd']],
['d', ['a', 'b', 'c']],
['e', ['b']],
]), ['b', 'e'])).toStrictEqual(
{
safe: true,
chunks: [['b'], ['e']],
cycles: [],
}
)
})
test('graph with fully connected subgraph. case 2', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c', 'd']],
['b', ['a', 'c', 'd']],
['c', ['a', 'b', 'd']],
['d', ['a', 'b', 'c']],
['e', ['b']],
]), ['a', 'b', 'e'])).toStrictEqual(
{
safe: false,
chunks: [['a', 'b'], ['e']],
cycles: [['a', 'b']],
}
)
})
test('graph with two self-cycles', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', ['b']],
['c', ['c']],
]))).toStrictEqual(
{
safe: true,
chunks: [['b', 'c'], ['a']],
cycles: [['b'], ['c']],
}
)
})
test('graph with two self-cycles. Sequencing a subgraph', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', ['b']],
['c', ['c']],
]), ['b', 'c'])).toStrictEqual(
{
safe: true,
chunks: [['b', 'c']],
cycles: [['b'], ['c']],
}
)
})
test('graph with many nodes', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', []],
['c', []],
['d', ['a']],
['e', ['a', 'b', 'c']],
]))).toStrictEqual(
{
safe: true,
chunks: [['b', 'c'], ['a'], ['d', 'e']],
cycles: [],
}
)
})
test('graph with many nodes. Sequencing a subgraph', () => {
expect(graphSequencer(new Map([
['a', ['b', 'c']],
['b', []],
['c', []],
['d', ['a']],
['e', ['a', 'b', 'c']],
]), ['a', 'd', 'e'])).toStrictEqual(
{
safe: true,
chunks: [['a'], ['d', 'e']],
cycles: [],
}
)
})

14
deps/graph-sequencer/tsconfig.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
],
"composite": true
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -37,7 +37,7 @@
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/fs.hard-link-dir": "workspace:*",
"@pnpm/graph-sequencer": "1.1.1",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/lifecycle": "workspace:*",
"@pnpm/link-bins": "workspace:*",
"@pnpm/patching.apply-patch": "workspace:*",

View File

@@ -1,4 +1,4 @@
import graphSequencer from '@pnpm/graph-sequencer'
import { graphSequencer } from '@pnpm/deps.graph-sequencer'
import { type PackageManifest, type PatchFile } from '@pnpm/types'
import filter from 'ramda/src/filter'
@@ -36,10 +36,7 @@ export function buildSequence (
nodesToBuildArray
.map((depPath) => [depPath, onlyFromBuildGraph(Object.values(depGraph[depPath].children))])
)
const graphSequencerResult = graphSequencer({
graph,
groups: [nodesToBuildArray],
})
const graphSequencerResult = graphSequencer(graph, nodesToBuildArray)
const chunks = graphSequencerResult.chunks as string[][]
return chunks
}

View File

@@ -9,6 +9,9 @@
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../deps/graph-sequencer"
},
{
"path": "../../fs/hard-link-dir"
},

View File

@@ -56,7 +56,7 @@
"@pnpm/error": "workspace:*",
"@pnpm/fs.hard-link-dir": "workspace:*",
"@pnpm/get-context": "workspace:*",
"@pnpm/graph-sequencer": "1.1.1",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/lifecycle": "workspace:*",
"@pnpm/link-bins": "workspace:*",
"@pnpm/lockfile-types": "workspace:*",

View File

@@ -30,7 +30,7 @@ import * as dp from '@pnpm/dependency-path'
import { hardLinkDir } from '@pnpm/fs.hard-link-dir'
import loadJsonFile from 'load-json-file'
import runGroups from 'run-groups'
import graphSequencer from '@pnpm/graph-sequencer'
import { graphSequencer } from '@pnpm/deps.graph-sequencer'
import npa from '@pnpm/npm-package-arg'
import pLimit from 'p-limit'
import semver from 'semver'
@@ -275,10 +275,10 @@ async function _rebuild (
.map(([pkgName, reference]) => dp.refToRelative(reference, pkgName))
.filter((childRelDepPath) => childRelDepPath && nodesToBuildAndTransitive.has(childRelDepPath)))
}
const graphSequencerResult = graphSequencer({
const graphSequencerResult = graphSequencer(
graph,
groups: [nodesToBuildAndTransitiveArray],
})
nodesToBuildAndTransitiveArray
)
const chunks = graphSequencerResult.chunks as string[][]
const warn = (message: string) => {
logger.info({ message, prefix: opts.dir })

View File

@@ -30,6 +30,9 @@
{
"path": "../../config/normalize-registries"
},
{
"path": "../../deps/graph-sequencer"
},
{
"path": "../../fs/hard-link-dir"
},

View File

@@ -26,7 +26,7 @@
"@pnpm/error": "workspace:*",
"@pnpm/filter-lockfile": "workspace:*",
"@pnpm/get-context": "workspace:*",
"@pnpm/graph-sequencer": "1.1.1",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/headless": "workspace:*",
"@pnpm/hoist": "workspace:*",
"@pnpm/hooks.read-package-hook": "workspace:*",

View File

@@ -27,6 +27,9 @@
{
"path": "../../config/normalize-registries"
},
{
"path": "../../deps/graph-sequencer"
},
{
"path": "../../exec/build-modules"
},
@@ -158,4 +161,4 @@
}
],
"composite": true
}
}

34
pnpm-lock.yaml generated
View File

@@ -884,6 +884,12 @@ importers:
specifier: 0.28.20
version: 0.28.20
deps/graph-sequencer:
devDependencies:
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: 'link:'
env/node.fetcher:
dependencies:
'@pnpm/create-cafs-store':
@@ -1057,12 +1063,12 @@ importers:
'@pnpm/core-loggers':
specifier: workspace:*
version: link:../../packages/core-loggers
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
'@pnpm/fs.hard-link-dir':
specifier: workspace:*
version: link:../../fs/hard-link-dir
'@pnpm/graph-sequencer':
specifier: 1.1.1
version: 1.1.1
'@pnpm/lifecycle':
specifier: workspace:*
version: link:../lifecycle
@@ -1182,6 +1188,9 @@ importers:
'@pnpm/dependency-path':
specifier: workspace:*
version: link:../../packages/dependency-path
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
@@ -1191,9 +1200,6 @@ importers:
'@pnpm/get-context':
specifier: workspace:*
version: link:../../pkg-manager/get-context
'@pnpm/graph-sequencer':
specifier: 1.1.1
version: 1.1.1
'@pnpm/lifecycle':
specifier: workspace:*
version: link:../lifecycle
@@ -2886,6 +2892,9 @@ importers:
'@pnpm/dependency-path':
specifier: workspace:*
version: link:../../packages/dependency-path
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
@@ -2895,9 +2904,6 @@ importers:
'@pnpm/get-context':
specifier: workspace:*
version: link:../get-context
'@pnpm/graph-sequencer':
specifier: 1.1.1
version: 1.1.1
'@pnpm/headless':
specifier: workspace:*
version: link:../headless
@@ -6195,9 +6201,9 @@ importers:
workspace/sort-packages:
dependencies:
'@pnpm/graph-sequencer':
specifier: 1.1.1
version: 1.1.1
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -7932,10 +7938,6 @@ packages:
graceful-fs: 4.2.11(patch_hash=ivtm2a2cfr5pomcfbedhmr5v2q)
dev: true
/@pnpm/graph-sequencer@1.1.1:
resolution: {integrity: sha512-nlLogZV9i8J2z9vw1cHtKAX8Caj3WeYUw63G1ni2ULLwvb+FfRAhdIfDsuce0gUHdOClF/gsKN+7H28yryNlAw==}
dev: false
/@pnpm/hooks.types@1.0.1:
resolution: {integrity: sha512-Zx2hzwxBKv1RmFzyu4pEVY7QeIGUb54smSSYt8GcJgByn+uMXgwJ7ydv9t2Koc90QTqk8J3P2J+RDrZVIQpVQw==}
engines: {node: '>=16.14'}

View File

@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/workspace/sort-packages#readme",
"dependencies": {
"@pnpm/graph-sequencer": "1.1.1",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/types": "workspace:*"
},
"funding": "https://opencollective.com/pnpm",
@@ -38,4 +38,4 @@
"exports": {
".": "./lib/index.js"
}
}
}

View File

@@ -1,6 +1,6 @@
import type { ProjectsGraph } from '@pnpm/types'
import graphSequencer from '@pnpm/graph-sequencer'
import type { Result as GraphSequencerResult } from '@pnpm/graph-sequencer'
import { graphSequencer } from '@pnpm/deps.graph-sequencer'
import type { Result as GraphSequencerResult } from '@pnpm/deps.graph-sequencer'
export function sequenceGraph (pkgGraph: ProjectsGraph): GraphSequencerResult<string> {
const keys = Object.keys(pkgGraph)
@@ -9,62 +9,10 @@ export function sequenceGraph (pkgGraph: ProjectsGraph): GraphSequencerResult<st
keys.map((pkgPath) => [
pkgPath,
pkgGraph[pkgPath].dependencies.filter(
/* remove cycles of length 1 (ie., package 'a' depends on 'a'). They
confuse the graph-sequencer, but can be ignored when ordering packages
topologically.
See the following example where 'b' and 'c' depend on themselves:
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['b', ['b']],
['c', ['b', 'c']]]
),
groups: [['a', 'b', 'c']]})
returns chunks:
[['b'],['a'],['c']]
But both 'b' and 'c' should be executed _before_ 'a', because 'a' depends on
them. It works (and is considered 'safe' if we run:)
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['b', []],
['c', ['b']]]
), groups: [['a', 'b', 'c']]})
returning:
[['b'], ['c'], ['a']]
*/
d => d !== pkgPath &&
/* remove unused dependencies that we can ignore due to a filter expression.
Again, the graph sequencer used to behave weirdly in the following edge case:
graphSequencer({graph: new Map([
['a', ['b', 'c']],
['d', ['a']],
['e', ['a', 'b', 'c']]]
),
groups: [['a', 'e', 'e']]})
returns chunks:
[['d'],['a'],['e']]
But we really want 'a' to be executed first.
*/
setOfKeys.has(d))]
d => d !== pkgPath && setOfKeys.has(d))]
)
)
return graphSequencer({
graph,
groups: [keys],
})
return graphSequencer(graph, keys)
}
export function sortPackages (pkgGraph: ProjectsGraph): string[][] {

View File

@@ -9,9 +9,12 @@
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../deps/graph-sequencer"
},
{
"path": "../../packages/types"
}
],
"composite": true
}
}