packages: add cli-common

This commit is contained in:
Patrik Oldsberg
2020-08-03 17:29:46 +02:00
parent bb24f5c448
commit 9306dc035c
6 changed files with 319 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { findPaths } from './paths';
export type { Paths } from './paths';
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { resolve as resolvePath } from 'path';
import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths';
describe('paths', () => {
it('findOwnDir and findOwnRootDir should find owns paths', () => {
const dir = findOwnDir(__dirname);
const root = findOwnRootDir(dir);
expect(dir).toBe(resolvePath(__dirname, '..'));
expect(root).toBe(resolvePath(__dirname, '../../..'));
});
it('findRootPath should find a root path', () => {
expect(findRootPath(__dirname, () => true)).toBe(
resolvePath(__dirname, '..'),
);
expect(findRootPath(__dirname, () => false)).toBeUndefined();
expect(
findRootPath(
__dirname,
path => path !== resolvePath(__dirname, '../package.json'),
),
).toBe(resolvePath(__dirname, '../../..'));
});
it('findPaths should find package paths', () => {
const dir = resolvePath(__dirname, '..');
const root = resolvePath(__dirname, '../../..');
const paths = findPaths(__dirname);
expect(paths.ownDir).toBe(dir);
expect(paths.ownRoot).toBe(root);
expect(paths.resolveOwn('./derp.txt')).toBe(resolvePath(dir, 'derp.txt'));
expect(paths.resolveOwnRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
expect(paths.targetDir).toBe(dir);
expect(paths.targetRoot).toBe(root);
expect(paths.resolveTarget('./derp.txt')).toBe(
resolvePath(dir, 'derp.txt'),
);
expect(paths.resolveTargetRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
});
it('findPaths should find mocked package paths', () => {
const mockCwd = resolvePath(__dirname, '../../core');
const mockDir = resolvePath(__dirname, '../../cli');
const root = resolvePath(__dirname, '../../..');
jest.spyOn(process, 'cwd').mockReturnValue(mockCwd);
const paths = findPaths(resolvePath(mockDir, 'src/lib'));
expect(paths.ownDir).toBe(mockDir);
expect(paths.ownRoot).toBe(root);
expect(paths.resolveOwn('./derp.txt')).toBe(
resolvePath(mockDir, 'derp.txt'),
);
expect(paths.resolveOwnRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
expect(paths.targetDir).toBe(mockCwd);
expect(paths.targetRoot).toBe(root);
expect(paths.resolveTarget('./derp.txt')).toBe(
resolvePath(mockCwd, 'derp.txt'),
);
expect(paths.resolveTargetRoot('./derp.txt')).toBe(
resolvePath(root, 'derp.txt'),
);
});
});
+155
View File
@@ -0,0 +1,155 @@
/*
* Copyright 2020 Spotify AB
*
* 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 fs from 'fs';
import { dirname, resolve as resolvePath } from 'path';
export type ResolveFunc = (...paths: string[]) => string;
// Common paths and resolve functions used by the cli.
// Currently assumes it is being executed within a monorepo.
export type Paths = {
// Root dir of the cli itself, containing package.json
ownDir: string;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: string;
// The location of the app that the cli is being executed in
targetDir: string;
// The monorepo root package of the app that the cli is being executed in.
targetRoot: string;
// Resolve a path relative to own repo
resolveOwn: ResolveFunc;
// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: ResolveFunc;
// Resolve a path relative to the app
resolveTarget: ResolveFunc;
// Resolve a path relative to the app repo root
resolveTargetRoot: ResolveFunc;
};
// Looks for a package.json with a workspace config to identify the root of the monorepo
export function findRootPath(
searchDir: string,
filterFunc: (pkgJsonPath: string) => boolean,
): string | undefined {
let path = searchDir;
// Some sanity check to avoid infinite loop
for (let i = 0; i < 1000; i++) {
const packagePath = resolvePath(path, 'package.json');
const exists = fs.existsSync(packagePath);
if (exists && filterFunc(packagePath)) {
return path;
}
const newPath = dirname(path);
if (newPath === path) {
return undefined;
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for root package.json at ${searchDir}`,
);
}
// Finds the root of a given package
export function findOwnDir(searchDir: string) {
const path = findRootPath(searchDir, () => true);
if (!path) {
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
}
return path;
}
// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo.
export function findOwnRootDir(ownDir: string) {
const isLocal = fs.existsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
);
}
return resolvePath(ownDir, '../..');
}
/**
* Find paths related to a package and its execution context.
*
* @example
*
* const paths = findPaths(__dirname)
*/
export function findPaths(searchDir: string): Paths {
const ownDir = findOwnDir(searchDir);
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootDir(ownDir);
}
return ownRoot;
};
// We're not always running in a monorepo, so we lazy init this to only crash commands
// that require a monorepo when we're not in one.
let targetRoot = '';
const getTargetRoot = () => {
if (!targetRoot) {
targetRoot =
findRootPath(targetDir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces?.packages);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? targetDir; // We didn't find any root package.json, assume we're not in a monorepo
}
return targetRoot;
};
return {
ownDir,
get ownRoot() {
return getOwnRoot();
},
targetDir,
get targetRoot() {
return getTargetRoot();
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}