Merge branch 'master' into feat/26838
This commit is contained in:
@@ -92,6 +92,7 @@ export function isMonoRepo(): Promise<boolean>;
|
||||
export class Lockfile {
|
||||
createSimplifiedDependencyGraph(): Map<string, Set<string>>;
|
||||
diff(otherLockfile: Lockfile): LockfileDiff;
|
||||
getDependencyTreeHash(startName: string): string;
|
||||
static load(path: string): Promise<Lockfile>;
|
||||
static parse(content: string): Lockfile;
|
||||
}
|
||||
|
||||
@@ -493,4 +493,95 @@ d@^1:
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDependencyTreeHash', () => {
|
||||
const content = `${MODERN_HEADER}
|
||||
"a@npm:^1":
|
||||
version: "1.0.0"
|
||||
checksum: sha512-a-1
|
||||
dependencies:
|
||||
b: "^2"
|
||||
|
||||
"b@npm:2.0.x, b@npm:^2":
|
||||
version: "2.0.0"
|
||||
checksum: sha512-b-1
|
||||
|
||||
"b@npm:4":
|
||||
version: "3.0.0"
|
||||
checksum: sha512-b-2
|
||||
|
||||
"c@npm:^1":
|
||||
version: "4.0.0"
|
||||
checksum: sha512-c-1
|
||||
`;
|
||||
const lockfile = Lockfile.parse(content);
|
||||
|
||||
const hashA = lockfile.getDependencyTreeHash('a');
|
||||
const hashB = lockfile.getDependencyTreeHash('b');
|
||||
const hashC = lockfile.getDependencyTreeHash('c');
|
||||
|
||||
it('should generate stable dependency hashes', () => {
|
||||
expect(hashA).toMatchInlineSnapshot(
|
||||
`"2d1d4c1c577c291e815e87779c72fc78a78e56cc"`,
|
||||
);
|
||||
expect(hashB).toMatchInlineSnapshot(
|
||||
`"7e46d0c7337540179b442c87a7c5555543798f15"`,
|
||||
);
|
||||
expect(hashC).toMatchInlineSnapshot(
|
||||
`"e65103abd217954bad40e2f834b990a5e6fa4054"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate different hashes for different versions', () => {
|
||||
const lockfileNewA = Lockfile.parse(content.replace('1.0.0', '1.0.1'));
|
||||
expect(lockfileNewA.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewA.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewA.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB1 = Lockfile.parse(content.replace('2.0.0', '2.0.1'));
|
||||
expect(lockfileNewB1.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB2 = Lockfile.parse(content.replace('3.0.0', '3.0.1'));
|
||||
expect(lockfileNewB2.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewC = Lockfile.parse(content.replace('4.0.0', '4.0.1'));
|
||||
expect(lockfileNewC.getDependencyTreeHash('a')).toBe(hashA);
|
||||
expect(lockfileNewC.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewC.getDependencyTreeHash('c')).not.toBe(hashC);
|
||||
});
|
||||
|
||||
it('should generate different hashes for different checksums', () => {
|
||||
const lockfileNewA = Lockfile.parse(
|
||||
content.replace('sha512-a-1', 'sha512-a-1-new'),
|
||||
);
|
||||
expect(lockfileNewA.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewA.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewA.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB1 = Lockfile.parse(
|
||||
content.replace('sha512-b-1', 'sha512-b-1-new'),
|
||||
);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB2 = Lockfile.parse(
|
||||
content.replace('sha512-b-2', 'sha512-b-2-new'),
|
||||
);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewC = Lockfile.parse(
|
||||
content.replace('sha512-c-1', 'sha512-c-1-new'),
|
||||
);
|
||||
expect(lockfileNewC.getDependencyTreeHash('a')).toBe(hashA);
|
||||
expect(lockfileNewC.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewC.getDependencyTreeHash('c')).not.toBe(hashC);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { parseSyml } from '@yarnpkg/parsers';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
|
||||
@@ -215,4 +216,63 @@ export class Lockfile {
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a sha1 hex hash of the dependency graph for a package.
|
||||
*/
|
||||
getDependencyTreeHash(startName: string): string {
|
||||
if (!this.packages.has(startName)) {
|
||||
throw new Error(`Package '${startName}' not found in lockfile`);
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
|
||||
const queue = [startName];
|
||||
const seen = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const name = queue.pop()!;
|
||||
|
||||
if (seen.has(name)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(name);
|
||||
|
||||
const entries = this.packages.get(name);
|
||||
if (!entries) {
|
||||
continue; // In case of missing optional peer dependencies
|
||||
}
|
||||
|
||||
hash.update(`pkg:${name}`);
|
||||
hash.update('\0');
|
||||
|
||||
// TODO(Rugvip): This uses the same simplified lookup as createSimplifiedDependencyGraph()
|
||||
// we could match version queries to make the resulting tree a bit smaller.
|
||||
const deps = new Array<string>();
|
||||
for (const entry of entries) {
|
||||
// We're not being particular about stable ordering here. If the lockfile ordering changes, so will likely hash.
|
||||
hash.update(entry.version);
|
||||
|
||||
const data = this.data[entry.dataKey];
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const checksum = data.checksum || data.integrity;
|
||||
if (checksum) {
|
||||
hash.update('#');
|
||||
hash.update(checksum);
|
||||
}
|
||||
|
||||
hash.update(' ');
|
||||
|
||||
deps.push(...Object.keys(data.dependencies ?? {}));
|
||||
deps.push(...Object.keys(data.peerDependencies ?? {}));
|
||||
}
|
||||
|
||||
queue.push(...new Set(deps));
|
||||
}
|
||||
|
||||
return hash.digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,8 @@ Usage: backstage-cli repo lint [options]
|
||||
Options:
|
||||
--format <format>
|
||||
--since <ref>
|
||||
--successCache
|
||||
--successCacheDir <path>
|
||||
--fix
|
||||
-h, --help
|
||||
```
|
||||
|
||||
@@ -61,6 +61,14 @@ export function registerRepoCommand(program: Command) {
|
||||
'--since <ref>',
|
||||
'Only lint packages that changed since the specified ref',
|
||||
)
|
||||
.option(
|
||||
'--successCache',
|
||||
'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
|
||||
)
|
||||
.option(
|
||||
'--successCacheDir <path>',
|
||||
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
|
||||
)
|
||||
.option('--fix', 'Attempt to automatically fix violations')
|
||||
.action(lazy(() => import('./repo/lint').then(m => m.command)));
|
||||
|
||||
|
||||
@@ -16,8 +16,14 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { PackageGraph, BackstagePackageJson } from '@backstage/cli-node';
|
||||
import fs from 'fs-extra';
|
||||
import { createHash } from 'crypto';
|
||||
import { relative as relativePath, resolve as resolvePath } from 'path';
|
||||
import {
|
||||
PackageGraph,
|
||||
BackstagePackageJson,
|
||||
Lockfile,
|
||||
} from '@backstage/cli-node';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { runWorkerQueueThreads } from '../../lib/parallel';
|
||||
import { createScriptOptionsParser } from './optionsParser';
|
||||
@@ -30,9 +36,43 @@ function depCount(pkg: BackstagePackageJson) {
|
||||
return deps + devDeps;
|
||||
}
|
||||
|
||||
const CACHE_FILE_NAME = 'lint-cache.json';
|
||||
|
||||
type Cache = string[];
|
||||
|
||||
async function readCache(dir: string): Promise<Cache | undefined> {
|
||||
try {
|
||||
const data = await fs.readJson(resolvePath(dir, CACHE_FILE_NAME));
|
||||
if (!Array.isArray(data)) {
|
||||
return undefined;
|
||||
}
|
||||
if (data.some(x => typeof x !== 'string')) {
|
||||
return undefined;
|
||||
}
|
||||
return data as Cache;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCache(dir: string, cache: Cache) {
|
||||
await fs.mkdirp(dir);
|
||||
await fs.writeJson(resolvePath(dir, CACHE_FILE_NAME), cache, { spaces: 2 });
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const cacheDir = resolvePath(
|
||||
opts.successCacheDir ?? 'node_modules/.cache/backstage-cli',
|
||||
);
|
||||
const cacheContext = opts.successCache
|
||||
? {
|
||||
cache: await readCache(cacheDir),
|
||||
lockfile: await Lockfile.load(paths.resolveTargetRoot('yarn.lock')),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (opts.since) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
packages = await graph.listChangedPackages({
|
||||
@@ -57,26 +97,66 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
|
||||
const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']);
|
||||
|
||||
const items = await Promise.all(
|
||||
packages.map(async pkg => {
|
||||
const lintOptions = parseLintScript(pkg.packageJson.scripts?.lint);
|
||||
const base = {
|
||||
fullDir: pkg.dir,
|
||||
relativeDir: relativePath(paths.targetRoot, pkg.dir),
|
||||
lintOptions,
|
||||
parentHash: undefined,
|
||||
};
|
||||
|
||||
if (!cacheContext) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const hash = createHash('sha1');
|
||||
|
||||
hash.update(
|
||||
cacheContext.lockfile.getDependencyTreeHash(pkg.packageJson.name),
|
||||
);
|
||||
hash.update('\0');
|
||||
hash.update(JSON.stringify(lintOptions));
|
||||
hash.update('\0');
|
||||
hash.update(process.version); // Node.js version
|
||||
hash.update('\0');
|
||||
hash.update('v1'); // The version of this implementation
|
||||
|
||||
return {
|
||||
...base,
|
||||
parentHash: hash.digest('hex'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const resultsList = await runWorkerQueueThreads({
|
||||
items: packages.map(pkg => ({
|
||||
fullDir: pkg.dir,
|
||||
relativeDir: relativePath(paths.targetRoot, pkg.dir),
|
||||
lintOptions: parseLintScript(pkg.packageJson.scripts?.lint),
|
||||
})),
|
||||
items,
|
||||
workerData: {
|
||||
fix: Boolean(opts.fix),
|
||||
format: opts.format as string | undefined,
|
||||
shouldCache: Boolean(cacheContext),
|
||||
successCache: cacheContext?.cache,
|
||||
},
|
||||
workerFactory: async ({ fix, format }) => {
|
||||
workerFactory: async ({ fix, format, shouldCache, successCache }) => {
|
||||
const { ESLint } = require('eslint') as typeof import('eslint');
|
||||
const crypto = require('crypto') as typeof import('crypto');
|
||||
const recursiveReadDir =
|
||||
require('recursive-readdir') as typeof import('recursive-readdir');
|
||||
const { readFile } =
|
||||
require('fs/promises') as typeof import('fs/promises');
|
||||
const { relative: workerRelativePath } =
|
||||
require('path') as typeof import('path');
|
||||
|
||||
return async ({
|
||||
fullDir,
|
||||
relativeDir,
|
||||
lintOptions,
|
||||
parentHash,
|
||||
}): Promise<{
|
||||
relativeDir: string;
|
||||
resultText: string;
|
||||
sha?: string;
|
||||
resultText?: string;
|
||||
failed: boolean;
|
||||
}> => {
|
||||
// Bit of a hack to make file resolutions happen from the correct directory
|
||||
@@ -89,6 +169,35 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
fix,
|
||||
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
|
||||
});
|
||||
|
||||
let sha: string | undefined = undefined;
|
||||
if (shouldCache) {
|
||||
const result = await recursiveReadDir(fullDir);
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(parentHash!);
|
||||
hash.update('\0');
|
||||
|
||||
for (const path of result.sort()) {
|
||||
if (await eslint.isPathIgnored(path)) {
|
||||
continue;
|
||||
}
|
||||
hash.update(workerRelativePath(fullDir, path));
|
||||
hash.update('\0');
|
||||
hash.update(await readFile(path));
|
||||
hash.update('\0');
|
||||
hash.update(
|
||||
JSON.stringify(await eslint.calculateConfigForFile(path)),
|
||||
);
|
||||
hash.update('\0');
|
||||
}
|
||||
sha = await hash.digest('hex');
|
||||
if (successCache?.includes(sha)) {
|
||||
console.log(`Skipped ${relativeDir} due to cache hit`);
|
||||
return { relativeDir, sha, failed: false };
|
||||
}
|
||||
}
|
||||
|
||||
const formatter = await eslint.loadFormatter(format);
|
||||
|
||||
const results = await eslint.lintFiles(['.']);
|
||||
@@ -112,13 +221,21 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
relativeDir,
|
||||
resultText,
|
||||
failed,
|
||||
sha,
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const outputSuccessCache = [];
|
||||
|
||||
let failed = false;
|
||||
for (const { relativeDir, resultText, failed: runFailed } of resultsList) {
|
||||
for (const {
|
||||
relativeDir,
|
||||
resultText,
|
||||
failed: runFailed,
|
||||
sha,
|
||||
} of resultsList) {
|
||||
if (runFailed) {
|
||||
console.log(chalk.red(`Lint failed in ${relativeDir}`));
|
||||
failed = true;
|
||||
@@ -129,9 +246,15 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
console.log();
|
||||
console.log(resultText.trimStart());
|
||||
}
|
||||
} else if (sha) {
|
||||
outputSuccessCache.push(sha);
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheContext) {
|
||||
await writeCache(cacheDir, outputSuccessCache);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ export async function makeRollupConfigs(
|
||||
format: 'commonjs',
|
||||
interop: 'compat',
|
||||
sourcemap: true,
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: `${targetDir}/src`,
|
||||
exports: 'named',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('backendModule factory', () => {
|
||||
`availability plugins${sep}test-backend-module-tester-two`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('backendPlugin factory', () => {
|
||||
`availability plugins${sep}test-backend`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'templating package.json.hbs',
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('frontendPlugin factory', () => {
|
||||
`availability plugins${sep}test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.tsx.hbs',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('nodeLibraryPackage factory', () => {
|
||||
`availability ${joinPath('packages', expectedNodeLibraryPackageName)}`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginCommon factory', () => {
|
||||
`availability plugins${sep}test-common`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginNode factory', () => {
|
||||
`availability plugins${sep}test-node`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginWeb factory', () => {
|
||||
`availability plugins${sep}test-react`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('scaffolderModule factory', () => {
|
||||
`availability plugins${sep}scaffolder-backend-module-test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('webLibraryPackage factory', () => {
|
||||
`availability ${joinPath('packages', expectedwebLibraryPackageName)}`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "*",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="jest" />
|
||||
/// <reference types="react" />
|
||||
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
@@ -32,6 +34,15 @@ import { TestApiProviderProps } from '@backstage/test-utils';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @public
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 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 { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
@@ -26,4 +26,5 @@ export {
|
||||
type MockStorageBucket,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
export { type ApiMock } from './ApiMock';
|
||||
export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi';
|
||||
|
||||
Reference in New Issue
Block a user