Merge pull request #17409 from backstage/rugvip/paths
repo-tools: include, exclude, and better path handling
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': patch
|
||||
---
|
||||
|
||||
Package paths provided to `api-reports` and OpenAPI commands will now match any path within the target package.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': patch
|
||||
---
|
||||
|
||||
Added `--include <patterns>` and `--exclude <patterns>` options for `api-reports` command that work based on package names.
|
||||
@@ -28,6 +28,8 @@ Options:
|
||||
--ci
|
||||
--tsc
|
||||
--docs
|
||||
--include <pattern>
|
||||
--exclude <pattern>
|
||||
-a, --allow-warnings <allowWarningsPaths>
|
||||
--allow-all-warnings
|
||||
-o, --omit-messages <messageCodes>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@microsoft/api-documenter": "^7.19.27",
|
||||
|
||||
@@ -1161,7 +1161,7 @@ export async function buildDocs({
|
||||
documenter.generateFiles();
|
||||
}
|
||||
|
||||
export async function categorizePackageDirs(packageDirs: any[]) {
|
||||
export async function categorizePackageDirs(packageDirs: string[]) {
|
||||
const dirs = packageDirs.slice();
|
||||
const tsPackageDirs = new Array<string>();
|
||||
const cliPackageDirs = new Array<string>();
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
|
||||
import { buildApiReports } from './api-reports';
|
||||
import { generateTypeDeclarations } from './generateTypeDeclarations';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
|
||||
jest.mock('./generateTypeDeclarations');
|
||||
// create mocks for the dependencies of the `buildApiReports` function
|
||||
@@ -52,6 +53,28 @@ jest
|
||||
jest.spyOn(projectPaths, 'resolveTargetRoot').mockImplementation((...path) => {
|
||||
return resolvePath(normalize('/root'), ...path);
|
||||
});
|
||||
jest.spyOn(PackageGraph, 'listTargetPackages').mockResolvedValue([
|
||||
{
|
||||
dir: '/root/packages/package-a',
|
||||
packageJson: { name: 'package-a', version: '0.0.0' },
|
||||
},
|
||||
{
|
||||
dir: '/root/packages/package-b',
|
||||
packageJson: { name: 'package-b', version: '0.0.0' },
|
||||
},
|
||||
{
|
||||
dir: '/root/plugins/plugin-a',
|
||||
packageJson: { name: 'plugin-a', version: '0.0.0' },
|
||||
},
|
||||
{
|
||||
dir: '/root/plugins/plugin-b',
|
||||
packageJson: { name: 'plugin-b', version: '0.0.0' },
|
||||
},
|
||||
{
|
||||
dir: '/root/plugins/plugin-c',
|
||||
packageJson: { name: 'plugin-c', version: '0.0.0' },
|
||||
},
|
||||
]);
|
||||
|
||||
describe('buildApiReports', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
runCliExtraction,
|
||||
buildDocs,
|
||||
} from './api-extractor';
|
||||
import { getMatchingWorkspacePaths, paths as cliPaths } from '../../lib/paths';
|
||||
import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths';
|
||||
import { generateTypeDeclarations } from './generateTypeDeclarations';
|
||||
|
||||
type Options = {
|
||||
@@ -48,7 +48,11 @@ export const buildApiReports = async (paths: string[] = [], opts: Options) => {
|
||||
const omitMessages = parseArrayOption(opts.omitMessages);
|
||||
|
||||
const isAllPackages = !paths?.length;
|
||||
const selectedPackageDirs = await getMatchingWorkspacePaths(paths);
|
||||
const selectedPackageDirs = await resolvePackagePaths({
|
||||
paths,
|
||||
include: opts.include,
|
||||
exclude: opts.exclude,
|
||||
});
|
||||
|
||||
if (isAllPackages && !isCiBuild && !isDocsBuild) {
|
||||
console.log('');
|
||||
|
||||
@@ -24,6 +24,16 @@ export function registerCommands(program: Command) {
|
||||
.option('--ci', 'CI run checks that there is no changes on API reports')
|
||||
.option('--tsc', 'executes the tsc compilation before extracting the APIs')
|
||||
.option('--docs', 'generates the api documentation')
|
||||
.option(
|
||||
'--include <pattern>',
|
||||
'Only include packages matching the provided patterns',
|
||||
(opt: string, opts: string[] = []) => [...opts, ...opt.split(',')],
|
||||
)
|
||||
.option(
|
||||
'--exclude <pattern>',
|
||||
'Exclude package matching the provided patterns',
|
||||
(opt: string, opts: string[] = []) => [...opts, ...opt.split(',')],
|
||||
)
|
||||
.option(
|
||||
'-a, --allow-warnings <allowWarningsPaths>',
|
||||
'continue processing packages after getting errors on selected packages Allows glob patterns and comma separated values (i.e. packages/core,plugins/core-*)',
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getMatchingWorkspacePaths } from '../../lib/paths';
|
||||
import { resolvePackagePaths } from '../../lib/paths';
|
||||
import pLimit from 'p-limit';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { paths as cliPaths } from '../../lib/paths';
|
||||
@@ -23,7 +23,7 @@ export async function runner(
|
||||
paths: string[],
|
||||
command: (dir: string) => Promise<void>,
|
||||
) {
|
||||
const packages = await getMatchingWorkspacePaths(paths);
|
||||
const packages = await resolvePackagePaths({ paths });
|
||||
const limit = pLimit(5);
|
||||
|
||||
const resultsList = await Promise.all(
|
||||
|
||||
@@ -14,105 +14,76 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath, join as joinPath, normalize } from 'path';
|
||||
import { resolvePackagePath, paths, findPackageDirs } from './paths';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { resolvePackagePaths } from './paths';
|
||||
|
||||
describe('paths', () => {
|
||||
jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue(normalize('/root'));
|
||||
jest.spyOn(paths, 'resolveTargetRoot').mockImplementation((...path) => {
|
||||
return resolvePath(normalize('/root'), ...path);
|
||||
describe('resolvePackages', () => {
|
||||
it('should return all packages', async () => {
|
||||
const paths = await resolvePackagePaths();
|
||||
expect(paths.length).toBeGreaterThan(10);
|
||||
expect(paths).toContain('packages/cli');
|
||||
expect(paths).toContain('packages/repo-tools');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[paths.targetRoot]: {
|
||||
'package.json': JSON.stringify({ name: 'test' }),
|
||||
packages: {
|
||||
'package-a': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
'package-b': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
'package-c': {},
|
||||
'README.md': 'Hello World',
|
||||
},
|
||||
plugins: {
|
||||
'plugin-a': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
'plugin-b': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
it('should filter by path', async () => {
|
||||
await expect(
|
||||
resolvePackagePaths({ paths: ['packages/repo-tools'] }),
|
||||
).resolves.toEqual(['packages/repo-tools']);
|
||||
|
||||
await expect(
|
||||
resolvePackagePaths({ paths: [resolvePath('packages/repo-tools')] }),
|
||||
).resolves.toEqual(['packages/repo-tools']);
|
||||
|
||||
await expect(
|
||||
resolvePackagePaths({
|
||||
paths: [resolvePath('packages/repo-tools/package.json')],
|
||||
}),
|
||||
).resolves.toEqual(['packages/repo-tools']);
|
||||
await expect(
|
||||
resolvePackagePaths({
|
||||
paths: ['packages/repo-tools/src/some/made/up/file.ts'],
|
||||
}),
|
||||
).resolves.toEqual(['packages/repo-tools']);
|
||||
await expect(
|
||||
resolvePackagePaths({
|
||||
paths: ['packages/repo-tools/src/some/made/up/file.ts', 'packages/cli'],
|
||||
}),
|
||||
).resolves.toEqual(['packages/cli', 'packages/repo-tools']);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
it('should filter with include', async () => {
|
||||
const allPackages = await resolvePackagePaths();
|
||||
const pluginPackages = await resolvePackagePaths({
|
||||
include: ['plugins/*'],
|
||||
});
|
||||
|
||||
expect(allPackages.length).toBeGreaterThan(10);
|
||||
expect(pluginPackages.length).toBeGreaterThan(10);
|
||||
expect(allPackages.length).toBeGreaterThan(pluginPackages.length);
|
||||
|
||||
expect(pluginPackages).toContain('plugins/catalog');
|
||||
|
||||
await expect(
|
||||
resolvePackagePaths({ include: ['packages/repo-t??ls'] }),
|
||||
).resolves.toEqual(['packages/repo-tools']);
|
||||
});
|
||||
|
||||
describe('resolvePackagePath', () => {
|
||||
it('should return undefined if the package does not exist or does not contain a package.json', async () => {
|
||||
expect(await resolvePackagePath('packages/package-d')).toBeUndefined();
|
||||
expect(await resolvePackagePath('packages/package-c')).toBeUndefined();
|
||||
});
|
||||
it('should return the path to the package if it exists and has a package.json', async () => {
|
||||
expect(await resolvePackagePath('packages/package-a')).toBe(
|
||||
joinPath('packages', 'package-a'),
|
||||
);
|
||||
expect(await resolvePackagePath('packages/package-b')).toBe(
|
||||
joinPath('packages', 'package-b'),
|
||||
);
|
||||
});
|
||||
it('should work with absolute paths', async () => {
|
||||
expect(await resolvePackagePath('/root/packages/package-a')).toBe(
|
||||
joinPath('packages', 'package-a'),
|
||||
);
|
||||
});
|
||||
it('should return undefined if the pat is not a directory', async () => {
|
||||
expect(await resolvePackagePath('packages/README.md')).toBeUndefined();
|
||||
it('should filter with exclude', async () => {
|
||||
const nonPluginPackages = await resolvePackagePaths({
|
||||
exclude: ['plugins/*'],
|
||||
});
|
||||
|
||||
expect(nonPluginPackages).toContain('packages/app');
|
||||
expect(nonPluginPackages).not.toContain('plugins/catalog');
|
||||
});
|
||||
describe('findPackageDirs', () => {
|
||||
it('should return only the given packages', async () => {
|
||||
expect(await findPackageDirs(['packages/package-a'])).toEqual([
|
||||
joinPath('packages', 'package-a'),
|
||||
]);
|
||||
});
|
||||
it('should return only the given packages when using glob patterns', async () => {
|
||||
expect(await findPackageDirs(['packages/*'])).toEqual([
|
||||
joinPath('packages', 'package-a'),
|
||||
joinPath('packages', 'package-b'),
|
||||
]);
|
||||
expect(await findPackageDirs(['packages/*', 'plugins/*'])).toEqual([
|
||||
joinPath('packages', 'package-a'),
|
||||
joinPath('packages', 'package-b'),
|
||||
joinPath('plugins', 'plugin-a'),
|
||||
joinPath('plugins', 'plugin-b'),
|
||||
]);
|
||||
});
|
||||
it('should return only the given packages when using absolute paths', async () => {
|
||||
expect(
|
||||
await findPackageDirs([
|
||||
'/root/packages/package-a',
|
||||
'/root/plugins/plugin-b',
|
||||
]),
|
||||
).toEqual([
|
||||
joinPath('packages', 'package-a'),
|
||||
joinPath('plugins', 'plugin-b'),
|
||||
]);
|
||||
});
|
||||
it('should return only the given packages when using absolute paths with glob patterns', async () => {
|
||||
expect(
|
||||
await findPackageDirs(['/root/packages/*', '/root/plugins/*-a']),
|
||||
).toEqual([
|
||||
joinPath('packages', 'package-a'),
|
||||
joinPath('packages', 'package-b'),
|
||||
joinPath('plugins', 'plugin-a'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should combine all options', async () => {
|
||||
await expect(
|
||||
resolvePackagePaths({
|
||||
paths: ['packages/cli', 'packages/backend', 'packages/app'],
|
||||
include: ['packages/app'],
|
||||
exclude: ['packages/back*'],
|
||||
}),
|
||||
).resolves.toEqual(['packages/app']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,90 +15,69 @@
|
||||
*/
|
||||
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { relative as relativePath, join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import g from 'glob';
|
||||
import isGlob from 'is-glob';
|
||||
|
||||
import { promisify } from 'util';
|
||||
|
||||
const glob = promisify(g);
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { isAbsolute, relative as relativePath } from 'path';
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
export const paths = findPaths(__dirname);
|
||||
|
||||
export async function resolvePackagePath(
|
||||
packagePath: string,
|
||||
): Promise<string | undefined> {
|
||||
const fullPackageDir = paths.resolveTargetRoot(packagePath);
|
||||
/** @internal */
|
||||
export interface ResolvePackagesOptions {
|
||||
paths?: string[];
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(fullPackageDir);
|
||||
if (!stat.isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
/** @internal */
|
||||
export async function resolvePackagePaths(
|
||||
options: ResolvePackagesOptions = {},
|
||||
): Promise<string[]> {
|
||||
const { paths: providedPaths, include, exclude } = options;
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const packageJsonPath = join(fullPackageDir, 'package.json');
|
||||
|
||||
await fs.access(packageJsonPath);
|
||||
} catch (e) {
|
||||
console.log(`folder omitted: ${fullPackageDir}, cause: ${e}`);
|
||||
return undefined;
|
||||
if (providedPaths && providedPaths.length > 0) {
|
||||
packages = packages.filter(({ dir }) =>
|
||||
providedPaths.some(
|
||||
path =>
|
||||
new Minimatch(path).match(relativePath(paths.targetRoot, dir)) ||
|
||||
isChildPath(dir, path),
|
||||
),
|
||||
);
|
||||
}
|
||||
return relativePath(paths.targetRoot, fullPackageDir);
|
||||
}
|
||||
|
||||
export async function findPackageDirs(selectedPaths: string[] = []) {
|
||||
const packageDirs = new Array<string>();
|
||||
for (const packageRoot of selectedPaths) {
|
||||
// if the path contain any glob notation we resolve all the paths to process one by one
|
||||
const dirs = isGlob(packageRoot)
|
||||
? await glob(packageRoot, { cwd: paths.targetRoot })
|
||||
: [packageRoot];
|
||||
for (const dir of dirs) {
|
||||
const packageDir = await resolvePackagePath(dir);
|
||||
if (!packageDir) {
|
||||
continue;
|
||||
}
|
||||
packageDirs.push(packageDir);
|
||||
}
|
||||
if (include) {
|
||||
packages = packages.filter(pkg =>
|
||||
include.some(pattern =>
|
||||
new Minimatch(pattern).match(relativePath(paths.targetRoot, pkg.dir)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return packageDirs;
|
||||
|
||||
if (exclude) {
|
||||
packages = packages.filter(pkg =>
|
||||
exclude.some(
|
||||
pattern =>
|
||||
!new Minimatch(pattern).match(
|
||||
relativePath(paths.targetRoot, pkg.dir),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return packages.map(pkg => relativePath(paths.targetRoot, pkg.dir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root.
|
||||
*
|
||||
* If the file does not exist, or the "workspaces" field is not present, returns `undefined`.
|
||||
*
|
||||
* @returns The list of package names, or `undefined` if not found.
|
||||
*/
|
||||
export async function getWorkspacePackagePathPatterns(): Promise<
|
||||
string[] | undefined
|
||||
> {
|
||||
const pkgJson = await fs
|
||||
.readJson(paths.resolveTargetRoot('package.json'))
|
||||
.catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
const workspaces = pkgJson?.workspaces?.packages;
|
||||
return workspaces;
|
||||
}
|
||||
/** @internal */
|
||||
export function isChildPath(base: string, path: string): boolean {
|
||||
const relative = relativePath(base, path);
|
||||
if (relative === '') {
|
||||
// The same directory
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of paths from the user, returns the listing package directories from the
|
||||
* workspace. Returns all directories if no paths are given.
|
||||
* @param cliPaths User given paths from CLI.
|
||||
* @returns Matching package directories or all if no cli paths passed in.
|
||||
*/
|
||||
export async function getMatchingWorkspacePaths(cliPaths: string[]) {
|
||||
const isAllPackages = !cliPaths?.length;
|
||||
const selectedPaths = isAllPackages
|
||||
? await getWorkspacePackagePathPatterns()
|
||||
: cliPaths;
|
||||
return await findPackageDirs(selectedPaths);
|
||||
const outsideBase = relative.startsWith('..'); // not outside base
|
||||
const differentDrive = isAbsolute(relative); // on Windows, this means dir is on a different drive from base.
|
||||
|
||||
return !outsideBase && !differentDrive;
|
||||
}
|
||||
|
||||
@@ -9472,6 +9472,7 @@ __metadata:
|
||||
"@apidevtools/swagger-parser": ^10.1.0
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli-common": "workspace:^"
|
||||
"@backstage/cli-node": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
|
||||
Reference in New Issue
Block a user