Merge branch 'master' into package-workspaces
Signed-off-by: Gabriel Dugny <gabriel.dugny@believe.com> # Conflicts: # packages/cli-node/src/pacman/yarn/Yarn.test.ts # packages/cli-node/src/pacman/yarn/Yarn.ts
This commit is contained in:
@@ -1,5 +1,39 @@
|
||||
# @backstage/cli-common
|
||||
|
||||
## 0.2.0-next.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 56bd494: 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')
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/errors@1.2.7
|
||||
|
||||
## 0.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"name": "@backstage/cli-common",
|
||||
"version": "0.1.18",
|
||||
"version": "0.2.0-next.0",
|
||||
"description": "Common functionality used by cli, backend, and create-app",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
@@ -20,8 +18,23 @@
|
||||
"directory": "packages/cli-common"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./testUtils": "./src/testUtils.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"testUtils": [
|
||||
"src/testUtils.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
## API Report File for "@backstage/cli-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public
|
||||
export function overrideTargetPaths(
|
||||
dirOrOptions: string | OverrideTargetPathsOptions,
|
||||
): TargetPathsOverride;
|
||||
|
||||
// @public
|
||||
export interface OverrideTargetPathsOptions {
|
||||
dir: string;
|
||||
rootDir?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface TargetPathsOverride {
|
||||
restore(): void;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -16,17 +16,27 @@ export function bootstrapEnvProxyAgents(): void;
|
||||
// @public
|
||||
export class ExitCodeError extends CustomErrorBase {
|
||||
constructor(code: number, command?: string);
|
||||
// (undocumented)
|
||||
readonly code: number;
|
||||
}
|
||||
|
||||
// @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 +78,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;
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@ import { CustomErrorBase } from '@backstage/errors';
|
||||
* @public
|
||||
*/
|
||||
export class ExitCodeError extends CustomErrorBase {
|
||||
/** The exit code of the child process. */
|
||||
readonly code: number;
|
||||
|
||||
constructor(code: number, command?: string) {
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { findPaths, BACKSTAGE_JSON } from './paths';
|
||||
export { findPaths, findOwnPaths, targetPaths, BACKSTAGE_JSON } from './paths';
|
||||
export { isChildPath } from './isChildPath';
|
||||
export type { Paths, ResolveFunc } from './paths';
|
||||
export type { Paths, TargetPaths, OwnPaths, ResolveFunc } from './paths';
|
||||
export { bootstrapEnvProxyAgents } from './proxyBootstrap';
|
||||
export {
|
||||
run,
|
||||
|
||||
@@ -25,35 +25,61 @@ import { dirname, resolve as resolvePath } from 'node:path';
|
||||
*/
|
||||
export type ResolveFunc = (...paths: string[]) => string;
|
||||
|
||||
/**
|
||||
* Resolved paths relative to the target project, based on `process.cwd()`.
|
||||
* Lazily initialized on first property access. Re-resolves automatically
|
||||
* when `process.cwd()` changes.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/** Resolve a path relative to the target repo root. */
|
||||
resolveRoot: ResolveFunc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolved paths relative to a specific package in the repository.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/** Resolve a path relative to the monorepo root containing the package. */
|
||||
resolveRoot: ResolveFunc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Common paths and resolve functions used by the cli.
|
||||
* Currently assumes it is being executed within a monorepo.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link targetPaths} and {@link findOwnPaths} instead.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -84,18 +110,7 @@ export function findRootPath(
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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) {
|
||||
@@ -107,64 +122,209 @@ export function findOwnRootDir(ownDir: string) {
|
||||
return resolvePath(ownDir, '../..');
|
||||
}
|
||||
|
||||
// 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>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
// Finds the root of a given package
|
||||
export function findOwnDir(searchDir: string) {
|
||||
return OwnPathsImpl.findDir(searchDir);
|
||||
}
|
||||
|
||||
// Used by the test utility in testUtils.ts to override targetPaths
|
||||
export let targetPathsOverride: TargetPaths | undefined;
|
||||
|
||||
/** @internal */
|
||||
export function setTargetPathsOverride(override: TargetPaths | undefined) {
|
||||
targetPathsOverride = override;
|
||||
}
|
||||
|
||||
class TargetPathsImpl implements TargetPaths {
|
||||
#cwd: string | undefined;
|
||||
#dir: string | undefined;
|
||||
#rootDir: string | undefined;
|
||||
|
||||
get dir(): string {
|
||||
if (targetPathsOverride) {
|
||||
return targetPathsOverride.dir;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
get rootDir(): string {
|
||||
if (targetPathsOverride) {
|
||||
return targetPathsOverride.rootDir;
|
||||
}
|
||||
// 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 => {
|
||||
if (targetPathsOverride) {
|
||||
return targetPathsOverride.resolve(...paths);
|
||||
}
|
||||
return resolvePath(this.dir, ...paths);
|
||||
};
|
||||
|
||||
resolveRoot = (...paths: string[]): string => {
|
||||
if (targetPathsOverride) {
|
||||
return targetPathsOverride.resolveRoot(...paths);
|
||||
}
|
||||
return resolvePath(this.rootDir, ...paths);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily resolved paths relative to the target project. Import this directly
|
||||
* for cwd-based path resolution without needing `__dirname`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const targetPaths: TargetPaths = new TargetPathsImpl();
|
||||
|
||||
/**
|
||||
* Find paths relative to the package that the calling code lives in.
|
||||
*
|
||||
* Results are cached per package root, and the package root lookup uses a
|
||||
* hierarchical directory cache so that multiple calls from different
|
||||
* subdirectories within the same package share work.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function findOwnPaths(searchDir: string): OwnPaths {
|
||||
return OwnPathsImpl.find(searchDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find paths related to a package and its execution context.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link targetPaths} for cwd-based paths and
|
||||
* {@link findOwnPaths} for package-relative paths instead.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const paths = findPaths(__dirname)
|
||||
*/
|
||||
export function findPaths(searchDir: string): Paths {
|
||||
const ownDir = findOwnDir(searchDir);
|
||||
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
|
||||
const targetDir = fs
|
||||
.realpathSync(process.cwd())
|
||||
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
|
||||
|
||||
// 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);
|
||||
} 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;
|
||||
};
|
||||
|
||||
const own = findOwnPaths(searchDir);
|
||||
return {
|
||||
ownDir,
|
||||
get ownDir() {
|
||||
return own.dir;
|
||||
},
|
||||
get ownRoot() {
|
||||
return getOwnRoot();
|
||||
return own.rootDir;
|
||||
},
|
||||
get targetDir() {
|
||||
return targetPaths.dir;
|
||||
},
|
||||
targetDir,
|
||||
get targetRoot() {
|
||||
return getTargetRoot();
|
||||
return targetPaths.rootDir;
|
||||
},
|
||||
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
|
||||
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
|
||||
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
|
||||
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
|
||||
resolveOwn: own.resolve,
|
||||
resolveOwnRoot: own.resolveRoot,
|
||||
resolveTarget: targetPaths.resolve,
|
||||
resolveTargetRoot: targetPaths.resolveRoot,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 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 { resolve as resolvePath } from 'node:path';
|
||||
import { setTargetPathsOverride } from './paths';
|
||||
|
||||
/**
|
||||
* Options for {@link overrideTargetPaths}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OverrideTargetPathsOptions {
|
||||
/** The target package directory. */
|
||||
dir: string;
|
||||
/** The target monorepo root directory. Defaults to `dir` if not provided. */
|
||||
rootDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return value of {@link overrideTargetPaths}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TargetPathsOverride {
|
||||
/** Restores `targetPaths` to its normal behavior. */
|
||||
restore(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the `targetPaths` singleton to resolve from the given directory
|
||||
* instead of `process.cwd()`.
|
||||
*
|
||||
* When called with a string, that value is used as both `dir` and `rootDir`.
|
||||
* Pass an options object to set them independently.
|
||||
*
|
||||
* Calling `restore()` on the return value reverts to normal behavior.
|
||||
* Restoration is only needed if you want to change the override within a
|
||||
* test file; each Jest worker starts with a clean module state.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function overrideTargetPaths(
|
||||
dirOrOptions: string | OverrideTargetPathsOptions,
|
||||
): TargetPathsOverride {
|
||||
const { dir, rootDir } =
|
||||
typeof dirOrOptions === 'string'
|
||||
? { dir: dirOrOptions, rootDir: dirOrOptions }
|
||||
: {
|
||||
dir: dirOrOptions.dir,
|
||||
rootDir: dirOrOptions.rootDir ?? dirOrOptions.dir,
|
||||
};
|
||||
|
||||
setTargetPathsOverride({
|
||||
dir,
|
||||
rootDir,
|
||||
resolve: (...paths: string[]) => resolvePath(dir, ...paths),
|
||||
resolveRoot: (...paths: string[]) => resolvePath(rootDir, ...paths),
|
||||
});
|
||||
|
||||
return {
|
||||
restore() {
|
||||
setTargetPathsOverride(undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user