Address PR review comments

- Refactor targetPaths/findOwnPaths to class-based implementations with
  unified caching and .dir/.rootDir properties alongside resolve methods
- Replace jest.mock with jest.spyOn in plugin-manager.test.ts
- Remove paths compatibility wrapper from repo-tools, migrate all internal
  consumers to use targetPaths from @backstage/cli-common directly
- Fix changeset package name (@techdocs/cli not @backstage/techdocs-cli)
- Add migration examples to cli-common changeset
- Update API report for cli-common

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-22 13:47:49 +01:00
parent 70fc178697
commit 07816d67f3
68 changed files with 394 additions and 321 deletions
+23
View File
@@ -3,3 +3,26 @@
---
Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths.
To migrate existing `findPaths` usage:
```ts
// Before
import { findPaths } from '@backstage/cli-common';
const paths = findPaths(__dirname);
// After — for target project paths (cwd-based):
import { targetPaths } from '@backstage/cli-common';
// paths.targetDir → targetPaths.dir
// paths.targetRoot → targetPaths.rootDir
// paths.resolveTarget('src') → targetPaths.resolve('src')
// paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock')
// After — for package-relative paths:
import { findOwnPaths } from '@backstage/cli-common';
const own = findOwnPaths(__dirname);
// paths.ownDir → own.dir
// paths.ownRoot → own.rootDir
// paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js')
// paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json')
```
+1 -1
View File
@@ -5,7 +5,7 @@
'@backstage/config-loader': patch
'@backstage/create-app': patch
'@backstage/repo-tools': patch
'@backstage/techdocs-cli': patch
'@techdocs/cli': patch
---
Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
@@ -40,37 +40,24 @@ import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils';
import { PluginScanner } from '../scanner/plugin-scanner';
import { targetPaths } from '@backstage/cli-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
jest.mock('@backstage/cli-common', () => {
const path = require('path');
const actual = jest.requireActual<typeof import('@backstage/cli-common')>(
'@backstage/cli-common',
);
const mockRoot = path.resolve(__dirname, '..', '..', '..', '..');
return {
...actual,
targetPaths: {
resolve: (...paths: string[]) => path.join(mockRoot, ...paths),
resolveRoot: (...paths: string[]) => path.join(mockRoot, ...paths),
},
findPaths: (searchDir: string) => ({
targetRoot: mockRoot,
targetDir: mockRoot,
ownDir: path.dirname(searchDir),
ownRoot: mockRoot,
resolveOwn: (...p: string[]) => path.join(path.dirname(searchDir), ...p),
resolveOwnRoot: (...p: string[]) => path.join(mockRoot, ...p),
resolveTarget: (...p: string[]) => path.join(mockRoot, ...p),
resolveTargetRoot: (...p: string[]) => path.join(mockRoot, ...p),
}),
};
});
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { BackstagePackageJson, PackageRole } from '@backstage/cli-node';
describe('backend-dynamic-feature-service', () => {
const mockDir = createMockDirectory();
beforeAll(() => {
jest
.spyOn(targetPaths, 'resolveRoot')
.mockImplementation((...paths: string[]) =>
require('path').resolve(__dirname, '../../../..', ...paths),
);
});
afterAll(() => {
jest.restoreAllMocks();
});
describe('loadPlugins', () => {
afterEach(() => {
jest.resetModules();
@@ -1069,7 +1056,7 @@ describe('backend-dynamic-feature-service', () => {
otherMockDir.resolve('a-dynamic-plugin'),
);
expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith(
targetPaths.resolveRoot(),
targetPaths.rootDir,
[realPath],
new Map<string, ScannedPluginManifest>([
[
@@ -56,7 +56,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
options: DynamicPluginManagerOptions,
): Promise<DynamicPluginManager> {
/* eslint-disable-next-line no-restricted-syntax */
const backstageRoot = targetPaths.resolveRoot();
const backstageRoot = targetPaths.rootDir;
const scanner = PluginScanner.create({
config: options.config,
logger: options.logger,
@@ -100,7 +100,7 @@ const dynamicPluginsSchemasServiceFactoryWithOptions = (
config,
logger,
// eslint-disable-next-line no-restricted-syntax
backstageRoot: targetPaths.resolveRoot(),
backstageRoot: targetPaths.rootDir,
preferAlpha: true,
});
+22
View File
@@ -21,12 +21,23 @@ export class ExitCodeError extends CustomErrorBase {
}
// @public
export function findOwnPaths(searchDir: string): OwnPaths;
// @public @deprecated
export function findPaths(searchDir: string): Paths;
// @public
export function isChildPath(base: string, path: string): boolean;
// @public
export type OwnPaths = {
dir: string;
rootDir: string;
resolve: ResolveFunc;
resolveRoot: ResolveFunc;
};
// @public @deprecated
export type Paths = {
ownDir: string;
ownRoot: string;
@@ -68,4 +79,15 @@ export function runOutput(
args: string[],
options?: RunOptions,
): Promise<string>;
// @public
export type TargetPaths = {
dir: string;
rootDir: string;
resolve: ResolveFunc;
resolveRoot: ResolveFunc;
};
// @public
export const targetPaths: TargetPaths;
```
+140 -115
View File
@@ -33,6 +33,12 @@ export type ResolveFunc = (...paths: string[]) => string;
* @public
*/
export type TargetPaths = {
/** The target package directory. */
dir: string;
/** The target monorepo root directory. */
rootDir: string;
/** Resolve a path relative to the target package directory. */
resolve: ResolveFunc;
@@ -46,6 +52,12 @@ export type TargetPaths = {
* @public
*/
export type OwnPaths = {
/** The package root directory. */
dir: string;
/** The monorepo root directory containing the package. */
rootDir: string;
/** Resolve a path relative to the package root. */
resolve: ResolveFunc;
@@ -98,48 +110,12 @@ export function findRootPath(
);
}
// Hierarchical cache for ownDir lookups. When we resolve a searchDir to its
// package root, we also cache every intermediate directory along the way. This
// means sibling directories only need to walk up until they hit a cached ancestor.
const ownDirCache = new Map<string, string>();
// Finds the root of a given package
export function findOwnDir(searchDir: string) {
const visited: string[] = [];
let dir = searchDir;
for (let i = 0; i < 1000; i++) {
const cached = ownDirCache.get(dir);
if (cached !== undefined) {
for (const d of visited) {
ownDirCache.set(d, cached);
}
return cached;
}
visited.push(dir);
const packagePath = resolvePath(dir, 'package.json');
if (fs.existsSync(packagePath)) {
for (const d of visited) {
ownDirCache.set(d, dir);
}
return dir;
}
const newDir = dirname(dir);
if (newDir === dir) {
break;
}
dir = newDir;
}
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
return OwnPathsImpl.findDir(searchDir);
}
// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo.
// Finds the root of the monorepo that the package exists in.
export function findOwnRootDir(ownDir: string) {
const isLocal = fs.existsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
@@ -151,46 +127,131 @@ export function findOwnRootDir(ownDir: string) {
return resolvePath(ownDir, '../..');
}
// Cached target path state. Re-resolves when process.cwd() changes.
let cachedTargetCwd: string | undefined;
let cachedTargetDir: string | undefined;
let cachedTargetRoot: string | undefined;
// Hierarchical directory cache shared across all OwnPathsImpl instances.
// When we resolve a searchDir to its package root, we also cache every
// intermediate directory, so sibling directories share work.
const dirCache = new Map<string, string>();
function getTargetDir(): string {
const cwd = process.cwd();
if (cachedTargetDir !== undefined && cachedTargetCwd === cwd) {
return cachedTargetDir;
class OwnPathsImpl implements OwnPaths {
static #instanceCache = new Map<string, OwnPathsImpl>();
static find(searchDir: string): OwnPathsImpl {
const dir = OwnPathsImpl.findDir(searchDir);
let instance = OwnPathsImpl.#instanceCache.get(dir);
if (!instance) {
instance = new OwnPathsImpl(dir);
OwnPathsImpl.#instanceCache.set(dir, instance);
}
return instance;
}
cachedTargetCwd = cwd;
cachedTargetRoot = undefined;
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
cachedTargetDir = fs
.realpathSync(cwd)
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
return cachedTargetDir;
static findDir(searchDir: string): string {
const visited: string[] = [];
let dir = searchDir;
for (let i = 0; i < 1000; i++) {
const cached = dirCache.get(dir);
if (cached !== undefined) {
for (const d of visited) {
dirCache.set(d, cached);
}
return cached;
}
visited.push(dir);
if (fs.existsSync(resolvePath(dir, 'package.json'))) {
for (const d of visited) {
dirCache.set(d, dir);
}
return dir;
}
const newDir = dirname(dir);
if (newDir === dir) {
break;
}
dir = newDir;
}
throw new Error(
`No package.json found while searching for package root of ${searchDir}`,
);
}
#dir: string;
#rootDir: string | undefined;
private constructor(dir: string) {
this.#dir = dir;
}
get dir(): string {
return this.#dir;
}
get rootDir(): string {
this.#rootDir ??= findOwnRootDir(this.#dir);
return this.#rootDir;
}
resolve = (...paths: string[]): string => {
return resolvePath(this.#dir, ...paths);
};
resolveRoot = (...paths: string[]): string => {
return resolvePath(this.rootDir, ...paths);
};
}
function getTargetRoot(): string {
// Ensure targetDir is fresh, which also invalidates targetRoot on cwd change
const targetDir = getTargetDir();
if (cachedTargetRoot !== undefined) {
return cachedTargetRoot;
class TargetPathsImpl implements TargetPaths {
#cwd: string | undefined;
#dir: string | undefined;
#rootDir: string | undefined;
get dir(): string {
const cwd = process.cwd();
if (this.#dir !== undefined && this.#cwd === cwd) {
return this.#dir;
}
this.#cwd = cwd;
this.#rootDir = undefined;
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
this.#dir = fs
.realpathSync(cwd)
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
return this.#dir;
}
// 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.
cachedTargetRoot =
findRootPath(targetDir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? targetDir;
return cachedTargetRoot;
get rootDir(): string {
// Access dir first to ensure cwd is fresh, which also invalidates rootDir on cwd change
const dir = this.dir;
if (this.#rootDir !== undefined) {
return this.#rootDir;
}
// Lazy init to only crash commands that require a monorepo when we're not in one
this.#rootDir =
findRootPath(dir, path => {
try {
const content = fs.readFileSync(path, 'utf8');
const data = JSON.parse(content);
return Boolean(data.workspaces);
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}) ?? dir;
return this.#rootDir;
}
resolve = (...paths: string[]): string => {
return resolvePath(this.dir, ...paths);
};
resolveRoot = (...paths: string[]): string => {
return resolvePath(this.rootDir, ...paths);
};
}
/**
@@ -198,18 +259,8 @@ function getTargetRoot(): string {
* for cwd-based path resolution without needing `__dirname`.
*
* @public
* @example
*
* import { targetPaths } from '\@backstage/cli-common';
*
* const lockfile = targetPaths.resolveRoot('yarn.lock');
*/
export const targetPaths: TargetPaths = {
resolve: (...paths) => resolvePath(getTargetDir(), ...paths),
resolveRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
const ownPathsCache = new Map<string, OwnPaths>();
export const targetPaths: TargetPaths = new TargetPathsImpl();
/**
* Find paths relative to the package that the calling code lives in.
@@ -219,35 +270,9 @@ const ownPathsCache = new Map<string, OwnPaths>();
* subdirectories within the same package share work.
*
* @public
* @example
*
* import { findOwnPaths } from '\@backstage/cli-common';
*
* const own = findOwnPaths(__dirname);
* const config = own.resolve('config/jest.js');
*/
export function findOwnPaths(searchDir: string): OwnPaths {
const ownDir = findOwnDir(searchDir);
const cached = ownPathsCache.get(ownDir);
if (cached) {
return cached;
}
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootDir(ownDir);
}
return ownRoot;
};
const paths: OwnPaths = {
resolve: (...p) => resolvePath(ownDir, ...p),
resolveRoot: (...p) => resolvePath(getOwnRoot(), ...p),
};
ownPathsCache.set(ownDir, paths);
return paths;
return OwnPathsImpl.find(searchDir);
}
/**
@@ -265,16 +290,16 @@ export function findPaths(searchDir: string): Paths {
const own = findOwnPaths(searchDir);
return {
get ownDir() {
return own.resolve();
return own.dir;
},
get ownRoot() {
return own.resolveRoot();
return own.rootDir;
},
get targetDir() {
return targetPaths.resolve();
return targetPaths.dir;
},
get targetRoot() {
return targetPaths.resolveRoot();
return targetPaths.rootDir;
},
resolveOwn: own.resolve,
resolveOwnRoot: own.resolveRoot,
+4 -4
View File
@@ -21,16 +21,16 @@ const ownPaths = findOwnPaths(__dirname);
/* eslint-disable-next-line no-restricted-syntax */
export const paths = {
get ownDir() {
return ownPaths.resolve();
return ownPaths.dir;
},
get ownRoot() {
return ownPaths.resolveRoot();
return ownPaths.rootDir;
},
get targetDir() {
return targetPaths.resolve();
return targetPaths.dir;
},
get targetRoot() {
return targetPaths.resolveRoot();
return targetPaths.rootDir;
},
resolveOwn: ownPaths.resolve,
resolveOwnRoot: ownPaths.resolveRoot,
+1 -1
View File
@@ -32,7 +32,7 @@ export class SuccessCache {
* location.
*/
static trimPaths(input: string) {
return input.replaceAll(targetPaths.resolveRoot(), '');
return input.replaceAll(targetPaths.rootDir, '');
}
constructor(name: string, basePath?: string) {
@@ -47,14 +47,14 @@ export async function command(opts: OptionValues): Promise<void> {
if (role === 'frontend') {
return buildFrontend({
targetDir: targetPaths.resolve(),
targetDir: targetPaths.dir,
configPaths,
writeStats: Boolean(opts.stats),
webpack,
});
}
return buildBackend({
targetDir: targetPaths.resolve(),
targetDir: targetPaths.dir,
configPaths,
skipBuildDependencies: Boolean(opts.skipBuildDependencies),
minify: Boolean(opts.minify),
@@ -77,7 +77,7 @@ export async function command(opts: OptionValues): Promise<void> {
if (isModuleFederationRemote) {
console.log('Building package as a module federation remote');
return buildFrontend({
targetDir: targetPaths.resolve(),
targetDir: targetPaths.dir,
configPaths: [],
writeStats: Boolean(opts.stats),
isModuleFederationRemote,
@@ -25,7 +25,7 @@ export async function command(opts: OptionValues): Promise<void> {
await startPackage({
role: await findRoleFromCommand(opts),
entrypoint: opts.entrypoint,
targetDir: targetPaths.resolve(),
targetDir: targetPaths.dir,
configPaths: opts.config as string[],
checksEnabled: Boolean(opts.check),
linkedWorkspace: await resolveLinkedWorkspace(opts.link),
@@ -44,7 +44,7 @@ export async function startBackend(options: StartBackendOptions) {
export async function startBackendPlugin(options: StartBackendOptions) {
const hasDevIndexEntry = await fs.pathExists(
resolvePath(options.targetDir ?? targetPaths.resolve(), 'dev/index.ts'),
resolvePath(options.targetDir ?? targetPaths.dir, 'dev/index.ts'),
);
if (!hasDevIndexEntry) {
console.warn(
@@ -39,7 +39,7 @@ interface StartAppOptions {
export async function startFrontend(options: StartAppOptions) {
const packageJson = (await readJson(
resolvePath(options.targetDir ?? targetPaths.resolve(), 'package.json'),
resolvePath(options.targetDir ?? targetPaths.dir, 'package.json'),
)) as BackstagePackageJson;
if (!hasReactDomClient()) {
@@ -59,7 +59,7 @@ export async function startFrontend(options: StartAppOptions) {
moduleFederationRemote: options.isModuleFederationRemote
? await getModuleFederationRemoteOptions(
packageJson,
resolvePath(targetPaths.resolve()),
resolvePath(targetPaths.dir),
)
: undefined,
});
@@ -90,7 +90,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
targetDir: pkg.dir,
packageJson: pkg.packageJson,
outputs,
logPrefix: `${chalk.cyan(relativePath(targetPaths.resolveRoot(), pkg.dir))}: `,
logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `,
workspacePackages: packages,
minify: opts.minify ?? buildOptions.minify,
};
@@ -117,7 +117,7 @@ export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
const targetDir = options.targetDir ?? targetPaths.resolve();
const targetDir = options.targetDir ?? targetPaths.dir;
let targetPkg = options.packageJson;
if (!targetPkg) {
@@ -287,7 +287,7 @@ export async function makeRollupConfigs(
e.name,
targetPaths.resolveRoot(
'dist-types',
relativePath(targetPaths.resolveRoot(), targetDir),
relativePath(targetPaths.rootDir, targetDir),
e.path.replace(/\.(?:ts|tsx)$/, '.d.ts'),
),
]),
@@ -34,7 +34,7 @@ export function formatErrorMessage(error: any) {
msg += `\n\n`;
for (const { text, location } of error.errors) {
const { line, column } = location;
const path = relativePath(targetPaths.resolve(), error.id);
const path = relativePath(targetPaths.dir, error.id);
const loc = chalk.cyan(`${path}:${line}:${column}`);
if (text === 'Unexpected "<"' && error.id.endsWith('.js')) {
@@ -107,7 +107,7 @@ export const buildPackage = async (options: BuildOptions) => {
const rollupConfigs = await makeRollupConfigs(options);
const targetDir = options.targetDir ?? targetPaths.resolve();
const targetDir = options.targetDir ?? targetPaths.dir;
await fs.remove(resolvePath(targetDir, 'dist'));
const buildTasks = rollupConfigs.map(rollupBuild);
@@ -20,7 +20,7 @@ import { targetPaths } from '@backstage/cli-common';
export function hasReactDomClient() {
try {
require.resolve('react-dom/client', {
paths: [targetPaths.resolve()],
paths: [targetPaths.dir],
});
return true;
} catch {
@@ -53,7 +53,7 @@ export async function createWorkspaceLinkingPlugins(
/^react(?:-router)?(?:-dom)?$/,
resource => {
if (!relativePath(linkedRoot.dir, resource.context).startsWith('..')) {
resource.context = targetPaths.resolve();
resource.context = targetPaths.dir;
}
},
),
@@ -147,7 +147,7 @@ export async function createDetectedModulesEntryPoint(options: {
// Previous versions of the CLI would write the detected modules file to the
// root `node_modules`, this makes sure that doesn't exist to minimize risk of conflicts
const legacyDetectedModulesPath = joinPath(
targetPaths.resolveRoot(),
targetPaths.rootDir,
'node_modules',
`${DETECTED_MODULES_MODULE_NAME}.js`,
);
@@ -21,14 +21,14 @@ import { targetPaths, findOwnPaths } from '@backstage/cli-common';
export type BundlingPathsOptions = {
// bundle entrypoint, e.g. 'src/index'
entry: string;
// Target directory, defaulting to targetPaths.resolve()
// Target directory, defaulting to targetPaths.dir
targetDir?: string;
// Relative dist directory, defaulting to 'dist'
dist?: string;
};
export function resolveBundlingPaths(options: BundlingPathsOptions) {
const { entry, targetDir = targetPaths.resolve() } = options;
const { entry, targetDir = targetPaths.dir } = options;
const resolveTargetModule = (pathString: string) => {
for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) {
@@ -70,7 +70,7 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
targetTsConfig: targetPaths.resolveRoot('tsconfig.json'),
targetPackageJson: resolvePath(targetDir, 'package.json'),
rootNodeModules: targetPaths.resolveRoot('node_modules'),
root: targetPaths.resolveRoot(),
root: targetPaths.rootDir,
};
}
@@ -54,7 +54,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
checkReactVersion();
const { name } = await fs.readJson(
resolvePath(options.targetDir ?? targetPaths.resolve(), 'package.json'),
resolvePath(options.targetDir ?? targetPaths.dir, 'package.json'),
);
let devServer: RspackDevServer | undefined = undefined;
@@ -272,7 +272,7 @@ function checkReactVersion() {
try {
// Make sure we're looking at the root of the target repo
const reactPkgPath = require.resolve('react/package.json', {
paths: [targetPaths.resolveRoot()],
paths: [targetPaths.rootDir],
});
const reactPkg = require(reactPkgPath);
if (reactPkg.version.startsWith('16.')) {
@@ -211,7 +211,7 @@ export async function createDistWorkspace(
targetDir: pkg.dir,
packageJson: pkg.packageJson,
outputs: outputs,
logPrefix: `${chalk.cyan(relativePath(targetPaths.resolveRoot(), pkg.dir))}: `,
logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `,
minify: options.minify,
workspacePackages: packages,
});
@@ -252,7 +252,7 @@ export async function createDistWorkspace(
if (options.skeleton) {
const skeletonFiles = targets
.map(target => {
const dir = relativePath(targetPaths.resolveRoot(), target.dir);
const dir = relativePath(targetPaths.rootDir, target.dir);
return joinPath(dir, 'package.json');
})
.sort();
@@ -301,7 +301,7 @@ async function moveToDistWorkspace(
fastPackPackages.map(async target => {
console.log(`Moving ${target.name} into dist workspace`);
const outputDir = relativePath(targetPaths.resolveRoot(), target.dir);
const outputDir = relativePath(targetPaths.rootDir, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await productionPack({
packageDir: target.dir,
@@ -321,7 +321,7 @@ async function moveToDistWorkspace(
cwd: target.dir,
}).waitForExit();
const outputDir = relativePath(targetPaths.resolveRoot(), target.dir);
const outputDir = relativePath(targetPaths.rootDir, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
@@ -23,6 +23,12 @@ const mockDir = createMockDirectory();
jest.mock('@backstage/cli-common', () => ({
...jest.requireActual('@backstage/cli-common'),
targetPaths: {
get dir() {
return mockDir.path;
},
get rootDir() {
return mockDir.path;
},
resolve: (...args: string[]) => mockDir.resolve(...args),
resolveRoot: (...args: string[]) => mockDir.resolve(...args),
},
@@ -35,7 +35,7 @@ type Options = {
};
export async function loadCliConfig(options: Options) {
const targetDir = options.targetDir ?? targetPaths.resolve();
const targetDir = options.targetDir ?? targetPaths.dir;
// Consider all packages in the monorepo when loading in config
const { packages } = await getPackages(targetDir);
@@ -74,7 +74,7 @@ export async function loadCliConfig(options: Options) {
? async name => process.env[name] || 'x'
: undefined,
watch: Boolean(options.watch),
rootDir: targetPaths.resolveRoot(),
rootDir: targetPaths.rootDir,
argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]),
});
@@ -84,7 +84,7 @@ export default async (options: InfoOptions) => {
const lockfilePath = targetPaths.resolveRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const targetPath = targetPaths.resolveRoot();
const targetPath = targetPaths.rootDir;
// Get workspace package names and their versions
const workspacePackages = new Map<string, string>();
@@ -22,7 +22,7 @@ import { ESLint } from 'eslint';
export default async (directories: string[], opts: OptionValues) => {
const eslint = new ESLint({
cwd: targetPaths.resolve(),
cwd: targetPaths.dir,
fix: opts.fix,
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
});
@@ -48,7 +48,7 @@ export default async (directories: string[], opts: OptionValues) => {
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
if (opts.format === 'eslint-formatter-friendly') {
process.chdir(targetPaths.resolveRoot());
process.chdir(targetPaths.rootDir);
}
const resultText = await formatter.format(results);
@@ -63,7 +63,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
if (opts.format === 'eslint-formatter-friendly') {
process.chdir(targetPaths.resolveRoot());
process.chdir(targetPaths.rootDir);
}
// Make sure lint output is colored unless the user explicitly disabled it
@@ -78,7 +78,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const lintOptions = parseLintScript(pkg.packageJson.scripts?.lint);
const base = {
fullDir: pkg.dir,
relativeDir: relativePath(targetPaths.resolveRoot(), pkg.dir),
relativeDir: relativePath(targetPaths.rootDir, pkg.dir),
lintOptions,
parentHash: undefined,
};
@@ -114,7 +114,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
shouldCache: Boolean(cacheContext),
maxWarnings: opts.maxWarnings ?? -1,
successCache: cacheContext?.entries,
rootDir: targetPaths.resolveRoot(),
rootDir: targetPaths.rootDir,
},
workerFactory: async ({
fix,
@@ -26,16 +26,16 @@ import { createTypeDistProject } from '../../../../lib/typeDistProject';
export const pre = async () => {
publishPreflightCheck({
dir: targetPaths.resolve(),
dir: targetPaths.dir,
packageJson: await fs.readJson(targetPaths.resolve('package.json')),
});
await productionPack({
packageDir: targetPaths.resolve(),
packageDir: targetPaths.dir,
featureDetectionProject: await createTypeDistProject(),
});
};
export const post = async () => {
await revertProductionPack(targetPaths.resolve());
await revertProductionPack(targetPaths.dir);
};
@@ -230,7 +230,7 @@ export function createRepositoryFieldFixer() {
return (pkg: FixablePackage) => {
const expectedPath = posix.join(
rootDir,
relativePath(targetPaths.resolveRoot(), pkg.dir),
relativePath(targetPaths.rootDir, pkg.dir),
);
const repoField = pkg.packageJson.repository;
@@ -319,7 +319,7 @@ export function fixPluginId(pkg: FixablePackage) {
role === 'backend-plugin-module')
) {
const path = relativePath(
targetPaths.resolveRoot(),
targetPaths.rootDir,
resolvePath(pkg.dir, 'package.json'),
);
const msg = `Failed to guess plugin ID for "${pkg.packageJson.name}", please set the 'backstage.pluginId' field manually in "${path}"`;
@@ -415,7 +415,7 @@ export function fixPluginPackages(
return;
}
const path = relativePath(
targetPaths.resolveRoot(),
targetPaths.rootDir,
resolvePath(pkg.dir, 'package.json'),
);
const suggestedRole =
@@ -464,7 +464,7 @@ export function fixPeerModules(pkg: FixablePackage) {
}
const packagePath = relativePath(
targetPaths.resolveRoot(),
targetPaths.rootDir,
resolvePath(pkg.dir, 'package.json'),
);
@@ -26,7 +26,7 @@ export async function command(opts: OptionValues) {
const packages = await PackageGraph.listTargetPackages();
const eslint = new ESLint({
cwd: targetPaths.resolve(),
cwd: targetPaths.dir,
overrideConfig: {
plugins: ['deprecation'],
rules: {
@@ -53,7 +53,7 @@ export async function command(opts: OptionValues) {
continue;
}
const path = relativePath(targetPaths.resolveRoot(), result.filePath);
const path = relativePath(targetPaths.rootDir, result.filePath);
deprecations.push({
path,
message: message.message,
@@ -22,7 +22,7 @@ import { targetPaths } from '@backstage/cli-common';
export default async () => {
const { packages } = await getPackages(targetPaths.resolve());
const { packages } = await getPackages(targetPaths.dir);
await Promise.all(
packages.map(async ({ dir, packageJson: pkg }) => {
@@ -65,6 +65,12 @@ jest.mock('@backstage/cli-common', () => {
return {
...actual,
targetPaths: {
get dir() {
return mockDir.path;
},
get rootDir() {
return mockDir.path;
},
resolve: (...args: string[]) => mockDir.resolve(...args),
resolveRoot: (...args: string[]) => mockDir.resolve(...args),
},
@@ -141,7 +141,7 @@ export default async (opts: OptionValues) => {
}
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(targetPaths.resolve(), pattern);
const dependencyMap = await mapDependencies(targetPaths.dir, pattern);
// Next check with the package registry to see which dependency ranges we need to bump
const versionBumps = new Map<string, PkgVersionInfo[]>();
@@ -38,6 +38,12 @@ jest.mock('@backstage/cli-common', () => {
return {
...actual,
targetPaths: {
get dir() {
return mockDir.path;
},
get rootDir() {
return mockDir.path;
},
resolve: (...args: string[]) => mockDir.resolve(...args),
resolveRoot: (...args: string[]) => mockDir.resolve(...args),
},
@@ -83,7 +83,7 @@ export async function addCodeownersEntry(
let filePath = codeownersFilePath;
if (!filePath) {
filePath = await getCodeownersFilePath(targetPaths.resolveRoot());
filePath = await getCodeownersFilePath(targetPaths.rootDir);
if (!filePath) {
return false;
}
@@ -39,7 +39,7 @@ export async function collectPortableTemplateInput(
): Promise<PortableTemplateInput> {
const { config, template, prefilledParams } = options;
const codeOwnersFilePath = await getCodeownersFilePath(targetPaths.resolveRoot());
const codeOwnersFilePath = await getCodeownersFilePath(targetPaths.rootDir);
const prompts = getPromptsForRole(template.role);
@@ -68,7 +68,7 @@ interface TestGlobal extends Global {
async function readPackageTreeHashes(graph: PackageGraph) {
const pkgs = Array.from(graph.values()).map(pkg => ({
...pkg,
path: relativePath(targetPaths.resolveRoot(), pkg.dir),
path: relativePath(targetPaths.rootDir, pkg.dir),
}));
const output = await runOutput([
'git',
@@ -157,7 +157,7 @@ export class ConfigSources {
static defaultForTargets(
options: ConfigSourcesDefaultForTargetsOptions,
): ConfigSource {
const rootDir = options.rootDir ?? targetPaths.resolveRoot();
const rootDir = options.rootDir ?? targetPaths.rootDir;
const argSources = options.targets.map(arg => {
if (arg.type === 'url') {
@@ -41,6 +41,12 @@ jest.mock('@backstage/cli-common', () => {
findPaths: jest.fn(),
findOwnPaths: () => mockOwnPaths,
targetPaths: {
get dir() {
return MOCK_TARGET_DIR;
},
get rootDir() {
return '/mock/target-root';
},
resolve: (...paths: string[]) =>
pathModule.resolve(MOCK_TARGET_DIR, ...paths),
resolveRoot: (...paths: string[]) =>
+3 -3
View File
@@ -76,8 +76,8 @@ export default async (opts: OptionValues): Promise<void> => {
// Use `--path` argument as application directory when specified, otherwise
// create a directory using `answers.name`
const appDir = opts.path
? resolvePath(targetPaths.resolve(), opts.path)
: resolvePath(targetPaths.resolve(), answers.name);
? resolvePath(targetPaths.dir, opts.path)
: resolvePath(targetPaths.dir, answers.name);
Task.log();
Task.log('Creating the app...');
@@ -100,7 +100,7 @@ export default async (opts: OptionValues): Promise<void> => {
// Template to temporary location, and then move files
Task.section('Checking if the directory is available');
await checkAppExistsTask(targetPaths.resolve(), answers.name);
await checkAppExistsTask(targetPaths.dir, answers.name);
Task.section('Creating a temporary app directory');
const tempDir = await fs.mkdtemp(resolvePath(os.tmpdir(), answers.name));
@@ -16,10 +16,10 @@
import fs from 'fs-extra';
import { join } from 'node:path';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
export async function createTemporaryTsConfig(includedPackageDirs: string[]) {
const path = cliPaths.resolveTargetRoot('tsconfig.tmp.json');
const path = targetPaths.resolveRoot('tsconfig.tmp.json');
process.once('exit', () => {
fs.removeSync(path);
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { run, ExitCodeError } from '@backstage/cli-common';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
/**
* Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file.
@@ -29,7 +29,7 @@ import { paths as cliPaths } from '../../../lib/paths';
* @returns {Promise<void>} A promise that resolves when the declaration files have been generated.
*/
export async function generateTypeDeclarations(tsconfigFilePath: string) {
await fs.remove(cliPaths.resolveTargetRoot('dist-types'));
await fs.remove(targetPaths.resolveRoot('dist-types'));
try {
await run(
[
@@ -43,7 +43,7 @@ export async function generateTypeDeclarations(tsconfigFilePath: string) {
'false',
],
{
cwd: cliPaths.targetRoot,
cwd: targetPaths.rootDir,
},
).waitForExit();
} catch (error) {
@@ -18,7 +18,7 @@ import { ExtractorMessage } from '@microsoft/api-extractor';
import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration';
import { Program } from 'typescript';
import { tryRunPrettier } from '../common';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
let applied = false;
@@ -162,7 +162,7 @@ export function patchApiReportGeneration() {
parser: 'markdown',
// We need a real-looking filepath for proper config resolution, not just a directory
// Ideally, the real filepath would be better, but it would require too much patching, for very little gain.
filepath: `${cliPaths.targetRoot}/report.api.md`,
filepath: `${targetPaths.rootDir}/report.api.md`,
});
};
}
@@ -31,11 +31,11 @@ import {
resolve as resolvePath,
} from 'node:path';
import { getPackageExportDetails } from '../../../lib/getPackageExportDetails';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { logApiReportInstructions } from '../common';
import { patchApiReportGeneration } from './patchApiReportGeneration';
const tmpDir = cliPaths.resolveTargetRoot(
const tmpDir = targetPaths.resolveRoot(
'./node_modules/.cache/api-extractor',
);
@@ -100,7 +100,7 @@ async function findPackageEntryPoints(packageDirs: string[]): Promise<
return Promise.all(
packageDirs.map(async packageDir => {
const pkg = await fs.readJson(
cliPaths.resolveTargetRoot(packageDir, 'package.json'),
targetPaths.resolveRoot(packageDir, 'package.json'),
);
return getPackageExportDetails(pkg).map(details => {
@@ -143,7 +143,7 @@ export async function runApiExtraction({
// inspected.
const allDistTypesEntryPointPaths = allEntryPoints.map(
({ packageDir, distTypesPath }) => {
return cliPaths.resolveTargetRoot(
return targetPaths.resolveRoot(
'./dist-types',
packageDir,
distTypesPath,
@@ -172,8 +172,8 @@ export async function runApiExtraction({
? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw))
: allowWarnings;
const projectFolder = cliPaths.resolveTargetRoot(packageDir);
const packageFolder = cliPaths.resolveTargetRoot(
const projectFolder = targetPaths.resolveRoot(packageDir);
const packageFolder = targetPaths.resolveRoot(
'./dist-types',
packageDir,
);
@@ -16,7 +16,7 @@
import { createMockDirectory } from '@backstage/backend-test-utils';
import { normalize } from 'node:path';
import * as pathsLib from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { categorizePackageDirs } from './categorizePackageDirs';
@@ -51,14 +51,12 @@ jest.mock('./categorizePackageDirs', () => ({
}),
}));
const projectPaths = pathsLib.paths;
const mockDir = createMockDirectory();
jest.spyOn(projectPaths, 'targetRoot', 'get').mockReturnValue(mockDir.path);
jest.spyOn(targetPaths, 'rootDir', 'get').mockReturnValue(mockDir.path);
jest
.spyOn(projectPaths, 'resolveTargetRoot')
.mockImplementation((...path) => mockDir.resolve(...path));
.spyOn(targetPaths, 'resolveRoot')
.mockImplementation((...path: string[]) => mockDir.resolve(...path));
jest.spyOn(PackageGraph, 'listTargetPackages').mockResolvedValue([
{
dir: normalize(mockDir.resolve('packages/package-a')),
@@ -85,7 +83,7 @@ jest.spyOn(PackageGraph, 'listTargetPackages').mockResolvedValue([
describe('buildApiReports', () => {
beforeEach(() => {
mockDir.setContent({
[projectPaths.targetRoot]: {
[targetPaths.rootDir]: {
'package.json': JSON.stringify({
workspaces: { packages: ['packages/*', 'plugins/*'] },
}),
@@ -16,7 +16,8 @@
import { OptionValues } from 'commander';
import { categorizePackageDirs } from './categorizePackageDirs';
import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { resolvePackagePaths } from '../../lib/paths';
import { runSqlExtraction } from './sql-reports';
import { runCliExtraction } from './cli-reports';
import {
@@ -37,7 +38,7 @@ type Options = {
} & OptionValues;
export async function buildApiReports(paths: string[] = [], opts: Options) {
const tmpDir = cliPaths.resolveTargetRoot(
const tmpDir = targetPaths.resolveRoot(
'./node_modules/.cache/api-extractor',
);
@@ -73,7 +74,7 @@ export async function buildApiReports(paths: string[] = [], opts: Options) {
temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs);
}
const tsconfigFilePath =
temporaryTsConfigPath ?? cliPaths.resolveTargetRoot('tsconfig.json');
temporaryTsConfigPath ?? targetPaths.resolveRoot('tsconfig.json');
if (runTsc) {
console.log('# Compiling TypeScript');
@@ -116,7 +117,7 @@ export async function buildApiReports(paths: string[] = [], opts: Options) {
console.log('# Generating package documentation');
await buildDocs({
inputDir: tmpDir,
outputDir: cliPaths.resolveTargetRoot('docs/reference'),
outputDir: targetPaths.resolveRoot('docs/reference'),
});
}
}
@@ -15,7 +15,7 @@
*/
import fs from 'fs-extra';
import { paths as cliPaths } from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
export async function categorizePackageDirs(packageDirs: string[]) {
const dirs = packageDirs.slice();
@@ -34,7 +34,7 @@ export async function categorizePackageDirs(packageDirs: string[]) {
}
const pkgJson = await fs
.readJson(cliPaths.resolveTargetRoot(dir, 'package.json'))
.readJson(targetPaths.resolveRoot(dir, 'package.json'))
.catch(error => {
if (error.code === 'ENOENT') {
return undefined;
@@ -46,7 +46,7 @@ export async function categorizePackageDirs(packageDirs: string[]) {
return; // Ignore packages without roles
}
if (
await fs.pathExists(cliPaths.resolveTargetRoot(dir, 'migrations'))
await fs.pathExists(targetPaths.resolveRoot(dir, 'migrations'))
) {
sqlPackageDirs.push(dir);
}
@@ -22,7 +22,7 @@ import {
import fs from 'fs-extra';
import { createBinRunner } from '../../util';
import { CliHelpPage, CliModel } from './types';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { generateCliReport } from './generateCliReport';
import { logApiReportInstructions } from '../common';
@@ -115,7 +115,7 @@ export async function runCliExtraction({
}: CliExtractionOptions) {
for (const packageDir of packageDirs) {
console.log(`## Processing ${packageDir}`);
const fullDir = cliPaths.resolveTargetRoot(packageDir);
const fullDir = targetPaths.resolveRoot(packageDir);
const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json'));
if (!pkgJson.bin) {
@@ -162,7 +162,7 @@ export async function runCliExtraction({
console.log('');
console.log(
`The conflicting file is ${relativePath(
cliPaths.targetRoot,
targetPaths.rootDir,
reportPath,
)}, expecting the following content:`,
);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import type { Config } from 'prettier';
/**
@@ -33,7 +33,7 @@ export async function tryRunPrettierAsync(
// Filepath for proper config resolution
const filepath =
extraConfig.filepath ??
`${cliPaths.targetRoot}/should-not-be-ignored.any`;
`${targetPaths.rootDir}/should-not-be-ignored.any`;
const config =
(await prettier.resolveConfig(filepath, { editorconfig: true })) ?? {};
const formattedContent = prettier.format(content, {
@@ -68,7 +68,7 @@ export function createPrettierSyncFormatter(
// We need a filepath for proper config resolution, not just a directory
const filepath =
extraConfig.filepath ??
`${cliPaths.targetRoot}/should-not-be-ignored.any`;
`${targetPaths.rootDir}/should-not-be-ignored.any`;
const resolveConfig =
// @ts-expect-error: v2 requires .sync, @prettier/sync v3 does not
prettierSync.resolveConfig?.sync ?? prettierSync.resolveConfig;
@@ -16,7 +16,7 @@
import fs, { readJson } from 'fs-extra';
import { relative as relativePath } from 'node:path';
import { paths as cliPaths } from '../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { diff as justDiff } from 'just-diff';
import { SchemaInfo } from './types';
import { getPgSchemaInfo } from './getPgSchemaInfo';
@@ -43,14 +43,14 @@ export async function runSqlExtraction(options: SqlExtractionOptions) {
let dbIndex = 1;
for (const packageDir of options.packageDirs) {
const migrationDir = cliPaths.resolveTargetRoot(packageDir, 'migrations');
const migrationDir = targetPaths.resolveRoot(packageDir, 'migrations');
if (!(await fs.pathExists(migrationDir))) {
console.log(`No SQL migrations found in ${packageDir}`);
continue;
}
const { name: pkgName } = await readJson(
cliPaths.resolveTargetRoot(packageDir, 'package.json'),
targetPaths.resolveRoot(packageDir, 'package.json'),
);
const migrationFiles = await fs.readdir(migrationDir, {
@@ -95,7 +95,7 @@ async function runSingleSqlExtraction(
knex: Knex,
options: SqlExtractionOptions,
) {
const migrationDir = cliPaths.resolveTargetRoot(
const migrationDir = targetPaths.resolveRoot(
targetDir,
'migrations',
migrationTarget,
@@ -152,7 +152,7 @@ async function runSingleSqlExtraction(
break;
}
}
const reportPath = cliPaths.resolveTargetRoot(
const reportPath = targetPaths.resolveRoot(
targetDir,
`report${migrationTarget === '.' ? '' : `-${migrationTarget}`}.sql.md`,
);
@@ -182,7 +182,7 @@ async function runSingleSqlExtraction(
console.log('');
console.log(
`The conflicting file is ${relativePath(
cliPaths.targetRoot,
targetPaths.rootDir,
reportPath,
)}, expecting the following content:`,
);
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { paths as cliPaths } from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import pLimit from 'p-limit';
import os from 'node:os';
import { relative as relativePath, resolve as resolvePath } from 'node:path';
@@ -100,9 +100,9 @@ async function handlePackage({
}: KnipPackageOptions) {
console.log(`## Processing ${packageDir}`);
const fullDir = cliPaths.resolveTargetRoot(packageDir);
const fullDir = targetPaths.resolveRoot(packageDir);
const reportPath = resolvePath(fullDir, 'knip-report.md');
const run = createBinRunner(cliPaths.targetRoot, '');
const run = createBinRunner(targetPaths.rootDir, '');
let report = await run(
`${knipDir}/knip.js`,
@@ -149,7 +149,7 @@ async function handlePackage({
console.log('');
console.log(
`The conflicting file is ${relativePath(
cliPaths.targetRoot,
targetPaths.rootDir,
reportPath,
)}, expecting the following content:`,
);
@@ -168,8 +168,8 @@ export async function runKnipReports({
packageDirs,
isLocalBuild,
}: KnipExtractionOptions) {
const knipDir = cliPaths.resolveTargetRoot('./node_modules/knip/bin/');
const knipConfigPath = cliPaths.resolveTargetRoot('./knip.json');
const knipDir = targetPaths.resolveRoot('./node_modules/knip/bin/');
const knipConfigPath = targetPaths.resolveRoot('./knip.json');
const limiter = pLimit(os.cpus().length);
await generateKnipConfig({ knipConfigPath });
@@ -16,11 +16,11 @@
import { Project } from 'ts-morph';
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
import fs from 'fs-extra';
import { paths as cliPaths } from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import path from 'node:path';
const project = new Project({
tsConfigFilePath: cliPaths.resolveTargetRoot('tsconfig.json'),
tsConfigFilePath: targetPaths.resolveRoot('tsconfig.json'),
});
function readPackageJson(pkg: string) {
@@ -15,7 +15,8 @@
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { resolvePackagePaths } from '../../lib/paths';
import { createTemporaryTsConfig } from './utils';
import { readFile, rm, writeFile } from 'node:fs/promises';
import pLimit from 'p-limit';
@@ -74,7 +75,7 @@ async function generateDocJson(pkg: string) {
const temporaryTsConfigPath: string = await createTemporaryTsConfig(pkg);
const packageJson = JSON.parse(
await readFile(cliPaths.resolveTargetRoot(pkg, 'package.json'), 'utf-8'),
await readFile(targetPaths.resolveRoot(pkg, 'package.json'), 'utf-8'),
);
const exports = getExports(packageJson);
@@ -85,17 +86,17 @@ async function generateDocJson(pkg: string) {
return false;
}
await mkdirp(cliPaths.resolveTargetRoot(`dist-types`, pkg));
await mkdirp(targetPaths.resolveRoot(`dist-types`, pkg));
const { stdout, stderr } = await execAsync(
[
cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'),
targetPaths.resolveRoot('node_modules/.bin/typedoc'),
'--json',
cliPaths.resolveTargetRoot(`dist-types`, pkg, 'docs.json'),
targetPaths.resolveRoot(`dist-types`, pkg, 'docs.json'),
'--tsconfig',
temporaryTsConfigPath,
'--basePath',
cliPaths.targetRoot,
targetPaths.rootDir,
'--skipErrorChecking',
...(getExports(packageJson).flatMap(e => [
'--entryPoints',
@@ -117,7 +118,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
console.warn('!!! This is an experimental command !!!');
const existingDocsJsonPaths = glob.sync(
cliPaths.resolveTargetRoot('dist-types/**/docs.json'),
targetPaths.resolveRoot('dist-types/**/docs.json'),
);
if (existingDocsJsonPaths.length > 0) {
console.warn(
@@ -129,7 +130,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
}
}
console.warn('!!! Deleting existing docs output !!!');
await rm(cliPaths.resolveTargetRoot('type-docs'), {
await rm(targetPaths.resolveRoot('type-docs'), {
recursive: true,
force: true,
});
@@ -140,8 +141,8 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
});
const cache = await PackageDocsCache.loadAsync(
cliPaths.resolveTargetRoot(),
await Lockfile.load(cliPaths.resolveTargetRoot('yarn.lock')),
targetPaths.rootDir,
await Lockfile.load(targetPaths.resolveRoot('yarn.lock')),
);
console.log(`### Generating docs.`);
@@ -150,7 +151,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
limit(async () => {
const pkgJson = JSON.parse(
await readFile(
cliPaths.resolveTargetRoot(pkg, 'package.json'),
targetPaths.resolveRoot(pkg, 'package.json'),
'utf-8',
),
);
@@ -173,7 +174,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
if (success) {
await cache.write(
pkg,
cliPaths.resolveTargetRoot(`dist-types`, pkg),
targetPaths.resolveRoot(`dist-types`, pkg),
);
}
} catch (e) {
@@ -187,7 +188,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
const generatedPackageDirs = [];
for (const pkg of selectedPackageDirs) {
try {
const docsJsonPath = cliPaths.resolveTargetRoot(
const docsJsonPath = targetPaths.resolveRoot(
`dist-types/${pkg}/docs.json`,
);
const docsJson = JSON.parse(await readFile(docsJsonPath, 'utf-8'));
@@ -211,7 +212,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
const { stdout, stderr } = await execAsync(
[
cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'),
targetPaths.resolveRoot('node_modules/.bin/typedoc'),
'--entryPointStrategy',
'merge',
...generatedPackageDirs.flatMap(pkg => [
@@ -220,13 +221,13 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
]),
...HIGHLIGHT_LANGUAGES.flatMap(e => ['--highlightLanguages', e]),
'--out',
cliPaths.resolveTargetRoot('type-docs'),
...(existsSync(cliPaths.resolveTargetRoot('typedoc.json'))
? ['--options', cliPaths.resolveTargetRoot('typedoc.json')]
targetPaths.resolveRoot('type-docs'),
...(existsSync(targetPaths.resolveRoot('typedoc.json'))
? ['--options', targetPaths.resolveRoot('typedoc.json')]
: []),
].join(' '),
{
cwd: cliPaths.targetRoot,
cwd: targetPaths.rootDir,
},
);
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { findOwnPaths } from '@backstage/cli-common';
import fs from 'fs-extra';
import { paths as cliPaths } from '../../lib/paths';
export async function createTemporaryTsConfig(dir: string) {
const path = cliPaths.resolveOwnRoot(dir, 'tsconfig.typedoc.tmp.json');
const path = findOwnPaths(__dirname).resolveRoot(dir, 'tsconfig.typedoc.tmp.json');
process.once('exit', () => {
fs.removeSync(path);
@@ -16,7 +16,7 @@
import chalk from 'chalk';
import { exec } from '../../../../lib/exec';
import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers';
import { paths as cliPaths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import { OptionValues } from 'commander';
import { env } from 'node:process';
import { readFile, rm } from 'node:fs/promises';
@@ -53,7 +53,7 @@ async function check(opts: OptionValues) {
baseRef,
],
{
cwd: cliPaths.targetRoot,
cwd: targetPaths.rootDir,
env: { CI: opts.json ? '1' : undefined, ...env },
},
);
@@ -65,7 +65,7 @@ async function check(opts: OptionValues) {
if (opts.json) {
const file = (
await readFile(resolve(cliPaths.targetRoot, 'ci-run-details.json'))
await readFile(resolve(targetPaths.rootDir, 'ci-run-details.json'))
).toString();
const results = JSON.parse(file);
console.log(file);
@@ -73,7 +73,7 @@ async function check(opts: OptionValues) {
throw new Error('Some checks failed');
}
await rm(resolve(cliPaths.targetRoot, 'ci-run-details.json'));
await rm(resolve(targetPaths.rootDir, 'ci-run-details.json'));
} else {
console.log(reduceOpticOutput(output));
if (!opts.ignore && failed) {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { paths as cliPaths } from '../../../../lib/paths';
import { targetPaths } from '@backstage/cli-common';
import chalk from 'chalk';
import { spawn } from '../../../../lib/exec';
import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers';
@@ -40,7 +40,7 @@ async function fuzz(opts: OptionValues) {
await fs.readFile(resolvedOpenapiPath, 'utf8'),
) as { info: { title: string } };
const configSource = ConfigSources.default({
rootDir: cliPaths.targetRoot,
rootDir: targetPaths.rootDir,
});
const config = await ConfigSources.toConfig(configSource);
const pluginId = openapiSpec.info.title;
@@ -48,7 +48,7 @@ async function fuzz(opts: OptionValues) {
if (opts.debug) {
args.push(
'--cassette-path',
cliPaths.resolveTargetRoot(join('.cassettes', `${pluginId}.yml`)),
targetPaths.resolveRoot(join('.cassettes', `${pluginId}.yml`)),
);
}
@@ -24,11 +24,11 @@ import {
OUTPUT_PATH,
} from '../../../../../lib/openapi/constants';
import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports';
import { targetPaths } from '@backstage/cli-common';
import {
getPathToCurrentOpenApiSpec,
toGeneratorAdditionalProperties,
} from '../../../../../lib/openapi/helpers';
import { paths as cliPaths } from '../../../../../lib/paths';
async function generate(
outputDirectory: string,
@@ -36,7 +36,7 @@ async function generate(
abortSignal?: AbortController,
) {
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
const resolvedOutputDirectory = cliPaths.resolveTargetRoot(
const resolvedOutputDirectory = targetPaths.resolveRoot(
outputDirectory,
OUTPUT_PATH,
);
@@ -95,7 +95,7 @@ async function generate(
signal: abortSignal?.signal,
});
const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier');
const prettier = targetPaths.resolveRoot('node_modules/.bin/prettier');
if (prettier) {
await exec(`${prettier} --write ${parentDirectory}`, [], {
signal: abortSignal?.signal,
@@ -27,23 +27,23 @@ import {
TS_SCHEMA_PATH,
} from '../../../../../lib/openapi/constants';
import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports';
import { targetPaths } from '@backstage/cli-common';
import {
getPathToCurrentOpenApiSpec,
getRelativePathToFile,
toGeneratorAdditionalProperties,
} from '../../../../../lib/openapi/helpers';
import { paths as cliPaths } from '../../../../../lib/paths';
async function generateSpecFile() {
const openapiPath = await getPathToCurrentOpenApiSpec();
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
const tsPath = cliPaths.resolveTarget(TS_SCHEMA_PATH);
const tsPath = targetPaths.resolve(TS_SCHEMA_PATH);
const schemaDir = dirname(tsPath);
await fs.mkdirp(schemaDir);
const oldTsPath = cliPaths.resolveTarget(OLD_SCHEMA_PATH);
const oldTsPath = targetPaths.resolve(OLD_SCHEMA_PATH);
if (fs.existsSync(oldTsPath)) {
console.warn(`Removing old schema file at ${oldTsPath}`);
fs.removeSync(oldTsPath);
@@ -77,9 +77,9 @@ export const createOpenApiRouter = async (
);
await exec(`yarn backstage-cli package lint`, ['--fix', tsPath, indexFile]);
if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) {
if (await targetPaths.resolveRoot('node_modules/.bin/prettier')) {
await exec(`yarn prettier`, ['--write', tsPath, indexFile], {
cwd: cliPaths.targetRoot,
cwd: targetPaths.rootDir,
});
}
}
@@ -150,7 +150,7 @@ async function generate(
},
);
const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier');
const prettier = targetPaths.resolveRoot('node_modules/.bin/prettier');
if (prettier) {
await exec(`${prettier} --write ${resolvedOutputDirectory}`, [], {
signal: abortSignal?.signal,
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { targetPaths } from '@backstage/cli-common';
import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants';
import { paths as cliPaths } from '../../../../lib/paths';
import chalk from 'chalk';
import { exec } from '../../../../lib/exec';
import {
@@ -49,7 +49,7 @@ capture:
${YAML_SCHEMA_PATH}:
# 🔧 Runnable example with simple get requests.
# Run with "PORT=3000 optic capture ${YAML_SCHEMA_PATH} --update interactive" in '${
cliPaths.targetDir
targetPaths.dir
}'
# You can change the server and the 'requests' section to experiment
server:
@@ -64,7 +64,7 @@ capture:
).join(' ')}
`,
);
if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) {
if (await targetPaths.resolveRoot('node_modules/.bin/prettier')) {
await exec(`yarn prettier`, ['--write', opticConfigFilePath]);
}
}
@@ -16,16 +16,16 @@
import { PackageGraph } from '@backstage/cli-node';
import { OptionValues } from 'commander';
import { exec } from '../../../../lib/exec';
import { targetPaths } from '@backstage/cli-common';
import {
CiRunDetails,
generateCompareSummaryMarkdown,
} from '../../../../lib/openapi/optic/helpers';
import { paths as cliPaths } from '../../../../lib/paths';
import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants';
function cleanUpApiName(e: { apiName: string }) {
e.apiName = e.apiName
.replace(cliPaths.targetDir, '')
.replace(targetPaths.dir, '')
.replace(YAML_SCHEMA_PATH, '');
}
@@ -46,7 +46,7 @@ export async function command(opts: OptionValues) {
const changedOpenApiSpecs = changedFiles
.split('\n')
.filter(e => e.endsWith(YAML_SCHEMA_PATH))
.map(e => cliPaths.resolveTarget(e));
.map(e => targetPaths.resolve(e));
// filter packages by changedFiles
packages = packages.filter(pkg =>
@@ -17,9 +17,9 @@
import fs from 'fs-extra';
import { join } from 'node:path';
import chalk from 'chalk';
import { findOwnPaths, targetPaths } from '@backstage/cli-common';
import { runner } from '../../../../lib/runner';
import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants';
import { paths as cliPaths } from '../../../../lib/paths';
import { exec } from '../../../../lib/exec';
import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers';
@@ -42,7 +42,9 @@ async function test(
let opticLocation = '';
try {
opticLocation = (
await exec(`yarn bin optic`, [], { cwd: cliPaths.ownRoot })
await exec(`yarn bin optic`, [], {
cwd: findOwnPaths(__dirname).rootDir,
})
).stdout as string;
} catch (err) {
throw new Error(
@@ -79,7 +81,7 @@ async function test(
throw err;
}
if (
(await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) &&
(await targetPaths.resolveRoot('node_modules/.bin/prettier')) &&
options?.update
) {
await exec(`yarn prettier`, ['--write', openapiPath]);
@@ -19,8 +19,8 @@ import { isEqual } from 'lodash';
import { join } from 'node:path';
import chalk from 'chalk';
import { relative as relativePath, resolve as resolvePath } from 'node:path';
import { targetPaths } from '@backstage/cli-common';
import { runner } from '../../../../lib/runner';
import { paths as cliPaths } from '../../../../lib/paths';
import {
OLD_SCHEMA_PATH,
TS_SCHEMA_PATH,
@@ -60,7 +60,7 @@ async function verify(directoryPath: string) {
throw new Error(`\`${TS_SCHEMA_PATH}\` needs to have a 'spec' export.`);
}
if (!isEqual(schema.spec, yaml)) {
const path = relativePath(cliPaths.targetRoot, directoryPath);
const path = relativePath(targetPaths.rootDir, directoryPath);
throw new Error(
`\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools package schema openapi generate\` from '${path}' to regenerate \`${TS_SCHEMA_PATH}\`.`,
);
@@ -18,8 +18,8 @@ import Parser from '@apidevtools/swagger-parser';
import fs, { pathExists } from 'fs-extra';
import YAML from 'js-yaml';
import { cloneDeep } from 'lodash';
import { targetPaths } from '@backstage/cli-common';
import { resolve } from 'node:path';
import { paths } from '../paths';
import { YAML_SCHEMA_PATH } from './constants';
export const getPathToFile = async (directory: string, filename: string) => {
@@ -27,7 +27,7 @@ export const getPathToFile = async (directory: string, filename: string) => {
};
export const getRelativePathToFile = async (filename: string) => {
return await getPathToFile(paths.targetDir, filename);
return await getPathToFile(targetPaths.dir, filename);
};
export const assertExists = async (path: string) => {
+6 -22
View File
@@ -14,27 +14,11 @@
* limitations under the License.
*/
import { targetPaths, findOwnPaths } from '@backstage/cli-common';
import { targetPaths } from '@backstage/cli-common';
import { PackageGraph } from '@backstage/cli-node';
import { Minimatch } from 'minimatch';
import { isAbsolute, relative as relativePath } from 'node:path';
/* eslint-disable-next-line no-restricted-syntax */
export const paths = {
get targetDir() {
return targetPaths.resolve();
},
get targetRoot() {
return targetPaths.resolveRoot();
},
get ownRoot() {
return findOwnPaths(__dirname).resolveRoot();
},
resolveTarget: targetPaths.resolve,
resolveTargetRoot: targetPaths.resolveRoot,
resolveOwnRoot: (...p: string[]) => findOwnPaths(__dirname).resolveRoot(...p),
};
/** @internal */
export interface ResolvePackagesOptions {
paths?: string[];
@@ -54,7 +38,7 @@ export async function resolvePackagePaths(
for (const path of providedPaths) {
const matches = packages.some(
({ dir }) =>
new Minimatch(path).match(relativePath(targetPaths.resolveRoot(), dir)) ||
new Minimatch(path).match(relativePath(targetPaths.rootDir, dir)) ||
isChildPath(dir, path),
);
if (!matches) {
@@ -70,7 +54,7 @@ export async function resolvePackagePaths(
packages = packages.filter(({ dir }) =>
providedPaths.some(
path =>
new Minimatch(path).match(relativePath(targetPaths.resolveRoot(), dir)) ||
new Minimatch(path).match(relativePath(targetPaths.rootDir, dir)) ||
isChildPath(dir, path),
),
);
@@ -79,7 +63,7 @@ export async function resolvePackagePaths(
if (include) {
packages = packages.filter(pkg =>
include.some(pattern =>
new Minimatch(pattern).match(relativePath(targetPaths.resolveRoot(), pkg.dir)),
new Minimatch(pattern).match(relativePath(targetPaths.rootDir, pkg.dir)),
),
);
}
@@ -89,13 +73,13 @@ export async function resolvePackagePaths(
exclude.some(
pattern =>
!new Minimatch(pattern).match(
relativePath(targetPaths.resolveRoot(), pkg.dir),
relativePath(targetPaths.rootDir, pkg.dir),
),
),
);
}
return packages.map(pkg => relativePath(targetPaths.resolveRoot(), pkg.dir));
return packages.map(pkg => relativePath(targetPaths.rootDir, pkg.dir));
}
/** @internal */
+2 -2
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { targetPaths } from '@backstage/cli-common';
import { resolvePackagePaths } from './paths';
import pLimit from 'p-limit';
import { relative as relativePath } from 'node:path';
import { paths as cliPaths } from './paths';
import portFinder from 'portfinder';
export async function runner(
@@ -58,7 +58,7 @@ export async function runner(
}
return {
relativeDir: relativePath(cliPaths.targetRoot, pkg),
relativeDir: relativePath(targetPaths.rootDir, pkg),
resultText,
};
}),
+1 -1
View File
@@ -86,7 +86,7 @@ describe('Backstage yarn plugin', () => {
let initialLockFileContent: string | undefined;
beforeAll(async () => {
const targetRoot = targetPaths.resolveRoot();
const targetRoot = targetPaths.rootDir;
await executeCommand('yarn', ['build'], {
cwd: joinPath(targetRoot, 'packages/yarn-plugin'),
});
@@ -50,6 +50,12 @@ describe('getWorkspaceRoot', () => {
jest.doMock('@backstage/cli-common', () => ({
...jest.requireActual('@backstage/cli-common'),
targetPaths: {
get dir() {
return mockResolveRoot();
},
get rootDir() {
return mockResolveRoot();
},
resolveRoot: mockResolveRoot,
},
}));
@@ -18,5 +18,5 @@ import { npath } from '@yarnpkg/fslib';
import { targetPaths } from '@backstage/cli-common';
export const getWorkspaceRoot = () => {
return npath.toPortablePath(targetPaths.resolveRoot());
return npath.toPortablePath(targetPaths.rootDir);
};