├── .editorconfig ├── .gitignore ├── .nojekyll ├── .travis.yml ├── LICENSE ├── README.md ├── code-of-conduct.md ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src ├── directedAcyclicGraph.ts ├── directedGraph.ts ├── errors.ts ├── graph.ts └── index.ts ├── test ├── directedAcyclicGraph.test.ts ├── directedGraph.test.ts ├── graph.test.ts └── readme.test.ts ├── tools ├── gh-pages-publish.ts └── semantic-release-prepare.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SegFaultx64/typescript-graph/183c9dacecb44bb500af0e4b5399b63c02da5624/.nojekyll -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '11' 10 | - '8' 11 | - '6' 12 | script: 13 | - npm run test:prod && npm run build 14 | after_success: 15 | - npm run travis-deploy-once "npm run report-coverage" 16 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run deploy-docs"; fi 17 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run semantic-release"; fi 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Max Walker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-graph 2 | 3 | This library provides some basic graph data structures and algorithms. 4 | 5 | Full docs are availible [here](https://segfaultx64.github.io/typescript-graph/). 6 | 7 | It supports undirected graphs, directed graphs and directed acyclic graphs (including inforcing acyclicality). It does not support weighted graphs at this time. 8 | 9 | The graphs created with this library can have nodes of any type, however at present it does not support attaching metadata to edges. 10 | 11 | The most useful functionality is the ability to compute cyclicality and topological sort order on [[`DirectedAcyclicGraph`]]s. This can be used for determining things like task order or tracing dependencies. 12 | 13 | ## Installing & Basic Usage 14 | 15 | ```bash 16 | npm install 'typescript-graph' 17 | ``` 18 | 19 | ```typescript 20 | import { Graph } from 'typescript-graph' 21 | 22 | // Identify the node type to be used with the graph 23 | type NodeType = { name: string, count: number, metadata: { [string: string]: string } } 24 | // Define a custom identity function with which to identify nodes 25 | const graph = new Graph((n: NodeType) => n.name) 26 | 27 | // Insert nodes into the graph 28 | const node1 = graph.insert({name: 'node1', count: 45, metadata: {color: 'green'}}) 29 | const node2 = graph.insert({name: 'node2', count: 5, metadata: {color: 'red', style: 'normal'}}) 30 | const node3 = graph.insert({name: 'node3', count: 15, metadata: {color: 'blue', size: 'large'}}) 31 | 32 | // Add edges between the nodes we created. 33 | graph.addEdge(node1, node2) 34 | graph.addEdge(node2, node3) 35 | 36 | // Get a node 37 | const node: NodeType = graph.getNode(node2); 38 | 39 | ``` 40 | 41 | ## Examples 42 | 43 | ### Creating a directed graph and detecting cycles. 44 | 45 | ```typescript 46 | import { DirectedGraph, DirectedAcyclicGraph } from 'typescript-graph' 47 | 48 | // Create the graph 49 | type NodeType = { name: string, count: number } 50 | const graph = new DirectedGraph((n: NodeType) => n.name) 51 | 52 | // Insert nodes into the graph 53 | const node1 = graph.insert({name: 'node1', count: 45}) 54 | const node2 = graph.insert({name: 'node2', count: 5}) 55 | const node3 = graph.insert({name: 'node3', count: 15}) 56 | 57 | // Check for cycles 58 | console.log(graph.isAcyclic()) // true 59 | 60 | // Add edges between the nodes we created. 61 | graph.addEdge(node1, node2) 62 | graph.addEdge(node2, node3) 63 | 64 | // Check for cycles again 65 | console.log(graph.isAcyclic()) // still true 66 | 67 | // Converts the graph into one that enforces acyclicality 68 | const dag = DirectedAcyclicGraph.fromDirectedGraph(graph) 69 | 70 | // Try to add an edge that will cause an cycle 71 | dag.addEdge(node3, node1) // throws an exception 72 | 73 | // You can add the edge that would cause a cycle on the preview graph 74 | graph.addEdge(node3, node1) 75 | 76 | // Check for cycles again 77 | console.log(graph.isAcyclic()) // now false 78 | 79 | DirectedAcyclicGraph.fromDirectedGraph(graph) // now throws an exception because graph is not acyclic 80 | ``` 81 | 82 | ### Creating a directed acyclic graph and getting the nodes in topological order 83 | 84 | ```typescript 85 | import { DirectedAcyclicGraph } from 'typescript-graph' 86 | 87 | // Create the graph 88 | type NodeType = { name: string } 89 | const graph = new DirectedAcyclicGraph((n: NodeType) => n.name) 90 | 91 | // Insert nodes into the graph 92 | const node1 = graph.insert({name: 'node1'}) 93 | const node2 = graph.insert({name: 'node2'}) 94 | const node3 = graph.insert({name: 'node3'}) 95 | const node4 = graph.insert({name: 'node4'}) 96 | const node5 = graph.insert({name: 'node5'}) 97 | 98 | // Add edges 99 | graph.addEdge(node1, node2) 100 | graph.addEdge(node2, node4) 101 | graph.addEdge(node1, node3) 102 | graph.addEdge(node3, node5) 103 | graph.addEdge(node5, node4) 104 | 105 | // Get the nodes in topologically sorted order 106 | graph.topologicallySortedNodes() // returns roughly [{ name: 'node1' }, { name: 'node3' }, { name: 'node5' }, { name: 'node2' }, { name: 'node4' }] 107 | ``` 108 | 109 | ## License 110 | MIT License 111 | 112 | ## Author 113 | Max Walker (max@maxwalker.me) 114 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexjovermorales@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-graph", 3 | "homepage": "https://segfaultx64.github.io/typescript-graph/", 4 | "version": "0.3.0", 5 | "description": "", 6 | "keywords": [], 7 | "main": "dist/typescript-graph.umd.js", 8 | "module": "dist/typescript-graph.es5.js", 9 | "typings": "dist/types/index.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "author": "Max Walker ", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/SegFaultx64/typescript-graph" 17 | }, 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=6.0.0" 21 | }, 22 | "scripts": { 23 | "lint": "tslint --projsect tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts' --fix", 24 | "prebuild": "rimraf dist", 25 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src", 26 | "start": "rollup -c rollup.config.ts -w", 27 | "test": "jest --coverage", 28 | "test:watch": "jest --coverage --watch", 29 | "test:prod": "npm run lint && npm run test -- --no-cache", 30 | "deploy-docs": "ts-node tools/gh-pages-publish", 31 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 32 | "commit": "git-cz", 33 | "semantic-release": "semantic-release", 34 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 35 | "precommit": "lint-staged", 36 | "travis-deploy-once": "travis-deploy-once" 37 | }, 38 | "lint-staged": { 39 | "{src,test}/**/*.ts": [ 40 | "prettier --write", 41 | "git add" 42 | ] 43 | }, 44 | "config": { 45 | "commitizen": { 46 | "path": "node_modules/cz-conventional-changelog" 47 | } 48 | }, 49 | "jest": { 50 | "transform": { 51 | ".(ts|tsx)": "ts-jest" 52 | }, 53 | "testEnvironment": "node", 54 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 55 | "moduleFileExtensions": [ 56 | "ts", 57 | "tsx", 58 | "js" 59 | ], 60 | "coveragePathIgnorePatterns": [ 61 | "/node_modules/", 62 | "/test/" 63 | ], 64 | "coverageThreshold": { 65 | "global": { 66 | "branches": 80, 67 | "functions": 85, 68 | "lines": 85, 69 | "statements": 85 70 | } 71 | }, 72 | "collectCoverageFrom": [ 73 | "src/*.{js,ts}" 74 | ] 75 | }, 76 | "prettier": { 77 | "semi": false, 78 | "singleQuote": true 79 | }, 80 | "commitlint": { 81 | "extends": [ 82 | "@commitlint/config-conventional" 83 | ] 84 | }, 85 | "devDependencies": { 86 | "@commitlint/cli": "^7.1.2", 87 | "@commitlint/config-conventional": "^7.1.2", 88 | "@types/jest": "^23.3.2", 89 | "@types/node": "^10.11.0", 90 | "@types/object-hash": "^1.3.4", 91 | "colors": "^1.3.2", 92 | "commitizen": "^3.0.0", 93 | "coveralls": "^3.0.2", 94 | "cross-env": "^5.2.0", 95 | "cz-conventional-changelog": "^2.1.0", 96 | "husky": "^1.0.1", 97 | "jest": "^23.6.0", 98 | "jest-config": "^23.6.0", 99 | "lint-staged": "^8.0.0", 100 | "lodash.camelcase": "^4.3.0", 101 | "prettier": "^1.14.3", 102 | "prompt": "^1.0.0", 103 | "replace-in-file": "^3.4.2", 104 | "rimraf": "^2.6.2", 105 | "rollup": "^0.67.0", 106 | "rollup-plugin-commonjs": "^9.1.8", 107 | "rollup-plugin-json": "^3.1.0", 108 | "rollup-plugin-node-resolve": "^3.4.0", 109 | "rollup-plugin-sourcemaps": "^0.4.2", 110 | "rollup-plugin-typescript2": "^0.29.0", 111 | "semantic-release": "^15.9.16", 112 | "shelljs": "^0.8.3", 113 | "travis-deploy-once": "^5.0.9", 114 | "ts-jest": "^23.10.2", 115 | "ts-node": "^7.0.1", 116 | "tslint": "^5.11.0", 117 | "tslint-config-prettier": "^1.15.0", 118 | "tslint-config-standard": "^8.0.1", 119 | "typedoc": "^0.19.2", 120 | "typescript": "^3.0.3" 121 | }, 122 | "dependencies": { 123 | "object-hash": "^2.0.3" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import sourceMaps from 'rollup-plugin-sourcemaps' 4 | import camelCase from 'lodash.camelcase' 5 | import typescript from 'rollup-plugin-typescript2' 6 | import json from 'rollup-plugin-json' 7 | 8 | const pkg = require('./package.json') 9 | 10 | const libraryName = 'typescript-graph' 11 | 12 | export default { 13 | input: `src/index.ts`, 14 | output: [ 15 | { file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true }, 16 | { file: pkg.module, format: 'es', sourcemap: true }, 17 | ], 18 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 19 | external: [], 20 | watch: { 21 | include: 'src/**', 22 | }, 23 | plugins: [ 24 | // Allow json resolution 25 | json(), 26 | // Compile TypeScript files 27 | typescript({ useTsconfigDeclarationDir: true }), 28 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 29 | commonjs(), 30 | // Allow node_modules resolution, so you can use 'external' to control 31 | // which external modules to include in the bundle 32 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 33 | resolve(), 34 | 35 | // Resolve source maps to the original source 36 | sourceMaps(), 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /src/directedAcyclicGraph.ts: -------------------------------------------------------------------------------- 1 | import DirectedGraph from "./directedGraph"; 2 | import { CycleError } from "./errors"; 3 | 4 | /** 5 | * # DirectedAcyclicGraph 6 | * 7 | * A DirectedAcyclicGraph is builds on a [[`DirectedGraph`]] but enforces acyclicality. You cannot add an edge to a DirectedAcyclicGraph that would create a cycle. 8 | * 9 | * @typeParam T `T` is the node type of the graph. Nodes can be anything in all the included examples they are simple objects. 10 | */ 11 | export default class DirectedAcyclicGraph extends DirectedGraph { 12 | private _topologicallySortedNodes?: Array; 13 | protected hasCycle = false; 14 | 15 | /** 16 | * Converts an existing directed graph into a directed acyclic graph. 17 | * Throws a {@linkcode CycleError} if the graph attempting to be converted contains a cycle. 18 | * @param graph The source directed graph to convert into a DAG 19 | */ 20 | static fromDirectedGraph(graph: DirectedGraph): DirectedAcyclicGraph { 21 | if (!graph.isAcyclic()) { 22 | throw new CycleError("Can't convert that graph to a DAG because it contains a cycle") 23 | } 24 | const toRet = new DirectedAcyclicGraph(); 25 | 26 | toRet.nodes = (graph as any).nodes 27 | toRet.adjacency = (graph as any).adjacency 28 | 29 | return toRet; 30 | } 31 | 32 | /** 33 | * Adds an edge to the graph similarly to [[`DirectedGraph.addEdge`]] but maintains correctness of the acyclic graph. 34 | * Thows a [[`CycleError`]] if adding the requested edge would create a cycle. 35 | * Adding an edge invalidates the cache of topologically sorted nodes, rather than updating it. 36 | * 37 | * @param fromNodeIdentity The identity string of the node the edge should run from. 38 | * @param toNodeIdentity The identity string of the node the edge should run to. 39 | */ 40 | addEdge(fromNodeIdentity: string, toNodeIdentity: string) { 41 | if (this.wouldAddingEdgeCreateCyle(fromNodeIdentity, toNodeIdentity)) { 42 | throw new CycleError(`Can't add edge from ${fromNodeIdentity} to ${toNodeIdentity} it would create a cycle`) 43 | } 44 | 45 | // Invalidate cache of toposorted nodes 46 | this._topologicallySortedNodes = undefined; 47 | super.addEdge(fromNodeIdentity, toNodeIdentity, true) 48 | } 49 | 50 | /** 51 | * Inserts a node into the graph and maintains topologic sort cache by prepending the node 52 | * (since all newly created nodes have an [[ indegreeOfNode | indegree ]] of zero.) 53 | * 54 | * @param node The node to insert 55 | */ 56 | insert(node: T): string { 57 | if (this._topologicallySortedNodes) { 58 | this._topologicallySortedNodes = [node, ...this._topologicallySortedNodes]; 59 | } 60 | 61 | return super.insert(node) 62 | } 63 | 64 | /** 65 | * Topologically sort the nodes using Kahn's algorithim. Uses a cache which means that repeated calls should be O(1) after the first call. 66 | * Non-cached calls are potentially expensive, Kahn's algorithim is O(|EdgeCount| + |NodeCount|). 67 | * There may be more than one valid topological sort order for a single graph, 68 | * so just because two graphs are the same does not mean that order of the resultant arrays will be. 69 | * 70 | * @returns An array of nodes sorted by the topological order. 71 | */ 72 | topologicallySortedNodes(): Array { 73 | if (this._topologicallySortedNodes !== undefined) { 74 | return this._topologicallySortedNodes; 75 | } 76 | 77 | const nodeIndices = Array.from(this.nodes.keys()); 78 | const nodeInDegrees = new Map(Array.from(this.nodes.keys()).map(n => [n, this.indegreeOfNode(n)])) 79 | 80 | const adjCopy = this.adjacency.map(a => [...a]) 81 | 82 | let toSearch = Array.from(nodeInDegrees).filter(pair => pair[1] === 0) 83 | 84 | if (toSearch.length === this.nodes.size) { 85 | const arrayOfNodes = Array.from(this.nodes.values()); 86 | this._topologicallySortedNodes = arrayOfNodes 87 | return arrayOfNodes 88 | } 89 | 90 | let toReturn: Array = [] 91 | 92 | while (toSearch.length) { 93 | const n = (toSearch.pop() as [string, number]); 94 | const curNode = (this.nodes.get(n[0]) as T); 95 | toReturn.push(curNode); 96 | 97 | (adjCopy[nodeIndices.indexOf(n[0])])?.forEach((edge, index) => { 98 | if (edge > 0) { 99 | adjCopy[nodeIndices.indexOf(n[0])][index] = 0; 100 | const target = (nodeInDegrees.get(nodeIndices[index]) as number); 101 | nodeInDegrees.set(nodeIndices[index], target - 1) 102 | 103 | if ((target - 1) === 0) { 104 | toSearch.push([nodeIndices[index], 0]) 105 | } 106 | } 107 | }) 108 | } 109 | 110 | // Update cache 111 | this._topologicallySortedNodes = toReturn; 112 | 113 | // we shouldn't need to account for the error case of there being a cycle because it shouldn't 114 | // be possible to instantiate this class in a state (or put it in a state) where there is a cycle. 115 | 116 | return toReturn 117 | } 118 | 119 | /** 120 | * Given a starting node this returns a new [[`DirectedA`]] containing all the nodes that can be reached. 121 | * Throws a [[`NodeDoesntExistError`]] if the start node does not exist. 122 | * 123 | * @param startNodeIdentity The string identity of the node from which the subgraph search should start. 124 | */ 125 | getSubGraphStartingFrom(startNodeIdentity: string): DirectedAcyclicGraph { 126 | return DirectedAcyclicGraph.fromDirectedGraph(super.getSubGraphStartingFrom(startNodeIdentity)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/directedGraph.ts: -------------------------------------------------------------------------------- 1 | import { NodeDoesntExistError } from "./errors"; 2 | import Graph, { Edge } from "./graph"; 3 | 4 | /** 5 | * # DirectedGraph 6 | * 7 | * A DirectedGraph is similar a [[`Graph`]] but with additional functionality. 8 | * 9 | * @typeParam T `T` is the node type of the graph. Nodes can be anything in all the included examples they are simple objects. 10 | */ 11 | export default class DirectedGraph extends Graph { 12 | /** Caches if the graph contains a cycle. If `undefined` then it is unknown. */ 13 | protected hasCycle?: boolean; 14 | 15 | /** 16 | * Returns `true` if there are no cycles in the graph. 17 | * This relies on a cached value so calling it multiple times without adding edges to the graph should be O(1) after the first call. 18 | * Non-cached calls are potentially expensive, the implementation is based on Kahn's algorithim which is O(|EdgeCount| + |NodeCount|). 19 | */ 20 | isAcyclic(): boolean { 21 | if (this.hasCycle !== undefined) { 22 | return !this.hasCycle 23 | } 24 | 25 | const nodeIndices = Array.from(this.nodes.keys()); 26 | const nodeInDegrees = new Map(Array.from(this.nodes.keys()).map(n => [n, this.indegreeOfNode(n)])) 27 | 28 | let toSearch = Array.from(nodeInDegrees).filter(pair => pair[1] === 0) 29 | 30 | let visitedNodes = 0; 31 | 32 | while (toSearch.length > 0) { 33 | const cur = toSearch.pop(); 34 | if (!cur) { 35 | continue; 36 | } 37 | 38 | const nodeIndex = nodeIndices.indexOf(cur[0]); 39 | this.adjacency[nodeIndex].forEach((hasAdj, index) => { 40 | if (hasAdj === 1) { 41 | const currentInDegree = nodeInDegrees.get(nodeIndices[index]); 42 | if (currentInDegree !== undefined) { 43 | nodeInDegrees.set(nodeIndices[index], currentInDegree - 1) 44 | if ((currentInDegree - 1) === 0) { 45 | toSearch.push([nodeIndices[index], currentInDegree - 1]) 46 | } 47 | } 48 | } 49 | }) 50 | 51 | visitedNodes++; 52 | } 53 | 54 | this.hasCycle = !(visitedNodes === this.nodes.size) 55 | 56 | return visitedNodes === this.nodes.size; 57 | } 58 | 59 | /** 60 | * The indegree of a node is the number of edges that point to it. This will always be an integer. 61 | * 62 | * Throws a [[`NodeDoesntExistError`]] the node does not exist. 63 | * 64 | * @param nodeID The string of the node identity of the node to calculate indegree for. 65 | */ 66 | indegreeOfNode(nodeID: string): number { 67 | const nodeIdentities = Array.from(this.nodes.keys()); 68 | const indexOfNode = nodeIdentities.indexOf(nodeID); 69 | 70 | if (indexOfNode === -1) { 71 | throw new NodeDoesntExistError(nodeID); 72 | } 73 | 74 | return this.adjacency.reduce((carry, row) => { 75 | return carry + ((row[indexOfNode] > 0)? 1 : 0); 76 | }, 0) 77 | } 78 | 79 | /** 80 | * Add a directed edge to the graph. 81 | * 82 | * @param fromNodeIdentity The identity string of the node the edge should run from. 83 | * @param toNodeIdentity The identity string of the node the edge should run to. 84 | * @param skipUpdatingCyclicality This boolean indicates if the cache of the cyclicality of the graph should be updated. 85 | * If `false` is passed the cached will be invalidated because we can not assure that a cycle has not been created. 86 | */ 87 | addEdge(fromNodeIdentity: string, toNodeIdentity: string, skipUpdatingCyclicality: boolean = false) { 88 | if (!this.hasCycle && !skipUpdatingCyclicality) { 89 | this.hasCycle = this.wouldAddingEdgeCreateCyle(fromNodeIdentity, toNodeIdentity); 90 | } else if (skipUpdatingCyclicality) { 91 | this.hasCycle = undefined; 92 | } 93 | 94 | super.addEdge(fromNodeIdentity, toNodeIdentity) 95 | } 96 | 97 | /** 98 | * Depth first search to see if one node is reachable from another following the directed edges. 99 | * 100 | * __Caveat:__ This will return false if `startNode` and `endNode` are the same node and the is not a cycle or a loop edge connecting them. 101 | * 102 | * @param startNode The string identity of the node to start at. 103 | * @param endNode The string identity of the node we are attempting to reach. 104 | */ 105 | canReachFrom(startNode: string, endNode: string): boolean { 106 | const nodeIdentities = Array.from(this.nodes.keys()); 107 | const startNodeIndex = nodeIdentities.indexOf(startNode); 108 | const endNodeIndex = nodeIdentities.indexOf(endNode); 109 | 110 | if (this.adjacency[startNodeIndex][endNodeIndex] > 0) { 111 | return true 112 | } 113 | 114 | return this.adjacency[startNodeIndex].reduce((carry, edge, index) => { 115 | if (carry || (edge < 1)) { 116 | return carry; 117 | } 118 | 119 | return this.canReachFrom(nodeIdentities[index], endNode) 120 | }, false) 121 | } 122 | 123 | /** 124 | * Checks if adding the specified edge would create a cycle. 125 | * Returns true in O(1) if the graph already contains a known cycle, or if `fromNodeIdentity` and `toNodeIdentity` are the same. 126 | * 127 | * @param fromNodeIdentity The string identity of the node the edge is from. 128 | * @param toNodeIdentity The string identity of the node the edge is to. 129 | */ 130 | wouldAddingEdgeCreateCyle(fromNodeIdentity: string, toNodeIdentity: string): boolean { 131 | return this.hasCycle || fromNodeIdentity === toNodeIdentity || this.canReachFrom(toNodeIdentity, fromNodeIdentity); 132 | } 133 | 134 | /** 135 | * Given a starting node this returns a new [[`DirectedGraph`]] containing all the nodes that can be reached. 136 | * Throws a [[`NodeDoesntExistError`]] if the start node does not exist. 137 | * 138 | * @param startNodeIdentity The string identity of the node from which the subgraph search should start. 139 | */ 140 | getSubGraphStartingFrom(startNodeIdentity: string): DirectedGraph { 141 | const nodeIndices = Array.from(this.nodes.keys()); 142 | const initalNode = this.nodes.get(startNodeIdentity) 143 | 144 | if (!initalNode) { 145 | throw new NodeDoesntExistError(startNodeIdentity); 146 | } 147 | 148 | const recur = (startNodeIdentity: string, nodesToInclude: T[]): T[] => { 149 | let toReturn = [...nodesToInclude]; 150 | const nodeIndex = nodeIndices.indexOf(startNodeIdentity); 151 | this.adjacency[nodeIndex].forEach((hasAdj, index) => { 152 | if (hasAdj === 1 && !nodesToInclude.find(n => this.nodeIdentity(n) === nodeIndices[index])) { 153 | const newNode = this.nodes.get(nodeIndices[index]) 154 | 155 | if (newNode) { 156 | toReturn = [...recur(nodeIndices[index], toReturn), newNode] 157 | } 158 | } 159 | }) 160 | 161 | return toReturn; 162 | } 163 | 164 | const newGraph = new DirectedGraph(this.nodeIdentity); 165 | const nodeList = recur(startNodeIdentity, [initalNode]) 166 | const includeIdents = nodeList.map(t => this.nodeIdentity(t)); 167 | Array.from(this.nodes.values()).forEach(n => { 168 | if (includeIdents.includes(this.nodeIdentity(n))) { 169 | newGraph.insert(n) 170 | } 171 | }); 172 | newGraph.adjacency = this.subAdj(nodeList); 173 | return newGraph 174 | } 175 | 176 | private subAdj(include: T[]): Array> { 177 | const includeIdents = include.map(t => this.nodeIdentity(t)); 178 | const nodeIndices = Array.from(this.nodes.keys()); 179 | 180 | return this.adjacency.reduce>>((carry, cur, index) => { 181 | if (includeIdents.includes(nodeIndices[index])) { 182 | return [...carry, cur.filter((_, index) => includeIdents.includes(nodeIndices[index]))] 183 | } else { 184 | return carry 185 | } 186 | }, []) 187 | } 188 | } -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * # NodeAlreadyExistsError 4 | * 5 | * This error is thrown when trying to create a node with the same identity as an existing node. 6 | * 7 | * @category Errors 8 | */ 9 | 10 | export class NodeAlreadyExistsError extends Error { 11 | public newNode: T; 12 | public oldNode: T; 13 | public identity: string; 14 | 15 | constructor(newNode: T, oldNode: T, identity: string) { 16 | super(`${JSON.stringify(newNode)} shares an identity (${identity}) with ${JSON.stringify(oldNode)}`); 17 | this.newNode = newNode; 18 | this.oldNode = oldNode; 19 | this.identity = identity; 20 | this.name = "NodeAlreadyExistsError"; 21 | 22 | // This bs is due to a limitation of Typescript: https://github.com/facebook/jest/issues/8279 23 | Object.setPrototypeOf(this, NodeAlreadyExistsError.prototype); 24 | } 25 | } 26 | 27 | /** 28 | * # NodeDoesntExistError 29 | * This error is thrown when trying to access a node in a graph by it's identity when that node doesn't exist 30 | * 31 | * @category Errors 32 | */ 33 | export class NodeDoesntExistError extends Error { 34 | public identity: string; 35 | 36 | constructor(identity: string) { 37 | super(`A node with identity ${identity} doesn't exist in the graph`); 38 | this.identity = identity; 39 | this.name = "NodeDoesntExistError"; 40 | 41 | // This bs is due to a limitation of Typescript: https://github.com/facebook/jest/issues/8279 42 | Object.setPrototypeOf(this, NodeDoesntExistError.prototype); 43 | } 44 | } 45 | 46 | /** 47 | * # CycleError 48 | * 49 | * This error is thrown when attempting to create or update a Directed Acyclic Graph that contains a cycle. 50 | * 51 | * @category Errors 52 | */ 53 | export class CycleError extends Error { 54 | constructor(message: string) { 55 | super(message); 56 | this.name = "CycleError"; 57 | 58 | // This bs is due to a limitation of Typescript: https://github.com/facebook/jest/issues/8279 59 | Object.setPrototypeOf(this, CycleError.prototype); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/graph.ts: -------------------------------------------------------------------------------- 1 | import { NodeAlreadyExistsError, NodeDoesntExistError } from './errors' 2 | 3 | /** 4 | * This is the default [[Graph.constructor | `nodeIdentity`]] function it is simply imported from [object-hash](https://www.npmjs.com/package/object-hash) 5 | */ 6 | const hash = require('object-hash') 7 | 8 | /** 9 | * @internal 10 | * This type is simply an indicator of whether an edge exists in the adjacency matrix. 11 | */ 12 | export type Edge = 1 | 0 13 | 14 | /** 15 | * # Graph 16 | * 17 | * A `Graph` is is a simple undirected graph. On it's own it isn't too useful but it forms the basic functionality for the [[`DirectedGraph`]] and [[`DirectedAcyclicGraph`]]. 18 | * 19 | * ## Creating a Graph 20 | * 21 | * You can create a graph to contain any type of node, for example: 22 | * 23 | * ```typescript 24 | * type NodeType = { a: Number, b: string } 25 | * const graph = new Graph() 26 | * 27 | * // Add a node of the defined type 28 | * const node: string = graph.insert({ a: 10, b: 'string' }) 29 | * 30 | * // Get the node back 31 | * const nodeValue: NodeType | undefined = graph.getNode(node); 32 | * ``` 33 | * 34 | * ### Defining a custom node identity 35 | * 36 | * When you create a graph you likely want to create include a custom `nodeIdentity` function. 37 | * This function tells the graph how to uniquely identify nodes in a graph, 38 | * default is to simply use an [[hash]] which means that functionality like [[`replace`]] will not work. 39 | * 40 | * ```typescript 41 | * type NodeType = { count: number, name: string } 42 | * const graph = new Graph((n) => n.name) 43 | * 44 | * // Add a node 45 | * graph.insert({ count: 5, name: 'node1' }) 46 | * // This will throw an error even though `count` is different because they share a name. 47 | * graph.insert({ count: 20, name: 'node1' }) 48 | * ``` 49 | * 50 | * ### Adding an edge 51 | * 52 | * Graphs without edges aren't very useful. Inserting edges is done using the node identity string returned by the node identity function. 53 | * 54 | * ```typescript 55 | * const node1: string = graph.insert({ count: 5, name: 'node1' }) 56 | * const node2: string = graph.insert({ count: 20, name: 'node2' }) 57 | * 58 | * graph.addEdge(node1, node2) 59 | * 60 | * // This will throw an error since there is no node with the later name. 61 | * graph.addEdge(node1, 'not a real node') 62 | * ``` 63 | * 64 | * In an undirected graph the order in which you input the node names doesn't matter, 65 | * but in directed graphs the "from node" comes first and the "to node" will come second. 66 | * 67 | * ### Replacing a node 68 | * 69 | * If a node already exists you can update it using [[`replace`]]. `nodeIdentity(newNode)` must be equal to `nodeIdentity(oldNode)`. 70 | * 71 | * ```typescript 72 | * const node1: string = graph.insert({ count: 5, name: 'node1' }) 73 | * const node2: string = graph.insert({ count: 20, name: 'node2' }) 74 | * 75 | * // This will work because the name has not changed. 76 | * graph.replace({ count: 15, name: 'node1' }) 77 | * 78 | * // This will not work because the name has changed. 79 | * graph.replace({ count: 20, name: 'node3' }) 80 | * ``` 81 | * 82 | * [[`replace`]] will throw a [[`NodeDoesntExistError`]] exception if you are trying to replace a node that is missing from the graph. 83 | * 84 | * ### Upsert 85 | * 86 | * Often you will want to create a node node if it doesn't exist and update it does. This can be achieved using [[`upsert`]]. 87 | * 88 | * ```typescript 89 | * const node1: string = graph.insert({ count: 5, name: 'node1' }) 90 | * 91 | * // Both of these will work, the first updating node1 and the second creating a node. 92 | * const node2: string = graph.upsert({ count: 15, name: 'node1' }) 93 | * const node3: string = graph.upsert({ count: 25, name: 'node3' }) 94 | * ``` 95 | * 96 | * [[`upsert`]] always return the node identity string of the inserted or updated node. At presented there is no way to tell if the node was created or updated. 97 | * 98 | * @typeParam T `T` is the node type of the graph. Nodes can be anything in all the included examples they are simple objects. 99 | */ 100 | export default class Graph { 101 | protected nodes: Map 102 | protected adjacency: Array> 103 | protected nodeIdentity: (t: T) => string 104 | 105 | constructor(nodeIdentity: (node: T) => string = node => hash(node)) { 106 | this.nodes = new Map() 107 | this.adjacency = [] 108 | this.nodeIdentity = nodeIdentity 109 | } 110 | 111 | /** 112 | * Add a node to the graph if it doesn't already exist. If it does, throw a [[`NodeAlreadyExistsError`]]. 113 | * 114 | * @param node The node to be added 115 | * @returns A `string` that is the identity of the newly inserted node. This is created by applying the [[constructor | `nodeIdentity`]]. 116 | */ 117 | insert(node: T): string { 118 | const isOverwrite = this.nodes.has(this.nodeIdentity(node)) 119 | 120 | if (isOverwrite) { 121 | throw new NodeAlreadyExistsError( 122 | node, 123 | this.nodes.get(this.nodeIdentity(node)), 124 | this.nodeIdentity(node) 125 | ) 126 | } 127 | 128 | this.nodes.set(this.nodeIdentity(node), node) 129 | this.adjacency.map(adj => adj.push(0)) 130 | this.adjacency.push(new Array(this.adjacency.length + 1).fill(0)) 131 | 132 | return this.nodeIdentity(node) 133 | } 134 | 135 | /** 136 | * This replaces an existing node in the graph with an updated version. 137 | * Throws a [[`NodeDoesNotExistsError`]] if no node with the same identity already exists. 138 | * 139 | * __Caveat_:_ The default identity function means that this will never work since if the node changes it will have a different [[`hash`]]. 140 | * 141 | * @param node The new node that is replacing the old one. 142 | */ 143 | replace(node: T) { 144 | const isOverwrite = this.nodes.has(this.nodeIdentity(node)) 145 | 146 | if (!isOverwrite) { 147 | throw new NodeDoesntExistError(this.nodeIdentity(node)) 148 | } 149 | 150 | this.nodes.set(this.nodeIdentity(node), node) 151 | } 152 | 153 | /** 154 | * This essentially combines the behavior of [[`insert`]] and [[`replace`]]. 155 | * If the node doesn't exist, create it. If the node already exists, replace it with the updated version. 156 | * 157 | * @param node The node to insert or update 158 | * @returns The identity string of the node inserted or updated. 159 | */ 160 | upsert(node: T): string { 161 | const isOverwrite = this.nodes.has(this.nodeIdentity(node)) 162 | 163 | this.nodes.set(this.nodeIdentity(node), node) 164 | 165 | if (!isOverwrite) { 166 | this.adjacency.map(adj => adj.push(0)) 167 | this.adjacency.push(new Array(this.adjacency.length + 1)) 168 | } 169 | 170 | return this.nodeIdentity(node) 171 | } 172 | 173 | /** 174 | * Create an edge between two nodes in the graph. 175 | * Throws a [[`NodeDoesNotExistsError`]] if no either of the nodes you are attempting to connect do not exist. 176 | * 177 | * @param node1Identity The first node to connect (in [[`DirectedGraph`]]s and [[`DirectedAcyclicGraph`]]s this is the `from` node.) 178 | * @param node2Identity The second node to connect (in [[`DirectedGraph`]]s and [[`DirectedAcyclicGraph`]]s this is the `to` node) 179 | */ 180 | addEdge(node1Identity: string, node2Identity: string) { 181 | const node1Exists = this.nodes.has(node1Identity) 182 | const node2Exists = this.nodes.has(node2Identity) 183 | 184 | if (!node1Exists) { 185 | throw new NodeDoesntExistError(node1Identity) 186 | } 187 | 188 | if (!node2Exists) { 189 | throw new NodeDoesntExistError(node2Identity) 190 | } 191 | 192 | const node1Index = Array.from(this.nodes.keys()).indexOf(node1Identity) 193 | const node2Index = Array.from(this.nodes.keys()).indexOf(node2Identity) 194 | 195 | this.adjacency[node1Index][node2Index] = 1 196 | } 197 | 198 | /** 199 | * This simply returns all the nodes stored in the graph 200 | * 201 | * @param compareFunc An optional function that indicates the sort order of the returned array 202 | */ 203 | getNodes(compareFunc?: (a: T, b: T) => number): T[] { 204 | const temp = Array.from(this.nodes.values()) 205 | 206 | if (compareFunc) { 207 | return temp.sort(compareFunc) 208 | } 209 | 210 | return temp 211 | } 212 | 213 | /** 214 | * Returns a specific node given the node identity returned from the [[`insert`]] function 215 | * 216 | * @param compareFunc An optional function that indicates the sort order of the returned array 217 | */ 218 | getNode(nodeIdentity: string): T | undefined { 219 | return this.nodes.get(nodeIdentity) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DirectedAcyclicGraph from "./directedAcyclicGraph" 2 | import DirectedGraph from "./directedGraph" 3 | import Graph from "./graph" 4 | 5 | export { Graph, DirectedGraph, DirectedAcyclicGraph }; 6 | -------------------------------------------------------------------------------- /test/directedAcyclicGraph.test.ts: -------------------------------------------------------------------------------- 1 | import { DirectedGraph, DirectedAcyclicGraph } from '../src/' 2 | import { CycleError } from '../src/errors' 3 | 4 | /*** 5 | * Directed Acyclic Graph test 6 | */ 7 | 8 | describe("Directed Acyclic Graph", () => { 9 | it("can be instantiated", () => { 10 | expect(new DirectedAcyclicGraph<{}>()).toBeInstanceOf(DirectedAcyclicGraph) 11 | }) 12 | 13 | it("can be converted from a directed graph", () => { 14 | type NodeType = { name: string } 15 | const graph = new DirectedGraph((n: NodeType) => n.name) 16 | 17 | graph.insert({ name: 'A' }) 18 | graph.insert({ name: 'B' }) 19 | graph.insert({ name: 'C' }) 20 | 21 | graph.addEdge('A', 'B') 22 | graph.addEdge('B', 'C') 23 | graph.addEdge('A', 'C') 24 | 25 | expect(DirectedAcyclicGraph.fromDirectedGraph(graph)).toBeInstanceOf(DirectedAcyclicGraph) 26 | 27 | graph.addEdge('C', 'A') 28 | 29 | expect(() => DirectedAcyclicGraph.fromDirectedGraph(graph)).toThrow(CycleError) 30 | }) 31 | 32 | 33 | it("can add an edge only if it wouldn't create a cycle", () => { 34 | type NodeType = { name: string } 35 | const graph = new DirectedAcyclicGraph((n: NodeType) => n.name) 36 | 37 | graph.insert({ name: 'A' }) 38 | graph.insert({ name: 'B' }) 39 | graph.insert({ name: 'C' }) 40 | 41 | graph.addEdge('A', 'B') 42 | graph.addEdge('B', 'C') 43 | graph.addEdge('A', 'C') 44 | 45 | expect(() => graph.addEdge('C', 'A')).toThrow(CycleError) 46 | }) 47 | 48 | it("can get it's nodes topologically sorted", () => { 49 | type NodeType = { name: string } 50 | const graph = new DirectedAcyclicGraph((n: NodeType) => n.name) 51 | 52 | expect(graph.topologicallySortedNodes()).toEqual([]) 53 | 54 | graph.insert({ name: 'A' }) 55 | graph.insert({ name: 'B' }) 56 | graph.insert({ name: 'C' }) 57 | 58 | const topoList1 = graph.topologicallySortedNodes(); 59 | 60 | expect(topoList1).toContainEqual({ name: 'A' }) 61 | expect(topoList1).toContainEqual({ name: 'B' }) 62 | expect(topoList1).toContainEqual({ name: 'C' }) 63 | 64 | graph.addEdge('A', 'C') 65 | graph.addEdge('C', 'B') 66 | 67 | const topoList2 = graph.topologicallySortedNodes(); 68 | 69 | expect(topoList2).toEqual([{ name: 'A' }, { name: 'C' }, { name: 'B' }]) 70 | 71 | graph.insert({ name: 'D' }) 72 | graph.insert({ name: 'E' }) 73 | 74 | graph.addEdge('A', 'D') 75 | graph.addEdge('B', 'E') 76 | 77 | const topoList3 = graph.topologicallySortedNodes(); 78 | 79 | expect(topoList3[0]).toEqual({ name: 'A' }) 80 | expect(topoList3[4]).toEqual({ name: 'E' }) 81 | 82 | expect([{ name: 'C' }, { name: 'D' }]).toContainEqual(topoList3[1]) 83 | expect([{ name: 'C' }, { name: 'D' }]).toContainEqual(topoList3[2]) 84 | 85 | graph.insert({ name: 'F' }) 86 | 87 | const topoList4 = graph.topologicallySortedNodes(); 88 | 89 | expect(topoList4).toContainEqual({ name: 'F' }) 90 | expect([{ name: 'A' }, { name: 'F' }]).toContainEqual(topoList4[0]) 91 | expect([{ name: 'A' }, { name: 'F' }]).toContainEqual(topoList4[1]) 92 | }) 93 | 94 | it("can return a subgraph based on walking from a start node", () => { 95 | type NodeType = { name: string } 96 | const graph = new DirectedAcyclicGraph((n: NodeType) => n.name) 97 | 98 | graph.insert({ name: 'A' }) 99 | graph.insert({ name: 'B' }) 100 | graph.insert({ name: 'C' }) 101 | 102 | const testGraph = new DirectedAcyclicGraph((n: NodeType) => n.name); 103 | testGraph.insert({ name: 'A' }) 104 | 105 | expect(graph.getSubGraphStartingFrom('A').getNodes()).toEqual(testGraph.getNodes()) 106 | 107 | graph.addEdge('A', 'B') 108 | graph.addEdge('B', 'C') 109 | 110 | const subGraph = graph.getSubGraphStartingFrom('A'); 111 | 112 | expect(subGraph.getNodes()).toContainEqual({ name: 'A' }) 113 | expect(subGraph.getNodes()).toContainEqual({ name: 'B' }) 114 | expect(subGraph.getNodes()).toContainEqual({ name: 'C' }) 115 | expect(subGraph.canReachFrom('A', 'C')).toBe(true); 116 | 117 | graph.insert({ name: 'D' }) 118 | 119 | const subGraph2 = graph.getSubGraphStartingFrom('A'); 120 | 121 | expect(subGraph2.getNodes()).not.toContainEqual({ name: 'D' }) 122 | 123 | graph.addEdge('B', 'D') 124 | 125 | const subGraph3 = graph.getSubGraphStartingFrom('A'); 126 | 127 | expect(subGraph3.getNodes()).toContainEqual({ name: 'D' }) 128 | expect(subGraph3.canReachFrom('A', 'C')).toBe(true); 129 | expect(subGraph3.canReachFrom('A', 'D')).toBe(true); 130 | expect(subGraph3.canReachFrom('B', 'D')).toBe(true); 131 | expect(subGraph3.canReachFrom('C', 'D')).toBe(false); 132 | 133 | }) 134 | 135 | }) -------------------------------------------------------------------------------- /test/directedGraph.test.ts: -------------------------------------------------------------------------------- 1 | import { DirectedGraph, Graph } from '../src/' 2 | import { NodeDoesntExistError } from '../src/errors' 3 | 4 | /*** 5 | * Directed Graph test 6 | */ 7 | 8 | describe("Directed Graph", () => { 9 | it("can be instantiated", () => { 10 | expect(new DirectedGraph<{}>()).toBeInstanceOf(DirectedGraph) 11 | }) 12 | 13 | it("can calculate the indegree of a node", () => { 14 | type NodeType = { name: string } 15 | const graph = new DirectedGraph((n: NodeType) => n.name) 16 | 17 | graph.insert({ name: 'A' }) 18 | graph.insert({ name: 'B' }) 19 | graph.insert({ name: 'C' }) 20 | 21 | expect(graph.indegreeOfNode('A')).toBe(0) 22 | expect(graph.indegreeOfNode('B')).toBe(0) 23 | expect(graph.indegreeOfNode('C')).toBe(0) 24 | expect(() => graph.indegreeOfNode('D')).toThrowError(NodeDoesntExistError) 25 | 26 | graph.addEdge('A', 'B') 27 | graph.addEdge('B', 'C') 28 | graph.addEdge('A', 'C') 29 | graph.addEdge('C', 'A') 30 | 31 | expect(graph.indegreeOfNode('A')).toBe(1) 32 | expect(graph.indegreeOfNode('B')).toBe(1) 33 | expect(graph.indegreeOfNode('C')).toBe(2) 34 | 35 | 36 | }) 37 | 38 | it("can determine if it is acyclical", () => { 39 | type NodeType = { name: string } 40 | const graph = new DirectedGraph((n: NodeType) => n.name) 41 | 42 | graph.insert({ name: 'A' }) 43 | graph.insert({ name: 'B' }) 44 | graph.insert({ name: 'C' }) 45 | 46 | expect(graph.isAcyclic()).toBe(true) 47 | 48 | graph.addEdge('A', 'B') 49 | 50 | expect(graph.isAcyclic()).toBe(true) 51 | 52 | graph.addEdge('A', 'C') 53 | 54 | expect(graph.isAcyclic()).toBe(true) 55 | 56 | graph.addEdge('C', 'A'); 57 | (graph as any).hasCycle = undefined; 58 | 59 | expect(graph.isAcyclic()).toBe(false) 60 | 61 | const graph2 = new DirectedGraph((n: NodeType) => n.name) 62 | graph2.insert({ name: 'A' }) 63 | 64 | expect(graph2.isAcyclic()).toBe(true) 65 | 66 | graph2.addEdge('A', 'A'); 67 | (graph2 as any).hasCycle = undefined; 68 | 69 | expect(graph2.isAcyclic()).toBe(false) 70 | 71 | const graph3 = new DirectedGraph((n: NodeType) => n.name) 72 | graph3.insert({ name: 'A' }) 73 | graph3.insert({ name: 'B' }) 74 | graph3.insert({ name: 'C' }) 75 | graph3.insert({ name: 'D' }) 76 | graph3.insert({ name: 'E' }) 77 | 78 | expect(graph3.isAcyclic()).toBe(true) 79 | 80 | graph3.addEdge('A', 'B') 81 | 82 | expect(graph3.isAcyclic()).toBe(true) 83 | 84 | graph3.addEdge('B', 'C') 85 | 86 | expect(graph3.isAcyclic()).toBe(true) 87 | 88 | graph3.addEdge('C', 'D') 89 | 90 | expect(graph3.isAcyclic()).toBe(true) 91 | 92 | graph3.addEdge('C', 'E') 93 | 94 | expect(graph3.isAcyclic()).toBe(true) 95 | 96 | graph3.addEdge('E', 'B'); 97 | (graph3 as any).hasCycle = undefined; 98 | 99 | expect(graph3.isAcyclic()).toBe(false) 100 | 101 | graph3.addEdge('E', 'C'); 102 | (graph3 as any).hasCycle = undefined; 103 | 104 | expect(graph3.isAcyclic()).toBe(false) 105 | 106 | graph3.addEdge('E', 'E'); 107 | (graph3 as any).hasCycle = undefined; 108 | 109 | expect(graph3.isAcyclic()).toBe(false) 110 | 111 | }) 112 | 113 | it("can determine if adding an edge would create a cycle", () => { 114 | type NodeType = { name: string } 115 | const graph = new DirectedGraph((n: NodeType) => n.name) 116 | 117 | graph.insert({ name: 'A' }) 118 | graph.insert({ name: 'B' }) 119 | graph.insert({ name: 'C' }) 120 | 121 | expect(graph.wouldAddingEdgeCreateCyle('A', 'B')).toBe(false) 122 | expect(graph.wouldAddingEdgeCreateCyle('A', 'A')).toBe(true) 123 | 124 | graph.addEdge('A', 'B') 125 | 126 | expect(graph.wouldAddingEdgeCreateCyle('B', 'C')).toBe(false) 127 | expect(graph.wouldAddingEdgeCreateCyle('B', 'A')).toBe(true) 128 | 129 | graph.addEdge('B', 'C') 130 | 131 | expect(graph.wouldAddingEdgeCreateCyle('A', 'C')).toBe(false) 132 | expect(graph.wouldAddingEdgeCreateCyle('C', 'A')).toBe(true) 133 | 134 | }) 135 | 136 | 137 | 138 | it("can determine if one node can be reached from another", () => { 139 | type NodeType = { name: string } 140 | const graph = new DirectedGraph((n: NodeType) => n.name) 141 | 142 | graph.insert({ name: 'A' }) 143 | graph.insert({ name: 'B' }) 144 | graph.insert({ name: 'C' }) 145 | graph.insert({ name: 'D' }) 146 | 147 | expect(graph.canReachFrom('A', 'B')).toBe(false) 148 | expect(graph.canReachFrom('A', 'A')).toBe(false) 149 | 150 | graph.addEdge('A', 'B') 151 | 152 | expect(graph.canReachFrom('B', 'C')).toBe(false) 153 | expect(graph.canReachFrom('A', 'B')).toBe(true) 154 | expect(graph.canReachFrom('B', 'A')).toBe(false) 155 | 156 | graph.addEdge('B', 'C') 157 | graph.addEdge('B', 'D') 158 | 159 | expect(graph.canReachFrom('A', 'C')).toBe(true) 160 | expect(graph.canReachFrom('B', 'D')).toBe(true) 161 | expect(graph.canReachFrom('C', 'D')).toBe(false) 162 | 163 | }) 164 | 165 | 166 | it("can return a subgraph based on walking from a start node", () => { 167 | type NodeType = { name: string } 168 | const graph = new DirectedGraph((n: NodeType) => n.name) 169 | 170 | graph.insert({ name: 'A' }) 171 | graph.insert({ name: 'B' }) 172 | graph.insert({ name: 'C' }) 173 | 174 | const testGraph = new DirectedGraph((n: NodeType) => n.name); 175 | testGraph.insert({ name: 'A' }) 176 | 177 | expect(graph.getSubGraphStartingFrom('A').getNodes()).toEqual(testGraph.getNodes()) 178 | 179 | graph.addEdge('A', 'B') 180 | graph.addEdge('B', 'C') 181 | 182 | const subGraph = graph.getSubGraphStartingFrom('A'); 183 | 184 | expect(subGraph.getNodes()).toContainEqual({ name: 'A' }) 185 | expect(subGraph.getNodes()).toContainEqual({ name: 'B' }) 186 | expect(subGraph.getNodes()).toContainEqual({ name: 'C' }) 187 | expect(subGraph.canReachFrom('A', 'C')).toBe(true); 188 | 189 | graph.insert({ name: 'D' }) 190 | 191 | const subGraph2 = graph.getSubGraphStartingFrom('A'); 192 | 193 | expect(subGraph2.getNodes()).not.toContainEqual({ name: 'D' }) 194 | 195 | graph.addEdge('B', 'D') 196 | 197 | const subGraph3 = graph.getSubGraphStartingFrom('A'); 198 | 199 | expect(subGraph3.getNodes()).toContainEqual({ name: 'D' }) 200 | expect(subGraph3.canReachFrom('A', 'C')).toBe(true); 201 | expect(subGraph3.canReachFrom('A', 'D')).toBe(true); 202 | expect(subGraph3.canReachFrom('B', 'D')).toBe(true); 203 | expect(subGraph3.canReachFrom('C', 'D')).toBe(false); 204 | 205 | }) 206 | 207 | }) -------------------------------------------------------------------------------- /test/graph.test.ts: -------------------------------------------------------------------------------- 1 | import { NodeAlreadyExistsError, NodeDoesntExistError } from '../src/errors' 2 | import { Graph } from '../src/' 3 | var hash = require('object-hash') 4 | 5 | /*** 6 | * Graph test 7 | */ 8 | 9 | describe('Graph', () => { 10 | it('can be instantiated', () => { 11 | expect(new Graph<{}>()).toBeInstanceOf(Graph) 12 | }) 13 | 14 | it('can add a node', () => { 15 | const graph = new Graph<{ a: number; b: string }>() 16 | 17 | graph.insert({ a: 1, b: 'b' }) 18 | 19 | expect((graph as any).nodes.size).toBe(1) 20 | expect((graph as any).adjacency.length).toBe(1) 21 | expect((graph as any).adjacency[0].length).toBe(1) 22 | 23 | expect(() => { 24 | graph.insert({ a: 1, b: 'b' }) 25 | }).toThrow(NodeAlreadyExistsError) 26 | expect((graph as any).nodes.size).toBe(1) 27 | expect((graph as any).adjacency.length).toBe(1) 28 | expect((graph as any).adjacency[0].length).toBe(1) 29 | }) 30 | 31 | it('can add a node with custom identity function', () => { 32 | type NodeType = { a: number; b: string } 33 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 34 | 35 | graph.insert({ a: 1, b: 'b' }) 36 | 37 | expect((graph as any).nodes.size).toBe(1) 38 | expect((graph as any).adjacency.length).toBe(1) 39 | expect((graph as any).adjacency[0].length).toBe(1) 40 | 41 | expect(() => { 42 | graph.insert({ a: 1, b: 'not b' }) 43 | }).toThrow(NodeAlreadyExistsError) 44 | expect(() => { 45 | graph.insert({ a: 1.0007, b: 'not b' }) 46 | }).toThrow(NodeAlreadyExistsError) 47 | 48 | expect((graph as any).nodes.size).toBe(1) 49 | expect((graph as any).adjacency.length).toBe(1) 50 | expect((graph as any).adjacency[0].length).toBe(1) 51 | 52 | graph.insert({ a: 2, b: 'not b' }) 53 | 54 | expect((graph as any).nodes.size).toBe(2) 55 | expect((graph as any).adjacency.length).toBe(2) 56 | expect((graph as any).adjacency[0].length).toBe(2) 57 | }) 58 | 59 | it('can replace a node', () => { 60 | const graph = new Graph<{ a: number; b: string }>() 61 | 62 | graph.insert({ a: 1, b: 'b' }) 63 | graph.replace({ a: 1, b: 'b' }) 64 | 65 | expect(() => { 66 | graph.replace({ a: 1, b: 'c' }) 67 | }).toThrow(NodeDoesntExistError) 68 | expect((graph as any).nodes.get(hash({ a: 1, b: 'c' }))).toBeUndefined() 69 | 70 | expect((graph as any).nodes.size).toBe(1) 71 | expect((graph as any).adjacency.length).toBe(1) 72 | expect((graph as any).adjacency[0].length).toBe(1) 73 | expect((graph as any).nodes.get(hash({ a: 1, b: 'b' }))).toEqual({ a: 1, b: 'b' }) 74 | }) 75 | 76 | it('can replace a node with custom identity function', () => { 77 | type NodeType = { a: number; b: string } 78 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 79 | 80 | graph.insert({ a: 1, b: 'b' }) 81 | graph.replace({ a: 1, b: 'not b' }) 82 | 83 | expect((graph as any).nodes.size).toBe(1) 84 | expect((graph as any).adjacency.length).toBe(1) 85 | expect((graph as any).adjacency[0].length).toBe(1) 86 | expect((graph as any).nodes.get('1.00')).toBeDefined() 87 | expect((graph as any).nodes.get('1.00')).toEqual({ a: 1, b: 'not b' }) 88 | 89 | graph.replace({ a: 1.0007, b: 'not b' }) 90 | 91 | expect((graph as any).nodes.size).toBe(1) 92 | expect((graph as any).adjacency.length).toBe(1) 93 | expect((graph as any).adjacency[0].length).toBe(1) 94 | expect((graph as any).nodes.get('1.00')).toBeDefined() 95 | expect((graph as any).nodes.get('1.00')).toEqual({ a: 1.0007, b: 'not b' }) 96 | 97 | expect(() => { 98 | graph.replace({ a: 2.5, b: 'c' }) 99 | }).toThrow(NodeDoesntExistError) 100 | expect((graph as any).nodes.get('2.50')).toBeUndefined() 101 | }) 102 | 103 | it('can upsert a node', () => { 104 | type NodeType = { a: number; b: string } 105 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 106 | 107 | graph.insert({ a: 1, b: 'b' }) 108 | graph.upsert({ a: 1, b: 'not b' }) 109 | 110 | expect((graph as any).nodes.size).toBe(1) 111 | expect((graph as any).adjacency.length).toBe(1) 112 | expect((graph as any).adjacency[0].length).toBe(1) 113 | expect((graph as any).nodes.get('1.00')).toBeDefined() 114 | expect((graph as any).nodes.get('1.00')).toEqual({ a: 1, b: 'not b' }) 115 | 116 | graph.upsert({ a: 2.5, b: 'super not b' }) 117 | 118 | expect((graph as any).nodes.size).toBe(2) 119 | expect((graph as any).adjacency.length).toBe(2) 120 | expect((graph as any).adjacency[0].length).toBe(2) 121 | expect((graph as any).nodes.get('2.50')).toBeDefined() 122 | expect((graph as any).nodes.get('2.50')).toEqual({ a: 2.5, b: 'super not b' }) 123 | }) 124 | 125 | it('can add an edge', () => { 126 | type NodeType = { a: number; b: string } 127 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 128 | 129 | graph.insert({ a: 1, b: 'b' }) 130 | 131 | expect(() => graph.addEdge('3.00', '2.00')).toThrow(NodeDoesntExistError) 132 | expect(() => graph.addEdge('1.00', '2.00')).toThrow(NodeDoesntExistError) 133 | expect(() => graph.addEdge('2.00', '1.00')).toThrow(NodeDoesntExistError) 134 | 135 | graph.insert({ a: 2, b: 'b' }) 136 | graph.insert({ a: 3, b: 'b' }) 137 | graph.insert({ a: 4, b: 'b' }) 138 | 139 | graph.addEdge('1.00', '2.00') 140 | expect((graph as any).adjacency[0][1]).toBe(1) 141 | expect((graph as any).adjacency[1][0]).toBeFalsy() 142 | expect((graph as any).adjacency[1][2]).toBe(0) 143 | 144 | graph.addEdge('2.00', '1.00') 145 | expect((graph as any).adjacency[0][1]).toBe(1) 146 | expect((graph as any).adjacency[1][0]).toBe(1) 147 | expect((graph as any).adjacency[1][2]).toBeFalsy() 148 | }) 149 | 150 | it('can return the nodes', () => { 151 | type NodeType = { a: number; b: string } 152 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 153 | 154 | graph.insert({ a: 1, b: 'b' }) 155 | 156 | expect(graph.getNodes()).toEqual([{ a: 1, b: 'b' }]) 157 | 158 | graph.insert({ a: 2, b: 'b' }) 159 | graph.insert({ a: 3, b: 'b' }) 160 | graph.insert({ a: 4, b: 'b' }) 161 | 162 | expect(graph.getNodes()).toContainEqual({ a: 1, b: 'b' }) 163 | expect(graph.getNodes()).toContainEqual({ a: 2, b: 'b' }) 164 | expect(graph.getNodes()).toContainEqual({ a: 3, b: 'b' }) 165 | expect(graph.getNodes()).toContainEqual({ a: 4, b: 'b' }) 166 | }) 167 | 168 | it('can return the nodes sorted', () => { 169 | type NodeType = { a: number; b: string } 170 | const graph = new Graph((n: NodeType) => n.a.toFixed(2)) 171 | 172 | graph.insert({ a: 2, b: 'b' }) 173 | graph.insert({ a: 4, b: 'b' }) 174 | graph.insert({ a: 1, b: 'b' }) 175 | graph.insert({ a: 3, b: 'b' }) 176 | 177 | expect(graph.getNodes((a, b) => a.a - b.a)).toEqual([ 178 | { a: 1, b: 'b' }, 179 | { a: 2, b: 'b' }, 180 | { a: 3, b: 'b' }, 181 | { a: 4, b: 'b' } 182 | ]) 183 | }) 184 | 185 | it('can get a specific node', () => { 186 | type NodeType = { a: number; b: string } 187 | const identityfn = (n: NodeType) => n.a.toFixed(2) 188 | const graph = new Graph(identityfn) 189 | 190 | const inputToRetrieve = { a: 1, b: 'c' } 191 | 192 | graph.insert({ a: 2, b: 'b' }) 193 | graph.insert({ a: 4, b: 'b' }) 194 | graph.insert(inputToRetrieve) 195 | graph.insert({ a: 3, b: 'b' }) 196 | 197 | expect(graph.getNode(identityfn(inputToRetrieve))).toBeDefined() 198 | expect(graph.getNode(identityfn(inputToRetrieve))).toEqual(inputToRetrieve) 199 | expect(graph.getNode('nonsense')).toBeUndefined() 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /test/readme.test.ts: -------------------------------------------------------------------------------- 1 | import { DirectedAcyclicGraph, DirectedGraph, Graph } from '../src' 2 | 3 | describe('The Readme', () => { 4 | it('runs the first example correctly', () => { 5 | // Identify the node type to be used with the graph 6 | type NodeType = { name: string; count: number; metadata: { [string: string]: string } } 7 | // Define a custom identity function with which to identify nodes 8 | const graph = new Graph((n: NodeType) => n.name) 9 | 10 | // Insert nodes into the graph 11 | const node1 = graph.insert({ name: 'node1', count: 45, metadata: { color: 'green' } }) 12 | const node2 = graph.insert({ 13 | name: 'node2', 14 | count: 5, 15 | metadata: { color: 'red', style: 'normal' } 16 | }) 17 | const node3 = graph.insert({ 18 | name: 'node3', 19 | count: 15, 20 | metadata: { color: 'blue', size: 'large' } 21 | }) 22 | 23 | // Add edges between the nodes we created. 24 | graph.addEdge(node1, node2) 25 | graph.addEdge(node2, node3) 26 | 27 | const node: NodeType | undefined = graph.getNode(node2) 28 | 29 | expect(graph).toBeInstanceOf(Graph) 30 | expect(node).toBeDefined() 31 | expect(node?.count).toEqual(5) 32 | }) 33 | 34 | it('runs the second example correctly', () => { 35 | // Create the graph 36 | type NodeType = { name: string; count: number } 37 | const graph = new DirectedGraph((n: NodeType) => n.name) 38 | 39 | // Insert nodes into the graph 40 | const node1 = graph.insert({ name: 'node1', count: 45 }) 41 | const node2 = graph.insert({ name: 'node2', count: 5 }) 42 | const node3 = graph.insert({ name: 'node3', count: 15 }) 43 | 44 | // Check for cycles 45 | expect(graph.isAcyclic()).toBe(true) // true 46 | 47 | // Add edges between the nodes we created. 48 | graph.addEdge(node1, node2) 49 | graph.addEdge(node2, node3) 50 | 51 | // Check for cycles again 52 | expect(graph.isAcyclic()).toBe(true) // still true 53 | 54 | // Converts the graph into one that enforces acyclicality 55 | const dag = DirectedAcyclicGraph.fromDirectedGraph(graph) 56 | 57 | // Try to add an edge that will cause an cycle 58 | expect(() => dag.addEdge(node3, node1)).toThrow() // throws an exception 59 | 60 | // You can add the edge that would cause a cycle on the preview graph 61 | graph.addEdge(node3, node1) 62 | 63 | // Check for cycles again 64 | expect(graph.isAcyclic()).toBe(false) // now false 65 | 66 | expect(() => DirectedAcyclicGraph.fromDirectedGraph(graph)).toThrow() // now throws an exception because graph is not acyclic 67 | }) 68 | 69 | it('runs the third example correctly', () => { 70 | // Create the graph 71 | type NodeType = { name: string } 72 | const graph = new DirectedAcyclicGraph((n: NodeType) => n.name) 73 | 74 | // Insert nodes into the graph 75 | const node1 = graph.insert({ name: 'node1' }) 76 | const node2 = graph.insert({ name: 'node2' }) 77 | const node3 = graph.insert({ name: 'node3' }) 78 | const node4 = graph.insert({ name: 'node4' }) 79 | const node5 = graph.insert({ name: 'node5' }) 80 | 81 | // Add edges 82 | graph.addEdge(node1, node2) 83 | graph.addEdge(node2, node4) 84 | graph.addEdge(node1, node3) 85 | graph.addEdge(node3, node5) 86 | graph.addEdge(node5, node4) 87 | 88 | // Get the nodes in topologically sorted order 89 | expect(graph.topologicallySortedNodes()).toEqual([ 90 | { name: 'node1' }, 91 | { name: 'node3' }, 92 | { name: 'node5' }, 93 | { name: 'node2' }, 94 | { name: 'node4' } 95 | ]) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require("shelljs") 2 | const { readFileSync } = require("fs") 3 | const url = require("url") 4 | 5 | let repoUrl 6 | let pkg = JSON.parse(readFileSync("package.json") as any) 7 | if (typeof pkg.repository === "object") { 8 | if (!pkg.repository.hasOwnProperty("url")) { 9 | throw new Error("URL does not exist in repository section") 10 | } 11 | repoUrl = pkg.repository.url 12 | } else { 13 | repoUrl = pkg.repository 14 | } 15 | 16 | let parsedUrl = url.parse(repoUrl) 17 | let repository = (parsedUrl.host || "") + (parsedUrl.path || "") 18 | let ghToken = process.env.GH_TOKEN 19 | 20 | echo("Deploying docs!!!") 21 | cd("docs") 22 | touch(".nojekyll") 23 | exec("git init") 24 | exec("git add .") 25 | exec('git config user.name "Max Walker"') 26 | exec('git config user.email "maxwell.a.walker@gmail.com"') 27 | exec('git commit -m "docs(docs): update gh-pages"') 28 | exec('git checkout -b gh-pages') 29 | exec( 30 | `git push --force --quiet "git@github.com:SegFaultx64/typescript-graph.git" gh-pages` 31 | ) 32 | echo("Docs deployed!!") 33 | -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { fork } = require("child_process") 3 | const colors = require("colors") 4 | 5 | const { readFileSync, writeFileSync } = require("fs") 6 | const pkg = JSON.parse( 7 | readFileSync(path.resolve(__dirname, "..", "package.json")) 8 | ) 9 | 10 | pkg.scripts.prepush = "npm run test:prod && npm run build" 11 | pkg.scripts.commitmsg = "commitlint -E HUSKY_GIT_PARAMS" 12 | 13 | writeFileSync( 14 | path.resolve(__dirname, "..", "package.json"), 15 | JSON.stringify(pkg, null, 2) 16 | ) 17 | 18 | // Call husky to set up the hooks 19 | fork(path.resolve(__dirname, "..", "node_modules", "husky", "lib", "installer", 'bin'), ['install']) 20 | 21 | console.log() 22 | console.log(colors.green("Done!!")) 23 | console.log() 24 | 25 | if (pkg.repository.url.trim()) { 26 | console.log(colors.cyan("Now run:")) 27 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 28 | console.log(colors.cyan(" semantic-release-cli setup")) 29 | console.log() 30 | console.log( 31 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 32 | ) 33 | console.log() 34 | console.log( 35 | colors.gray( 36 | 'Note: Make sure "repository.url" in your package.json is correct before' 37 | ) 38 | ) 39 | } else { 40 | console.log( 41 | colors.red( 42 | 'First you need to set the "repository.url" property in package.json' 43 | ) 44 | ) 45 | console.log(colors.cyan("Then run:")) 46 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 47 | console.log(colors.cyan(" semantic-release-cli setup")) 48 | console.log() 49 | console.log( 50 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 51 | ) 52 | } 53 | 54 | console.log() 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module":"es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "declarationDir": "dist/types", 14 | "outDir": "dist/lib", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ] 18 | }, 19 | "include": [ 20 | "src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } --------------------------------------------------------------------------------