UNPKG

gdo

Version:

Group- and Dependency-based Ordering

195 lines (170 loc) 7.38 kB
/* ** GDO -- Group- and Dependency-based Ordering ** Copyright (c) 2015-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> ** ** 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: ** ** The above copyright notice and this permission notice shall be included ** in all copies or substantial portions of the Software. ** ** 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. */ import toposort from "toposort" /* the API class */ class GDO { constructor () { this.reset() } /* reset the instance */ reset () { this._groups = [] this._elements = [] } /* configure all groups */ groups (groups) { this._groups = groups } /* configure one element */ element (element) { this._elements.push(element) } /* topologically order the elements by taking account of tags, groups and dependencies */ order () { /* determine all graph nodes */ const nodes = {} this._elements.forEach((element) => { if (nodes[element.name]) throw new Error(`element named "${element.name}" occurs multiple times (has to be unique)`) nodes[element.name] = true }) /* internal helper data structures */ const DAG = {} const TAG = {} const GRP = {} const BEFORE = {} const AFTER = {} /* pre-fill all groups with sentinel elements to ensure that a group dependency always has at least one element it can be expanded to */ this._groups.forEach((group) => { if (GRP[group]) throw new Error(`group named "${group}" occurs multiple times (has to be unique)`) GRP[group] = [ `@@@${group}` ] nodes[`@@@${group}`] = true }) /* helper function for taking zero or more strings out of a field */ const takeField = (field) => { if (typeof field === "object" && field instanceof Array) return field else if (typeof field === "string") return [ field ] else return [] } /* pass 1: iterate over all elements and pre-process information */ this._elements.forEach((element) => { /* take information of element */ const name = element.name const tag = takeField(element.tag) const before = takeField(element.before) const after = takeField(element.after) const group = element.group /* remember (a mutable copy of) after/before information */ BEFORE[name] = [].concat(before) AFTER[name] = [].concat(after) /* remember mapping of tag to element */ tag.forEach((tag) => { if (this._groups.indexOf(tag) > -1) throw new Error(`element "${element.name}" has invalid tag "${tag}" ` + "(tag cannot have same name as existing group)") if (TAG[tag] === undefined) TAG[tag] = [] TAG[tag].push(name) }) /* remember group of module */ if (group !== undefined) { const idx = this._groups.indexOf(group) if (idx === -1) throw new Error(`element "${element.name}" has invalid group "${group}" ` + "(group has to be explicitly defined)") GRP[group].push(name) /* add implicit before/after for elements of intermediate groups */ if (idx < this._groups.length - 1) BEFORE[name].push(this._groups[idx + 1]) if (idx > 0) AFTER[name].push(this._groups[idx - 1]) } }) /* helper function: insert edge into DAG */ const insertDAG = (name, list, order) => { list.forEach((element) => { let elements let via if (TAG[element] !== undefined) { elements = TAG[element] via = "tag-based" } else if (GRP[element] !== undefined) { elements = GRP[element] via = "group-based" } else { elements = [ element ] via = "direct" } elements.forEach((element) => { const [ before, after ] = order(name, element) if (nodes[before] === undefined) throw new Error(`element "${name}" has invalid ${via} before-reference ` + `to unknown element "${before}"`) if (nodes[after] === undefined) throw new Error(`element "${name}" has invalid ${via} after-reference ` + `to unknown element "${after}"`) if (DAG[before] === undefined) DAG[before] = {} DAG[before][after] = true }) }) } /* pass 2: iterate over all elements and process "after" and "before" information */ this._elements.forEach((element) => { /* take information of module */ const name = element.name const before = BEFORE[name] const after = AFTER[name] /* insert all "after" dependencies into DAG (as standard "after" dependencies) */ insertDAG(name, after, (name, element) => [ element, name ]) /* insert all "before" dependencies into DAG (as inverse "after" dependencies) */ insertDAG(name, before, (name, element) => [ name, element ]) }) /* determine resulting graph edges */ const edges = [] Object.keys(DAG).forEach((before) => { Object.keys(DAG[before]).forEach((after) => { edges.push([ before, after ]) }) }) /* perform a topological sorting of the graph */ let elements = toposort.array(Object.keys(nodes), edges) /* remove group sentinel values again */ elements = elements.filter((element) => !element.match(/^@@@.+/)) /* return the final ordered list of elements */ return elements } } module.exports = GDO