import { hoist, HoisterTree, HoisterResult, HoisterDependencyKind } from "app-builder-lib/out/node-module-collector/hoist"
import { expect } from "vitest"
const toTree = (obj: any, key: string = `.`, nodes = new Map()): HoisterTree => {
let node = nodes.get(key)
const name = key.match(/@?[^@]+/)![0]
if (!node) {
node = {
name,
identName: (obj[key] || {}).identName || name,
reference: key.match(/@?[^@]+@?(.+)?/)![1] || ``,
dependencies: new Set<HoisterTree>(),
peerNames: new Set<string>((obj[key] || {}).peerNames || []),
dependencyKind: (obj[key] || {}).dependencyKind,
}
nodes.set(key, node)
for (const dep of (obj[key] || {}).dependencies || []) {
node.dependencies.add(toTree(obj, dep, nodes))
}
}
return node
}
const getTreeHeight = (tree: HoisterResult): number => {
let height = 0
let maxHeight = 0
const seen = new Set<HoisterResult>()
const visitNode = (node: HoisterResult) => {
if (seen.has(node)) return
seen.add(node)
height += 1
maxHeight = Math.max(height, maxHeight)
for (const dep of node.dependencies) visitNode(dep)
height -= 1
}
visitNode(tree)
return maxHeight
}
describe(`hoist`, () => {
it(`should do very basic hoisting`, () => {
const tree = {
".": { dependencies: [`A`] },
A: { dependencies: [`B`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should support basic cyclic dependencies`, () => {
const tree = {
".": { dependencies: [`C`] },
C: { dependencies: [`A`] },
A: { dependencies: [`B`, `D`] },
B: { dependencies: [`A`, `E`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should support cyclic peer dependencies`, () => {
const tree = {
".": { dependencies: [`D`, `E@X`] },
D: { dependencies: [`A`, `B`, `C`, `E@Y`] },
A: { dependencies: [`B`], peerNames: [`B`] },
B: { dependencies: [`C`], peerNames: [`C`] },
C: { dependencies: [`A`, `E@Y`], peerNames: [`A`, `E`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should keep require promise`, () => {
const tree = {
".": { dependencies: [`A`, `C@Y`, `D@Y`] },
A: { dependencies: [`B`, `C@Z`, `F@Z`] },
B: { dependencies: [`C@X`, `F@X`] },
"F@X": { dependencies: [`G@X`] },
"C@X": { dependencies: [`D@X`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should not forget hoisted dependencies`, () => {
const tree = {
".": { dependencies: [`A`, `C@Y`] },
A: { dependencies: [`B`] },
B: { dependencies: [`A`, `C@X`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should not hoist different package with the same name`, () => {
const tree = {
".": { dependencies: [`A`, `B@Y`] },
A: { dependencies: [`B@X`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should not hoist package that has several versions on the same tree path`, () => {
const tree = {
".": { dependencies: [`A`] },
A: { dependencies: [`B@X`] },
"B@X": { dependencies: [`C`] },
C: { dependencies: [`B@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should perform deep hoisting`, () => {
const tree = {
".": { dependencies: [`A`, `B@Y`, `C@X`] },
A: { dependencies: [`B@X`, `C@X`] },
"B@X": { dependencies: [`C@Y`] },
C: { dependencies: [`B@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should tolerate self-dependencies`, () => {
const tree = {
".": { dependencies: [`.`, `A`, `B@Y`, `C@X`] },
A: { dependencies: [`A`, `B@X`, `C@X`] },
"B@X": { dependencies: [`B@X`, `C@Y`] },
C: { dependencies: [`B@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should honor package popularity when hoisting`, () => {
const tree = {
".": { dependencies: [`A`, `C`, `D`, `E`, `F`] },
A: { dependencies: [`B@X`] },
C: { dependencies: [`B@X`] },
D: { dependencies: [`B@Y`] },
E: { dependencies: [`B@Y`] },
F: { dependencies: [`G`] },
G: { dependencies: [`B@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should honor peer dependencies`, () => {
const tree = {
".": { dependencies: [`A`, `D@Y`] },
A: { dependencies: [`B`, `D@X`] },
B: { dependencies: [`D@X`], peerNames: [`D`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should honor package popularity considering number of all references over number of references by peers`, () => {
const tree = toTree({
".": { dependencies: [`A`, `B`, `C`, `D`] },
A: { dependencies: [`Z@X`] },
B: { dependencies: [`Z@X`] },
C: { dependencies: [`Z@X`] },
D: { dependencies: [`Z@Y`, `U`] },
U: { dependencies: [`Z@Y`], peerNames: [`Z`] },
})
const result = hoist(tree, { check: true })
expect(getTreeHeight(result)).toEqual(3)
const topLevelDeps = [...result.dependencies]
const hoistedZ = topLevelDeps.find(x => x.name === `Z`)!
expect(hoistedZ.references).toContain(`X`)
expect(hoistedZ.references).not.toContain(`Y`)
const D = topLevelDeps.find(x => x.name === `D`)!
const dDeps = [...D.dependencies]
expect(dDeps.length).toEqual(2)
const nestedZ = dDeps.find(x => x.name === `Z`)!
expect(nestedZ.references).not.toContain(`X`)
expect(nestedZ.references).toContain(`Y`)
})
it(`should hoist dependencies after hoisting peer dep`, () => {
const tree = {
".": { dependencies: [`A`] },
A: { dependencies: [`B`, `D@X`] },
B: { dependencies: [`D@X`], peerNames: [`D`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should honor unhoisted peer dependencies`, () => {
const tree = {
".": { dependencies: [`A`, `B@X`, `C@Y`] },
A: { dependencies: [`B@X`, `C@X`], peerNames: [`B`] },
"C@X": { dependencies: [`B@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should honor peer dependency promise for the same version of dependency`, () => {
const tree = {
".": { dependencies: [`A`], peerNames: [`B`] },
A: { dependencies: [`B`] },
B: { dependencies: [`C`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should hoist different copies of a package independently`, () => {
const tree = {
".": { dependencies: [`A`, `D`, `B@Y`, `C@Z`] },
A: { dependencies: [`B@X`, `C@Y`] },
"B@X": { dependencies: [`C@X`] },
D: { dependencies: [`B@X`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should hoist different copies of a package independently (complicated case)`, () => {
const tree = {
".": { dependencies: [`A`, `E`, `F`, `B@Y`, `C@Z`, `D@Y`] },
A: { dependencies: [`B@X`, `C@Y`] },
"B@X": { dependencies: [`C@X`] },
"C@X": { dependencies: [`D@X`] },
E: { dependencies: [`B@X`] },
F: { dependencies: [`G`] },
G: { dependencies: [`B@X`, `D@Z`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should keep peer dependency promise for the case where the package with same ident is a dependency of parent node`, () => {
const tree = {
".": { dependencies: [`A`, `B@X#2`, `C@X`] },
A: { dependencies: [`B@X#1`, `C@Y`] },
"B@X#1": { dependencies: [`C@Y`], peerNames: [`C`] },
"B@X#2": { dependencies: [`C@X`], peerNames: [`C`] },
}
const hoistedTree = hoist(toTree(tree), { check: true })
const [A] = Array.from(hoistedTree.dependencies).filter(x => x.name === `A`)
expect(Array.from(A.dependencies).filter(x => x.name === `B`)).toBeDefined()
})
it(`should hoist cyclic peer dependencies`, () => {
const tree = {
".": { dependencies: [`A`] },
A: { dependencies: [`B`, `C`, `D`, `E`] },
B: { dependencies: [`C`, `D`, `E`, `F`, `G`], peerNames: [`E`] },
C: { dependencies: [`D`], peerNames: [`D`] },
D: { dependencies: [`E`, `C`], peerNames: [`E`, `C`] },
E: { dependencies: [`C`], peerNames: [`C`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should respect transitive peer dependencies mixed with direct peer dependencies`, () => {
const tree = {
".": { dependencies: [`A`, `C@Y`] },
A: { dependencies: [`B`, `C@X`] },
B: { dependencies: [`D`, `E`, `C@X`], peerNames: [`C`] },
D: { dependencies: [`C@X`, `E`], peerNames: [`C`, `E`] },
}
const hoistedTree = hoist(toTree(tree), { check: true })
const D = Array.from(hoistedTree.dependencies).filter(x => x.name === `D`)
expect(D).toEqual([])
})
it(`should not hoist packages past hoist boundary`, () => {
const tree = {
".": { dependencies: [`A`, `C`] },
A: { dependencies: [`B`] },
B: { dependencies: [`D`] },
C: { dependencies: [`D`] },
}
const hoistingLimits = new Map([
[`.@`, new Set([`C`])],
[`A@`, new Set([`B`])],
])
expect(getTreeHeight(hoist(toTree(tree), { check: true, hoistingLimits }))).toEqual(3)
})
it(`should not hoist multiple package past nohoist root`, () => {
const tree = {
".": { dependencies: [`A`] },
A: { dependencies: [`B`] },
B: { dependencies: [`C`] },
C: { dependencies: [`D`] },
D: { dependencies: [`E`] },
}
const hoistingLimits = new Map([[`A@`, new Set([`B`])]])
expect(getTreeHeight(hoist(toTree(tree), { check: true, hoistingLimits }))).toEqual(3)
})
it(`should hoist a tree which requires multiple passes to get terminal result`, () => {
const tree = {
".": { dependencies: [`A`, `B@X`, `C@Z`, `D@Y`, `E@Y`, `F@Y`] },
A: { dependencies: [`D@X`] },
"D@X": { dependencies: [`F@X`, `C@X`] },
"F@X": { dependencies: [`E@X`, `C@Z`] },
"E@X": { dependencies: [`B@Y`, `C@X`] },
"B@Y": { dependencies: [`C@Z`] },
}
const hoistedTree = hoist(toTree(tree), { check: true })
const AC = Array.from(Array.from(hoistedTree.dependencies).filter(x => x.name === `A`)[0].dependencies).filter(x => x.name === `C`)
expect(AC).toEqual([])
})
it(`should hoist dependencies that peer dependent on their parent`, () => {
const tree = {
".": { dependencies: [`C`] },
C: { dependencies: [`A`] },
A: { dependencies: [`A`, `B`] },
B: { dependencies: [`A`], peerNames: [`A`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should hoist direct workspace dependencies into non-root workspace`, () => {
const tree = {
".": { dependencies: [`W1(w)`, `W3`, `A@Z`], dependencyKind: HoisterDependencyKind.WORKSPACE },
"W1(w)": { dependencies: [`W2(w)`, `A@Y`], dependencyKind: HoisterDependencyKind.WORKSPACE },
"W2(w)": { dependencies: [`W3(w)`], dependencyKind: HoisterDependencyKind.WORKSPACE },
"W3(w)": { dependencies: [`A@X`], dependencyKind: HoisterDependencyKind.WORKSPACE },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
it(`should hoist dependencies to the top from workspaces that have no hoist borders given there is workspace with hoist borders`, () => {
const tree = {
".": { dependencies: [`W1(w)`, `W2(w)`], dependencyKind: HoisterDependencyKind.WORKSPACE },
"W1(w)": { dependencies: [`A@X`, `B@X`], dependencyKind: HoisterDependencyKind.WORKSPACE },
"A@X": { dependencies: [`B@X`], peerNames: [`B`] },
"A@Y": { dependencies: [`B@Y`], peerNames: [`B`] },
"W2(w)": { dependencies: [`A@Y`, `B@Y`], dependencyKind: HoisterDependencyKind.WORKSPACE },
}
const hoistingLimits = new Map([[`.@`, new Set([`W1(w)`])]])
const hoistedTree = hoist(toTree(tree), { check: true, hoistingLimits })
const W2 = Array.from(Array.from(hoistedTree.dependencies).filter(x => x.name === `W2(w)`)[0].dependencies)
expect(W2).toEqual([])
})
it(`should hoist aliased packages`, () => {
const tree = {
".": { dependencies: [`Aalias`] },
Aalias: { identName: `A`, dependencies: [`A`] },
A: { dependencies: [`B`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should not hoist portal with unhoistable dependencies`, () => {
const tree = {
".": { dependencies: [`P1`, `B@Y`] },
P1: { dependencies: [`P2`], dependencyKind: HoisterDependencyKind.EXTERNAL_SOFT_LINK },
P2: { dependencies: [`B@X`], dependencyKind: HoisterDependencyKind.EXTERNAL_SOFT_LINK },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(3)
})
it(`should hoist nested portals with hoisted dependencies`, () => {
const tree = {
".": { dependencies: [`P1`, `B@X`] },
P1: { dependencies: [`P2`, `B@X`], dependencyKind: HoisterDependencyKind.EXTERNAL_SOFT_LINK },
P2: { dependencies: [`P3`, `B@X`], dependencyKind: HoisterDependencyKind.EXTERNAL_SOFT_LINK },
P3: { dependencies: [`B@X`], dependencyKind: HoisterDependencyKind.EXTERNAL_SOFT_LINK },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(2)
})
it(`should support two branch circular graph hoisting`, () => {
const tree = {
".": { dependencies: [`B`, `C`] },
B: { dependencies: [`D@X`] },
C: { dependencies: [`D@Y`] },
"D@X": { dependencies: [`E@X`, `F@X`] },
"D@Y": { dependencies: [`E@Y`, `F@X`] },
"E@X": { dependencies: [`D@X`, `F@X`] },
"E@Y": { dependencies: [`D@Y`, `F@Y`] },
}
expect(getTreeHeight(hoist(toTree(tree), { check: true }))).toEqual(4)
})
})