diff --git a/.changeset/nasty-needles-hear.md b/.changeset/nasty-needles-hear.md new file mode 100644 index 0000000000..32a9d0dfb7 --- /dev/null +++ b/.changeset/nasty-needles-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added `/testUtils` entry point, with a utility for mocking resolve package paths as returned by `resolvePackagePath`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index ee43b74c16..164f388434 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -10,6 +10,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", + "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "typesVersions": { @@ -17,6 +18,9 @@ "alpha": [ "src/alpha.ts" ], + "testUtils": [ + "src/testUtils.ts" + ], "package.json": [ "package.json" ] diff --git a/packages/backend-common/src/paths.ts b/packages/backend-common/src/paths.ts index c8a8849d6c..4ec8e01bc4 100644 --- a/packages/backend-common/src/paths.ts +++ b/packages/backend-common/src/paths.ts @@ -18,6 +18,12 @@ import { isChildPath } from '@backstage/cli-common'; import { NotAllowedError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; +/** @internal */ +export const packagePathMocks = new Map< + string, + (paths: string[]) => string | undefined +>(); + /** * Resolve a path relative to the root of a package directory. * Additional path arguments are resolved relative to the package dir. @@ -29,6 +35,14 @@ import { resolve as resolvePath } from 'path'; * @public */ export function resolvePackagePath(name: string, ...paths: string[]) { + const mockedResolve = packagePathMocks.get(name); + if (mockedResolve) { + const resolved = mockedResolve(paths); + if (resolved) { + return resolved; + } + } + const req = typeof __non_webpack_require__ === 'undefined' ? require diff --git a/packages/backend-common/src/testUtils.ts b/packages/backend-common/src/testUtils.ts new file mode 100644 index 0000000000..0b23333434 --- /dev/null +++ b/packages/backend-common/src/testUtils.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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 { packagePathMocks } from './paths'; +import { posix as posixPath, resolve as resolvePath } from 'path'; + +/** @public */ +export interface PackagePathMock { + /** Restored the normal behavior of resolvePackagePath */ + restore(): void; +} + +/** @public */ +export interface PackagePathMockOptions { + /** The name of the package to mock the resolved path of */ + name: string; + /** A replacement for the root package path */ + path?: string; + /** + * Replacements for package sub-paths, each key must be an exact match of the posix-style path + * that is being resolved within the package. + * + * For example, code calling `resolvePackagePath('x', 'foo', 'bar')` would match only the following + * configuration: `createPackagePathMock({name: 'x', paths: {'foo/bar': baz}})` + */ + paths?: { [path in string]: string | (() => string) }; +} + +/** @public */ +export function createPackagePathMock( + options: PackagePathMockOptions, +): PackagePathMock { + if (packagePathMocks.has(options.name)) { + throw new Error( + `Duplicate package path mock for package '${options.name}'`, + ); + } + + packagePathMocks.set(options.name, paths => { + const joinedPath = posixPath.join(...paths); + const localResolver = options.paths?.[joinedPath]; + if (localResolver) { + return typeof localResolver === 'function' + ? localResolver() + : localResolver; + } + if (options.path) { + return resolvePath(options.path, ...paths); + } + return undefined; + }); + + return { + restore() { + packagePathMocks.delete(options.name); + }, + }; +}