cli: add PackagesGraph for traversing monorepo package graphs
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@lerna/package-graph": "^4.0.0",
|
||||
"@lerna/project": "^4.0.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@octokit/request": "^5.4.12",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { paths } from '../paths';
|
||||
import { PackageGraph } from './PackageGraph';
|
||||
|
||||
const testPackages = [
|
||||
{
|
||||
dir: '/packages/a',
|
||||
packageJson: {
|
||||
name: 'a',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
b: '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
c: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: '/packages/b',
|
||||
packageJson: {
|
||||
name: 'b',
|
||||
version: '1.0.0',
|
||||
devDependencies: {
|
||||
c: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: '/packages/c',
|
||||
packageJson: {
|
||||
name: 'c',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('PackageGraph', () => {
|
||||
it('is able to construct a graph from this repo', async () => {
|
||||
const { packages } = await getPackages(paths.ownDir);
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
expect(graph.has('@backstage/cli')).toBe(true);
|
||||
});
|
||||
|
||||
it('creates a graph', () => {
|
||||
const graph = PackageGraph.fromPackages(testPackages);
|
||||
const a = graph.get('a');
|
||||
const b = graph.get('b');
|
||||
const c = graph.get('c');
|
||||
|
||||
expect(a).toMatchObject({
|
||||
name: 'a',
|
||||
dir: '/packages/a',
|
||||
allLocalDependencies: new Map([
|
||||
['b', b],
|
||||
['c', c],
|
||||
]),
|
||||
publishedLocalDependencies: new Map([['b', b]]),
|
||||
localDependencies: new Map([['b', b]]),
|
||||
localDevDependencies: new Map([['c', c]]),
|
||||
localOptionalDependencies: new Map(),
|
||||
});
|
||||
expect(b).toMatchObject({
|
||||
name: 'b',
|
||||
dir: '/packages/b',
|
||||
allLocalDependencies: new Map([['c', c]]),
|
||||
publishedLocalDependencies: new Map(),
|
||||
localDependencies: new Map(),
|
||||
localDevDependencies: new Map([['c', c]]),
|
||||
localOptionalDependencies: new Map(),
|
||||
});
|
||||
expect(c).toMatchObject({
|
||||
name: 'c',
|
||||
dir: '/packages/c',
|
||||
allLocalDependencies: new Map(),
|
||||
publishedLocalDependencies: new Map(),
|
||||
localDependencies: new Map(),
|
||||
localDevDependencies: new Map(),
|
||||
localOptionalDependencies: new Map(),
|
||||
});
|
||||
});
|
||||
|
||||
it('collects package names', () => {
|
||||
const graph = PackageGraph.fromPackages(testPackages);
|
||||
|
||||
expect(graph.collectPackageNames(['a'], () => undefined)).toEqual(
|
||||
new Set(['a']),
|
||||
);
|
||||
expect(
|
||||
graph.collectPackageNames(['a'], pkg => pkg.localDependencies.keys()),
|
||||
).toEqual(new Set(['a', 'b']));
|
||||
expect(
|
||||
graph.collectPackageNames(['a'], pkg => pkg.localDevDependencies.keys()),
|
||||
).toEqual(new Set(['a', 'c']));
|
||||
expect(
|
||||
graph.collectPackageNames(['b', 'a'], pkg =>
|
||||
pkg.localDevDependencies.keys(),
|
||||
),
|
||||
).toEqual(new Set(['b', 'a', 'c']));
|
||||
|
||||
// Should not get stuck in cycles
|
||||
expect(graph.collectPackageNames(['a'], () => ['a', 'b', 'c'])).toEqual(
|
||||
new Set(['b', 'a', 'c']),
|
||||
);
|
||||
|
||||
// Throws on unknown packages
|
||||
expect(() => graph.collectPackageNames(['a'], () => ['unknown'])).toThrow(
|
||||
`Package 'unknown' not found`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Package } from '@manypkg/get-packages';
|
||||
|
||||
export type PackageGraphNode = {
|
||||
/** The name of the package */
|
||||
name: string;
|
||||
/** The directory of the package */
|
||||
dir: string;
|
||||
/** The package data of the package itself */
|
||||
packageJson: Package['packageJson'];
|
||||
/** All direct local dependencies of the package */
|
||||
allLocalDependencies: Map<string, PackageGraphNode>;
|
||||
/** All direct local dependencies that will be present in the published package */
|
||||
publishedLocalDependencies: Map<string, PackageGraphNode>;
|
||||
/** Local dependencies */
|
||||
localDependencies: Map<string, PackageGraphNode>;
|
||||
/** Local devDependencies */
|
||||
localDevDependencies: Map<string, PackageGraphNode>;
|
||||
/** Local optionalDependencies */
|
||||
localOptionalDependencies: Map<string, PackageGraphNode>;
|
||||
};
|
||||
|
||||
export class PackageGraph extends Map<string, PackageGraphNode> {
|
||||
static fromPackages(packages: Package[]): PackageGraph {
|
||||
const graph = new PackageGraph();
|
||||
|
||||
// Add all local packages to the graph
|
||||
for (const pkg of packages) {
|
||||
const name = pkg.packageJson.name;
|
||||
const existingPkg = graph.get(name);
|
||||
if (existingPkg) {
|
||||
throw new Error(
|
||||
`Duplicate package name '${name}' at ${pkg.dir} and ${existingPkg.dir}`,
|
||||
);
|
||||
}
|
||||
|
||||
graph.set(name, {
|
||||
...pkg,
|
||||
name,
|
||||
allLocalDependencies: new Map(),
|
||||
publishedLocalDependencies: new Map(),
|
||||
localDependencies: new Map(),
|
||||
localDevDependencies: new Map(),
|
||||
localOptionalDependencies: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
// Populate the local dependency structure
|
||||
for (const node of graph.values()) {
|
||||
for (const depName of Object.keys(node.packageJson.dependencies || {})) {
|
||||
const depPkg = graph.get(depName);
|
||||
if (depPkg) {
|
||||
node.allLocalDependencies.set(depName, depPkg);
|
||||
node.publishedLocalDependencies.set(depName, depPkg);
|
||||
node.localDependencies.set(depName, depPkg);
|
||||
}
|
||||
}
|
||||
for (const depName of Object.keys(
|
||||
node.packageJson.devDependencies || {},
|
||||
)) {
|
||||
const depPkg = graph.get(depName);
|
||||
if (depPkg) {
|
||||
node.allLocalDependencies.set(depName, depPkg);
|
||||
node.localDevDependencies.set(depName, depPkg);
|
||||
}
|
||||
}
|
||||
for (const depName of Object.keys(
|
||||
node.packageJson.optionalDependencies || {},
|
||||
)) {
|
||||
const depPkg = graph.get(depName);
|
||||
if (depPkg) {
|
||||
node.allLocalDependencies.set(depName, depPkg);
|
||||
node.publishedLocalDependencies.set(depName, depPkg);
|
||||
node.localOptionalDependencies.set(depName, depPkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the package graph and collects a set of package names.
|
||||
*
|
||||
* The traversal starts at the provided list names, and continues
|
||||
* throughout all the names returned by the `collectFn`, which is
|
||||
* called once for each seen package.
|
||||
*/
|
||||
collectPackageNames(
|
||||
startingPackageNames: string[],
|
||||
collectFn: (pkg: PackageGraphNode) => Iterable<string> | undefined,
|
||||
): Set<string> {
|
||||
const targets = new Set<string>();
|
||||
const searchNames = startingPackageNames.slice();
|
||||
|
||||
while (searchNames.length) {
|
||||
const name = searchNames.pop()!;
|
||||
|
||||
if (targets.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node = this.get(name);
|
||||
if (!node) {
|
||||
throw new Error(`Package '${name}' not found`);
|
||||
}
|
||||
|
||||
targets.add(name);
|
||||
|
||||
const collected = collectFn(node);
|
||||
if (collected) {
|
||||
searchNames.push(...collected);
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { PackageGraph } from './PackageGraph';
|
||||
export type { PackageGraphNode } from './PackageGraph';
|
||||
@@ -4532,6 +4532,18 @@
|
||||
globby "^11.0.0"
|
||||
read-yaml-file "^1.1.0"
|
||||
|
||||
"@manypkg/get-packages@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz#e184db9bba792fa4693de4658cfb1463ac2c9c47"
|
||||
integrity sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
"@changesets/types" "^4.0.1"
|
||||
"@manypkg/find-root" "^1.1.0"
|
||||
fs-extra "^8.1.0"
|
||||
globby "^11.0.0"
|
||||
read-yaml-file "^1.1.0"
|
||||
|
||||
"@mapbox/node-pre-gyp@^1.0.0":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950"
|
||||
|
||||
Reference in New Issue
Block a user