Merge branch 'master' into storybook8
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The check for `react-dom/client` in the Jest configuration will now properly always run from the target directory.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added `--alwaysPack` as a replacement for the now hidden `--alwaysYarnPack` flag for the `build-workspace` command.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The `--successCache` option for the `repo test` and `repo lint` commands now use an additive store that keeps old entries around for a week before they are cleaned up automatically.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Removed unused `msw` dependency.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': minor
|
||||
---
|
||||
|
||||
Adds a `--watch` mode to the `schema openapi generate` command for a better local schema writing experience.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Bump the Webpack dependency range to `^5.94.0`, as our current configuration is not compatible with some older versions.
|
||||
@@ -5,6 +5,12 @@ sidebar_label: Identity
|
||||
description: Documentation for the Identity service
|
||||
---
|
||||
|
||||
:::note Note
|
||||
|
||||
This service is deprecated, you are likely looking for the [Auth Service](./auth.md) instead. If you're wondering how to get the user's entity ref and ownership claims in your backend plugin, you should see the [User Info Service](./user-info.md) documentation.
|
||||
|
||||
:::
|
||||
|
||||
When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them.
|
||||
|
||||
## Using the service
|
||||
|
||||
@@ -9,8 +9,12 @@ This service lets you extract more information about a set of user credentials.
|
||||
Specifically, it can be used to extract the ownership entity refs for a user
|
||||
principal.
|
||||
|
||||
See also the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services for
|
||||
general credentials handling.
|
||||
:::note Note
|
||||
|
||||
Please also refer to [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services for
|
||||
general credentials handling which is a prerequisite for the below examples.
|
||||
|
||||
:::
|
||||
|
||||
## Using the Service
|
||||
|
||||
|
||||
@@ -84,7 +84,10 @@ const config: Config = {
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
{
|
||||
docs: {
|
||||
editUrl: 'https://github.com/backstage/backstage/edit/master/docs/',
|
||||
editUrl: ({ docPath }) => {
|
||||
// Always point to the non-versioned directory when editing a doc page
|
||||
return `https://github.com/backstage/backstage/edit/master/docs/${docPath}`;
|
||||
},
|
||||
path: '../docs',
|
||||
sidebarPath: 'sidebars.js',
|
||||
...(useVersionedDocs
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
"fs-extra": "^11.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"knex": "^3.0.0",
|
||||
"msw": "^1.0.0",
|
||||
"mysql2": "^3.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pg-connection-string": "^2.3.0",
|
||||
|
||||
@@ -34,7 +34,7 @@ Commands:
|
||||
Usage: backstage-cli build-workspace [options] <workspace-dir> [packages...]
|
||||
|
||||
Options:
|
||||
--alwaysYarnPack
|
||||
--alwaysPack
|
||||
-h, --help
|
||||
```
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ const envOptions = {
|
||||
};
|
||||
|
||||
try {
|
||||
require.resolve('react-dom/client');
|
||||
require.resolve('react-dom/client', {
|
||||
paths: [paths.targetRoot],
|
||||
});
|
||||
process.env.HAS_REACT_DOM_CLIENT = true;
|
||||
} catch {
|
||||
/* ignored */
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"ts-morph": "^23.0.0",
|
||||
"util": "^0.12.3",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-dev-server": "^5.0.0",
|
||||
"yaml": "^2.0.0",
|
||||
"yargs": "^16.2.0",
|
||||
|
||||
@@ -18,7 +18,7 @@ import fs from 'fs-extra';
|
||||
import { createDistWorkspace } from '../lib/packager';
|
||||
|
||||
type Options = {
|
||||
alwaysYarnPack?: boolean;
|
||||
alwaysPack?: boolean;
|
||||
};
|
||||
|
||||
export default async (dir: string, packages: string[], options: Options) => {
|
||||
@@ -28,7 +28,7 @@ export default async (dir: string, packages: string[], options: Options) => {
|
||||
|
||||
await createDistWorkspace(packages, {
|
||||
targetDir: dir,
|
||||
alwaysYarnPack: options.alwaysYarnPack,
|
||||
alwaysPack: options.alwaysPack,
|
||||
enableFeatureDetection: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { Command, Option } from 'commander';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { exitWithError } from '../lib/errors';
|
||||
|
||||
@@ -375,8 +375,16 @@ export function registerCommands(program: Command) {
|
||||
|
||||
program
|
||||
.command('build-workspace <workspace-dir> [packages...]')
|
||||
.addOption(
|
||||
new Option(
|
||||
'--alwaysYarnPack',
|
||||
'Alias for --alwaysPack for backwards compatibility.',
|
||||
)
|
||||
.implies({ alwaysPack: true })
|
||||
.hideHelp(true),
|
||||
)
|
||||
.option(
|
||||
'--alwaysYarnPack',
|
||||
'--alwaysPack',
|
||||
'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
|
||||
)
|
||||
.description('Builds a temporary dist workspace from the provided packages')
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import { createHash } from 'crypto';
|
||||
import { relative as relativePath, resolve as resolvePath } from 'path';
|
||||
import { relative as relativePath } from 'path';
|
||||
import {
|
||||
PackageGraph,
|
||||
BackstagePackageJson,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
import { paths } from '../../lib/paths';
|
||||
import { runWorkerQueueThreads } from '../../lib/parallel';
|
||||
import { createScriptOptionsParser } from './optionsParser';
|
||||
import { SuccessCache } from '../../lib/cache/SuccessCache';
|
||||
|
||||
function depCount(pkg: BackstagePackageJson) {
|
||||
const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0;
|
||||
@@ -36,39 +36,13 @@ 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 cache = new SuccessCache('lint', opts.successCacheDir);
|
||||
const cacheContext = opts.successCache
|
||||
? {
|
||||
cache: await readCache(cacheDir),
|
||||
entries: await cache.read(),
|
||||
lockfile: await Lockfile.load(paths.resolveTargetRoot('yarn.lock')),
|
||||
}
|
||||
: undefined;
|
||||
@@ -136,7 +110,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
fix: Boolean(opts.fix),
|
||||
format: opts.format as string | undefined,
|
||||
shouldCache: Boolean(cacheContext),
|
||||
successCache: cacheContext?.cache,
|
||||
successCache: cacheContext?.entries,
|
||||
rootDir: paths.targetRoot,
|
||||
},
|
||||
workerFactory: async ({
|
||||
@@ -202,7 +176,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
hash.update('\0');
|
||||
}
|
||||
sha = await hash.digest('hex');
|
||||
if (successCache?.includes(sha)) {
|
||||
if (successCache?.has(sha)) {
|
||||
console.log(`Skipped ${relativeDir} due to cache hit`);
|
||||
return { relativeDir, sha, failed: false };
|
||||
}
|
||||
@@ -262,7 +236,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
|
||||
if (cacheContext) {
|
||||
await writeCache(cacheDir, outputSuccessCache);
|
||||
await cache.write(outputSuccessCache);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
|
||||
import os from 'os';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'fs-extra';
|
||||
import yargs from 'yargs';
|
||||
import { resolve as resolvePath, relative as relativePath } from 'path';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { Lockfile, PackageGraph } from '@backstage/cli-node';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { runCheck, runPlain } from '../../lib/run';
|
||||
import { isChildPath } from '@backstage/cli-common';
|
||||
import { SuccessCache } from '../../lib/cache/SuccessCache';
|
||||
|
||||
type JestProject = {
|
||||
displayName: string;
|
||||
@@ -50,30 +50,6 @@ interface GlobalWithCache extends Global {
|
||||
};
|
||||
}
|
||||
|
||||
const CACHE_FILE_NAME = 'test-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;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(dir: string, cache: Cache) {
|
||||
fs.mkdirpSync(dir);
|
||||
fs.writeJsonSync(resolvePath(dir, CACHE_FILE_NAME), cache, { spaces: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Use git to get the HEAD tree hashes of each package in the project.
|
||||
*/
|
||||
@@ -272,10 +248,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
removeOptionArg(args, '--successCache', 1);
|
||||
removeOptionArg(args, '--successCacheDir');
|
||||
|
||||
const cacheDir = resolvePath(
|
||||
opts.successCacheDir ?? 'node_modules/.cache/backstage-cli',
|
||||
);
|
||||
|
||||
// Parse the args to ensure that no file filters are provided, in which case we refuse to run
|
||||
const { _: parsedArgs } = await yargs(args).options(jestCli.yargsOptions)
|
||||
.argv;
|
||||
@@ -293,6 +265,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const cache = new SuccessCache('test', opts.successCacheDir);
|
||||
const graph = await getPackageGraph();
|
||||
|
||||
// Shared state for the bridge
|
||||
@@ -305,7 +278,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
globalWithCache.__backstageCli_jestSuccessCache = {
|
||||
// This is called by `config/jest.js` after the project configs have been gathered
|
||||
async filterConfigs(projectConfigs, globalRootConfig) {
|
||||
const cache = await readCache(cacheDir);
|
||||
const cacheEntries = await cache.read();
|
||||
const lockfile = await Lockfile.load(
|
||||
paths.resolveTargetRoot('yarn.lock'),
|
||||
);
|
||||
@@ -350,7 +323,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
|
||||
projectHashes.set(packageName, sha);
|
||||
|
||||
if (cache?.includes(sha)) {
|
||||
if (cacheEntries.has(sha)) {
|
||||
if (!selectedProjects || selectedProjects.includes(packageName)) {
|
||||
console.log(`Skipped ${packageName} due to cache hit`);
|
||||
}
|
||||
@@ -391,7 +364,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await writeCache(cacheDir, outputSuccessCache);
|
||||
await cache.write(outputSuccessCache);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
|
||||
const DEFAULT_CACHE_BASE_PATH = 'node_modules/.cache/backstage-cli';
|
||||
|
||||
const CACHE_MAX_AGE_MS = 7 * 24 * 3600_000;
|
||||
|
||||
export class SuccessCache {
|
||||
readonly #path: string;
|
||||
|
||||
constructor(name: string, basePath?: string) {
|
||||
this.#path = resolvePath(basePath ?? DEFAULT_CACHE_BASE_PATH, name);
|
||||
}
|
||||
|
||||
async read(): Promise<Set<string>> {
|
||||
try {
|
||||
const stat = await fs.stat(this.#path);
|
||||
if (!stat.isDirectory()) {
|
||||
await fs.rm(this.#path);
|
||||
return new Set();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return new Set();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const items = await fs.readdir(this.#path);
|
||||
|
||||
const returned = new Set<string>();
|
||||
const removed = new Set<string>();
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (const item of items) {
|
||||
const split = item.split('_');
|
||||
if (split.length !== 2) {
|
||||
removed.add(item);
|
||||
continue;
|
||||
}
|
||||
const createdAt = parseInt(split[0], 10);
|
||||
if (Number.isNaN(createdAt) || now - createdAt > CACHE_MAX_AGE_MS) {
|
||||
removed.add(item);
|
||||
} else {
|
||||
returned.add(split[1]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of removed) {
|
||||
await fs.unlink(resolvePath(this.#path, item));
|
||||
}
|
||||
|
||||
return returned;
|
||||
}
|
||||
|
||||
async write(newEntries: Iterable<string>): Promise<void> {
|
||||
const now = Date.now();
|
||||
|
||||
await fs.ensureDir(this.#path);
|
||||
|
||||
const existingItems = await fs.readdir(this.#path);
|
||||
|
||||
const empty = Buffer.alloc(0);
|
||||
for (const key of newEntries) {
|
||||
// Remove any existing items with the key we're about to add
|
||||
const trimmedItems = existingItems.filter(item =>
|
||||
item.endsWith(`_${key}`),
|
||||
);
|
||||
for (const trimmedItem of trimmedItems) {
|
||||
await fs.unlink(resolvePath(this.#path, trimmedItem));
|
||||
}
|
||||
|
||||
await fs.writeFile(resolvePath(this.#path, `${now}_${key}`), empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ type Options = {
|
||||
* workspace. This ensures correct workspace output at significant cost to
|
||||
* command performance.
|
||||
*/
|
||||
alwaysYarnPack?: boolean;
|
||||
alwaysPack?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, the TypeScript feature detection will be enabled, which
|
||||
@@ -242,7 +242,7 @@ export async function createDistWorkspace(
|
||||
await moveToDistWorkspace(
|
||||
targetDir,
|
||||
targets,
|
||||
Boolean(options.alwaysYarnPack),
|
||||
Boolean(options.alwaysPack),
|
||||
Boolean(options.enableFeatureDetection),
|
||||
);
|
||||
|
||||
@@ -286,13 +286,13 @@ const FAST_PACK_SCRIPTS = [
|
||||
async function moveToDistWorkspace(
|
||||
workspaceDir: string,
|
||||
localPackages: PackageGraphNode[],
|
||||
alwaysYarnPack: boolean,
|
||||
alwaysPack: boolean,
|
||||
enableFeatureDetection: boolean,
|
||||
): Promise<void> {
|
||||
const [fastPackPackages, slowPackPackages] = partition(
|
||||
localPackages,
|
||||
pkg =>
|
||||
!alwaysYarnPack &&
|
||||
!alwaysPack &&
|
||||
FAST_PACK_SCRIPTS.includes(pkg.packageJson.scripts?.prepack),
|
||||
);
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"@stoplight/types": "^14.0.0",
|
||||
"@useoptic/openapi-utilities": "^0.55.0",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"codeowners-utils": "^1.0.2",
|
||||
"command-exists": "^1.2.9",
|
||||
"commander": "^12.0.0",
|
||||
|
||||
@@ -58,6 +58,8 @@ function registerPackageCommand(program: Command) {
|
||||
.description(
|
||||
'Additional properties that can be passed to @openapitools/openapi-generator-cli',
|
||||
)
|
||||
.option('--watch')
|
||||
.description('Watch the OpenAPI spec for changes and regenerate on save.')
|
||||
.action(
|
||||
lazy(() =>
|
||||
import('./package/schema/openapi/generate').then(m => m.command),
|
||||
|
||||
@@ -30,6 +30,7 @@ import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'
|
||||
async function generate(
|
||||
outputDirectory: string,
|
||||
clientAdditionalProperties?: string,
|
||||
abortSignal?: AbortController,
|
||||
) {
|
||||
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
|
||||
const resolvedOutputDirectory = cliPaths.resolveTargetRoot(
|
||||
@@ -69,6 +70,7 @@ async function generate(
|
||||
additionalProperties,
|
||||
],
|
||||
{
|
||||
signal: abortSignal?.signal,
|
||||
maxBuffer: Number.MAX_VALUE,
|
||||
cwd: resolvePackagePath('@backstage/repo-tools'),
|
||||
env: {
|
||||
@@ -79,11 +81,15 @@ async function generate(
|
||||
|
||||
await exec(
|
||||
`yarn backstage-cli package lint --fix ${resolvedOutputDirectory}`,
|
||||
[],
|
||||
{ signal: abortSignal?.signal },
|
||||
);
|
||||
|
||||
const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier');
|
||||
if (prettier) {
|
||||
await exec(`${prettier} --write ${resolvedOutputDirectory}`);
|
||||
await exec(`${prettier} --write ${resolvedOutputDirectory}`, [], {
|
||||
signal: abortSignal?.signal,
|
||||
});
|
||||
}
|
||||
|
||||
fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore'));
|
||||
@@ -97,17 +103,29 @@ async function generate(
|
||||
export async function command(
|
||||
outputPackage: string,
|
||||
clientAdditionalProperties?: string,
|
||||
{
|
||||
abortSignal,
|
||||
isWatch = false,
|
||||
}: { abortSignal?: AbortController; isWatch?: boolean } = {},
|
||||
): Promise<void> {
|
||||
try {
|
||||
await generate(outputPackage, clientAdditionalProperties);
|
||||
await generate(outputPackage, clientAdditionalProperties, abortSignal);
|
||||
console.log(
|
||||
chalk.green(`Generated client in ${outputPackage}/${OUTPUT_PATH}`),
|
||||
);
|
||||
} catch (err) {
|
||||
console.log();
|
||||
console.log(chalk.red(`Client generation failed:`));
|
||||
console.log(err);
|
||||
|
||||
process.exit(1);
|
||||
if (err.name === 'AbortError') {
|
||||
console.debug('Server generation aborted.');
|
||||
return;
|
||||
}
|
||||
if (isWatch) {
|
||||
console.log(chalk.red(`Client generation failed:`));
|
||||
console.group();
|
||||
console.log(chalk.red(err.message));
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.log(chalk.red(`Client generation failed.`));
|
||||
console.log(chalk.red(err.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import path from 'path';
|
||||
|
||||
jest.mock(
|
||||
'lodash',
|
||||
() =>
|
||||
({
|
||||
...jest.requireActual('lodash'),
|
||||
debounce: (fn: any) => fn,
|
||||
} as any as typeof import('lodash')),
|
||||
);
|
||||
|
||||
describe('generateOpenApiSchema', () => {
|
||||
const inputDir = createMockDirectory();
|
||||
const outputDir = createMockDirectory();
|
||||
|
||||
beforeEach(() => {
|
||||
inputDir.clear();
|
||||
outputDir.clear();
|
||||
|
||||
inputDir.addContent({
|
||||
'openapi.yaml': `
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Test API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/test:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
`,
|
||||
});
|
||||
jest.mock('../../../../../lib/openapi/helpers', () => ({
|
||||
getPathToCurrentOpenApiSpec: jest.fn(() =>
|
||||
Promise.resolve(path.join(inputDir.path, 'openapi.yaml')),
|
||||
),
|
||||
loadAndValidateOpenApiYaml: jest.fn(),
|
||||
}));
|
||||
});
|
||||
it('should handle watch mode', async () => {
|
||||
const generateClientMock = jest.fn();
|
||||
const generateServerMock = jest.fn();
|
||||
jest.doMock('./client', () => ({ command: generateClientMock }));
|
||||
jest.doMock('./server', () => ({ command: generateServerMock }));
|
||||
|
||||
const mockWatch = jest.fn();
|
||||
jest.mock('chokidar', () => ({
|
||||
watch: mockWatch,
|
||||
}));
|
||||
let resolve: (val?: any) => void;
|
||||
const block = () =>
|
||||
new Promise(res => {
|
||||
resolve = res;
|
||||
});
|
||||
jest.mock('../../../../../lib/runner', () => ({
|
||||
block,
|
||||
}));
|
||||
|
||||
const mockOn: Record<string, any> = {};
|
||||
mockWatch.mockReturnValue({
|
||||
on: jest.fn((event, cb) => {
|
||||
console.log(event);
|
||||
mockOn[event] = cb;
|
||||
}),
|
||||
} as any);
|
||||
|
||||
const { command } = await import('./index');
|
||||
|
||||
const actions = async () => {
|
||||
while (!mockOn.ready) {
|
||||
// Wait for the watcher to be registered
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
expect(generateClientMock).toHaveBeenCalledTimes(1);
|
||||
mockOn.change();
|
||||
// Wait for the debounce to finish with the initial load.
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
expect(generateClientMock).toHaveBeenCalledTimes(2);
|
||||
resolve();
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
actions(),
|
||||
command({ watch: true, clientPackage: 'test123' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,13 @@ import chalk from 'chalk';
|
||||
import { OptionValues } from 'commander';
|
||||
import { command as generateClient } from './client';
|
||||
import { command as generateServer } from './server';
|
||||
import chokidar from 'chokidar';
|
||||
import {
|
||||
getPathToCurrentOpenApiSpec,
|
||||
loadAndValidateOpenApiYaml,
|
||||
} from '../../../../../lib/openapi/helpers';
|
||||
import { debounce } from 'lodash';
|
||||
import { block } from '../../../../../lib/runner';
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
if (!opts.clientPackage && !opts.server) {
|
||||
@@ -25,10 +32,70 @@ export async function command(opts: OptionValues) {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.clientPackage) {
|
||||
await generateClient(opts.clientPackage, opts.clientAdditionalProperties);
|
||||
}
|
||||
if (opts.server) {
|
||||
await generateServer();
|
||||
|
||||
const sharedCommand = async (abortSignal?: AbortController) => {
|
||||
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
|
||||
await loadAndValidateOpenApiYaml(resolvedOpenapiPath);
|
||||
const promises = [];
|
||||
const options = {
|
||||
isWatch: opts.watch,
|
||||
abortSignal,
|
||||
};
|
||||
if (opts.clientPackage) {
|
||||
promises.push(
|
||||
generateClient(
|
||||
opts.clientPackage,
|
||||
opts.clientAdditionalProperties,
|
||||
options,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (opts.server) {
|
||||
promises.push(generateServer(options));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
if (opts.watch) {
|
||||
try {
|
||||
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
|
||||
let abortController = new AbortController();
|
||||
const watcher = chokidar.watch(resolvedOpenapiPath);
|
||||
|
||||
// The generate command currently takes ~8 seconds to run, so let's debounce calling it so we don't have to cancel it so much.
|
||||
const debouncedCommand = debounce(() => {
|
||||
console.log('Detected changes! Regenerating...');
|
||||
abortController.abort();
|
||||
abortController = new AbortController();
|
||||
sharedCommand(abortController).catch(err => {
|
||||
console.error(chalk.red('Error: ', err));
|
||||
});
|
||||
}, 500);
|
||||
|
||||
watcher.on('change', () => {
|
||||
debouncedCommand();
|
||||
});
|
||||
watcher.on('error', error => {
|
||||
console.error('Error happened', error);
|
||||
});
|
||||
|
||||
watcher.on('ready', async () => {
|
||||
console.log(
|
||||
'Watching for changes in OpenAPI spec. Press Ctrl+C to stop.',
|
||||
);
|
||||
});
|
||||
|
||||
debouncedCommand();
|
||||
await block();
|
||||
} catch (err) {
|
||||
console.error(chalk.red('Error: ', err));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await sharedCommand();
|
||||
} catch (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function generate() {
|
||||
async function generate(abortSignal?: AbortController) {
|
||||
const openapiPath = await getPathToCurrentOpenApiSpec();
|
||||
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
|
||||
|
||||
@@ -50,21 +50,40 @@ export const createOpenApiRouter = async (
|
||||
`,
|
||||
);
|
||||
|
||||
await exec(`yarn backstage-cli package lint --fix ${tsPath}`);
|
||||
await exec(`yarn backstage-cli package lint --fix ${tsPath}`, {
|
||||
signal: abortSignal?.signal,
|
||||
});
|
||||
if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) {
|
||||
await exec(`yarn prettier --write ${tsPath}`, {
|
||||
cwd: cliPaths.targetRoot,
|
||||
signal: abortSignal?.signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function command(): Promise<void> {
|
||||
export async function command({
|
||||
abortSignal,
|
||||
isWatch = false,
|
||||
}: {
|
||||
abortSignal?: AbortController;
|
||||
isWatch?: boolean;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await generate();
|
||||
console.log(chalk.green('Generated all files.'));
|
||||
await generate(abortSignal);
|
||||
console.log(chalk.green('Generated server files.'));
|
||||
} catch (err) {
|
||||
console.log(chalk.red(`OpenAPI server stub generation failed.`));
|
||||
console.log(err.message);
|
||||
process.exit(1);
|
||||
if (err.name === 'AbortError') {
|
||||
console.debug('Server generation aborted.');
|
||||
return;
|
||||
}
|
||||
if (isWatch) {
|
||||
console.log(chalk.red(`Server generation failed:`));
|
||||
console.group();
|
||||
console.log(chalk.red(err.message));
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.log(chalk.red(err.message));
|
||||
console.log(chalk.red(`OpenAPI server stub generation failed.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import YAML from 'js-yaml';
|
||||
import { isEqual, cloneDeep } from 'lodash';
|
||||
import { isEqual } from 'lodash';
|
||||
import { join } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { relative as relativePath, resolve as resolvePath } from 'path';
|
||||
import Parser from '@apidevtools/swagger-parser';
|
||||
import { runner } from '../../../../lib/runner';
|
||||
import { paths as cliPaths } from '../../../../lib/paths';
|
||||
import {
|
||||
@@ -28,7 +26,10 @@ import {
|
||||
TS_SCHEMA_PATH,
|
||||
YAML_SCHEMA_PATH,
|
||||
} from '../../../../lib/openapi/constants';
|
||||
import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers';
|
||||
import {
|
||||
getPathToOpenApiSpec,
|
||||
loadAndValidateOpenApiYaml,
|
||||
} from '../../../../lib/openapi/helpers';
|
||||
|
||||
async function verify(directoryPath: string) {
|
||||
let openapiPath = '';
|
||||
@@ -38,8 +39,7 @@ async function verify(directoryPath: string) {
|
||||
// Unable to find spec at path.
|
||||
return;
|
||||
}
|
||||
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
|
||||
await Parser.validate(cloneDeep(yaml) as any);
|
||||
const yaml = await loadAndValidateOpenApiYaml(openapiPath);
|
||||
|
||||
const schemaPath = join(directoryPath, TS_SCHEMA_PATH);
|
||||
if (!(await fs.pathExists(schemaPath))) {
|
||||
|
||||
@@ -18,6 +18,10 @@ import { pathExists } from 'fs-extra';
|
||||
import { paths } from '../paths';
|
||||
import { YAML_SCHEMA_PATH } from './constants';
|
||||
import { resolve } from 'path';
|
||||
import YAML from 'js-yaml';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import Parser from '@apidevtools/swagger-parser';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export const getPathToFile = async (directory: string, filename: string) => {
|
||||
return resolve(directory, filename);
|
||||
@@ -41,3 +45,9 @@ export const getPathToOpenApiSpec = async (directory: string) => {
|
||||
export const getPathToCurrentOpenApiSpec = async () => {
|
||||
return await assertExists(await getRelativePathToFile(YAML_SCHEMA_PATH));
|
||||
};
|
||||
|
||||
export async function loadAndValidateOpenApiYaml(path: string) {
|
||||
const yaml = YAML.load(await fs.readFile(path, 'utf8'));
|
||||
await Parser.validate(cloneDeep(yaml) as any);
|
||||
return yaml;
|
||||
}
|
||||
|
||||
@@ -67,3 +67,7 @@ export async function runner(
|
||||
|
||||
return resultsList;
|
||||
}
|
||||
|
||||
export async function block() {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
@@ -3809,7 +3809,6 @@ __metadata:
|
||||
fs-extra: ^11.0.0
|
||||
keyv: ^4.5.2
|
||||
knex: ^3.0.0
|
||||
msw: ^1.0.0
|
||||
mysql2: ^3.0.0
|
||||
pg: ^8.11.3
|
||||
pg-connection-string: ^2.3.0
|
||||
@@ -4029,7 +4028,7 @@ __metadata:
|
||||
vite: ^5.0.0
|
||||
vite-plugin-html: ^3.2.2
|
||||
vite-plugin-node-polyfills: ^0.22.0
|
||||
webpack: ^5.70.0
|
||||
webpack: ^5.94.0
|
||||
webpack-dev-server: ^5.0.0
|
||||
yaml: ^2.0.0
|
||||
yargs: ^16.2.0
|
||||
@@ -8310,6 +8309,7 @@ __metadata:
|
||||
"@types/prettier": ^2.0.0
|
||||
"@useoptic/openapi-utilities": ^0.55.0
|
||||
chalk: ^4.0.0
|
||||
chokidar: ^3.5.3
|
||||
codeowners-utils: ^1.0.2
|
||||
command-exists: ^1.2.9
|
||||
commander: ^12.0.0
|
||||
@@ -18122,9 +18122,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash@npm:^4.14.151":
|
||||
version: 4.17.10
|
||||
resolution: "@types/lodash@npm:4.17.10"
|
||||
checksum: 4600f2f25270c8fee6953e363d318149a5f0f1b1bb820aa2f42d7ada6e4f7de31848bb5ffc2c687b40bd73aa982167bdd6e6d8d456e72abe0c660ec77d1fa7e9
|
||||
version: 4.17.12
|
||||
resolution: "@types/lodash@npm:4.17.12"
|
||||
checksum: 7b564e4114f09ce5ae31a2e9493592baf20bb498507f3705c5d91cf838c2298b4f6a06f2d6c8dc608fcac63e210a2b7b13388c7a5e220e15688f813521030127
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18289,11 +18289,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:>=13.7.0":
|
||||
version: 22.7.6
|
||||
resolution: "@types/node@npm:22.7.6"
|
||||
version: 22.7.7
|
||||
resolution: "@types/node@npm:22.7.7"
|
||||
dependencies:
|
||||
undici-types: ~6.19.2
|
||||
checksum: 6afe2a1bd70ee0afa76c904ffdb04c634366987103bb2bef2d5b036f3bb49a4e828bb5e9fc64af442a76aaac4896df9277e725c843d85375d135aa71eb2fe3d9
|
||||
checksum: 70492e46d92bf00b537c8700322ad001c1f8d4fc65fc1627064a91c7edebb8ad18730b95a3c6bdff212dd0252a337a4b92e4cfcbc21c9f08616302e87ea855b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18326,20 +18326,20 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18, @types/node@npm:^18.11.9, @types/node@npm:^18.17.15, @types/node@npm:^18.17.8":
|
||||
version: 18.19.56
|
||||
resolution: "@types/node@npm:18.19.56"
|
||||
version: 18.19.57
|
||||
resolution: "@types/node@npm:18.19.57"
|
||||
dependencies:
|
||||
undici-types: ~5.26.4
|
||||
checksum: 2660aa2e50ddb77e4ea4e29e6b917e942ca49ecbd46b6333a7aeffdffafc5d7e48f8a731b6570a61bebdd0ba8dc104a250c432bea00e70aec5a4cb6e6f2df771
|
||||
checksum: d7be072513df5f6b14384dc284561ddf4e6393ce85b8fd3cc9ab0c0a7abd03bfdab6519548dab7c743ec9dfca9ce686dde2db0fbbf1d357e0822b4f99a54b193
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20.1.1, @types/node@npm:^20.11.16":
|
||||
version: 20.16.12
|
||||
resolution: "@types/node@npm:20.16.12"
|
||||
version: 20.16.13
|
||||
resolution: "@types/node@npm:20.16.13"
|
||||
dependencies:
|
||||
undici-types: ~6.19.2
|
||||
checksum: 648b2a35713157f1d5861123c29546cf316b543afd536ff2c02234d78a99cea07d2259559efd7f58cf2c45c6a2e80c1064ff03c5b5213bb99b91a7d598b7975f
|
||||
checksum: 2e998ac5ae133b1a4261dc89d0034e63834d285bb2124901b9d805e7b98c0f809fc2586fd1bf7ad9a0f0348bb503fac6c41612a3ca272334b943070a3e82227c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -44713,7 +44713,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webpack@npm:^5, webpack@npm:^5.70.0":
|
||||
"webpack@npm:^5, webpack@npm:^5.94.0":
|
||||
version: 5.95.0
|
||||
resolution: "webpack@npm:5.95.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user