Merge branch 'master' into storybook8

This commit is contained in:
Charles de Dreuille
2024-10-20 18:38:13 +01:00
29 changed files with 441 additions and 122 deletions
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Added `--alwaysPack` as a replacement for the now hidden `--alwaysYarnPack` flag for the `build-workspace` command.
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Removed unused `msw` dependency.
+5
View File
@@ -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.
+5
View File
@@ -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
+4 -1
View File
@@ -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
-1
View File
@@ -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",
+1 -1
View File
@@ -34,7 +34,7 @@ Commands:
Usage: backstage-cli build-workspace [options] <workspace-dir> [packages...]
Options:
--alwaysYarnPack
--alwaysPack
-h, --help
```
+3 -1
View File
@@ -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 */
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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,
});
};
+10 -2
View File
@@ -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')
+7 -33
View File
@@ -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) {
+6 -33
View File
@@ -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
View File
@@ -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),
);
+1
View File
@@ -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;
}
+4
View File
@@ -67,3 +67,7 @@ export async function runner(
return resultsList;
}
export async function block() {
return new Promise(() => {});
}
+15 -15
View File
@@ -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: