repo-tools: refactor cli reports
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -15,12 +15,7 @@
|
||||
*/
|
||||
|
||||
import { groupBy } from 'lodash';
|
||||
import {
|
||||
basename,
|
||||
join,
|
||||
relative as relativePath,
|
||||
resolve as resolvePath,
|
||||
} from 'path';
|
||||
import { join, relative as relativePath, resolve as resolvePath } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
CompilerState,
|
||||
@@ -1266,198 +1261,6 @@ export async function categorizePackageDirs(packageDirs: string[]) {
|
||||
return { tsPackageDirs, cliPackageDirs, sqlPackageDirs };
|
||||
}
|
||||
|
||||
function parseHelpPage(helpPageContent: string) {
|
||||
const [, usage] = helpPageContent.match(/^\s*Usage: (.*)$/im) ?? [];
|
||||
const lines = helpPageContent.split(/\r?\n/);
|
||||
|
||||
let options = new Array<string>();
|
||||
let commands = new Array<string>();
|
||||
let commandArguments = new Array<string>();
|
||||
|
||||
while (lines.length > 0) {
|
||||
while (lines.length > 0 && !lines[0].endsWith(':')) {
|
||||
lines.shift();
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
// Start of a new section, e.g. "Options:"
|
||||
const sectionName = lines.shift();
|
||||
// Take lines until we hit the next section or the end
|
||||
const sectionEndIndex = lines.findIndex(
|
||||
line => line && !line.match(/^\s/),
|
||||
);
|
||||
const sectionLines = lines.slice(0, sectionEndIndex);
|
||||
lines.splice(0, sectionLines.length);
|
||||
|
||||
// Trim away documentation
|
||||
const sectionItems = sectionLines
|
||||
.map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1])
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (sectionName?.toLocaleLowerCase('en-US') === 'options:') {
|
||||
options = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') {
|
||||
commands = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') {
|
||||
commandArguments = sectionItems;
|
||||
} else {
|
||||
throw new Error(`Unknown CLI section: ${sectionName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
usage,
|
||||
options,
|
||||
commands,
|
||||
commandArguments,
|
||||
};
|
||||
}
|
||||
|
||||
// Represents the help page os a CLI command
|
||||
interface CliHelpPage {
|
||||
// Path of commands to reach this page
|
||||
path: string[];
|
||||
// Parsed content
|
||||
usage: string | undefined;
|
||||
options: string[];
|
||||
commands: string[];
|
||||
commandArguments: string[];
|
||||
}
|
||||
|
||||
async function exploreCliHelpPages(
|
||||
run: (...args: string[]) => Promise<string>,
|
||||
): Promise<CliHelpPage[]> {
|
||||
const helpPages = new Array<CliHelpPage>();
|
||||
|
||||
async function exploreHelpPage(...path: string[]) {
|
||||
const content = await run(...path, '--help');
|
||||
const parsed = parseHelpPage(content);
|
||||
helpPages.push({ path, ...parsed });
|
||||
|
||||
await Promise.all(
|
||||
parsed.commands.map(async fullCommand => {
|
||||
const command = fullCommand.split(/[|\s]/)[0];
|
||||
if (command !== 'help') {
|
||||
await exploreHelpPage(...path, command);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await exploreHelpPage();
|
||||
|
||||
helpPages.sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')));
|
||||
|
||||
return helpPages;
|
||||
}
|
||||
|
||||
// The API model for a CLI entry point
|
||||
interface CliModel {
|
||||
name: string;
|
||||
helpPages: CliHelpPage[];
|
||||
}
|
||||
|
||||
function generateCliReport(name: string, models: CliModel[]): string {
|
||||
const content = [
|
||||
`## CLI Report file for "${name}"`,
|
||||
'',
|
||||
'> Do not edit this file. It is a report generated by `yarn build:api-reports`',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const model of models) {
|
||||
for (const helpPage of model.helpPages) {
|
||||
content.push(
|
||||
`### \`${[model.name, ...helpPage.path].join(' ')}\``,
|
||||
'',
|
||||
'```',
|
||||
`Usage: ${helpPage.usage ?? '<none>'}`,
|
||||
);
|
||||
|
||||
if (helpPage.options.length > 0) {
|
||||
content.push('', 'Options:', ...helpPage.options.map(l => ` ${l}`));
|
||||
}
|
||||
|
||||
if (helpPage.commands.length > 0) {
|
||||
content.push('', 'Commands:', ...helpPage.commands.map(l => ` ${l}`));
|
||||
}
|
||||
content.push('```', '');
|
||||
}
|
||||
}
|
||||
|
||||
return content.join('\n');
|
||||
}
|
||||
|
||||
interface CliExtractionOptions {
|
||||
packageDirs: string[];
|
||||
isLocalBuild: boolean;
|
||||
}
|
||||
|
||||
export async function runCliExtraction({
|
||||
packageDirs,
|
||||
isLocalBuild,
|
||||
}: CliExtractionOptions) {
|
||||
for (const packageDir of packageDirs) {
|
||||
console.log(`## Processing ${packageDir}`);
|
||||
const fullDir = cliPaths.resolveTargetRoot(packageDir);
|
||||
const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json'));
|
||||
|
||||
if (!pkgJson.bin) {
|
||||
throw new Error(`CLI Package in ${packageDir} has no bin field`);
|
||||
}
|
||||
|
||||
const models = new Array<CliModel>();
|
||||
if (typeof pkgJson.bin === 'string') {
|
||||
const run = createBinRunner(fullDir, pkgJson.bin);
|
||||
const helpPages = await exploreCliHelpPages(run);
|
||||
models.push({ name: basename(pkgJson.bin), helpPages });
|
||||
} else {
|
||||
for (const [name, path] of Object.entries<string>(pkgJson.bin)) {
|
||||
const run = createBinRunner(fullDir, path);
|
||||
const helpPages = await exploreCliHelpPages(run);
|
||||
models.push({ name, helpPages });
|
||||
}
|
||||
}
|
||||
|
||||
const report = generateCliReport(pkgJson.name, models);
|
||||
|
||||
const reportPath = resolvePath(fullDir, 'cli-report.md');
|
||||
const existingReport = await fs
|
||||
.readFile(reportPath, 'utf8')
|
||||
.catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (existingReport !== report) {
|
||||
if (isLocalBuild) {
|
||||
console.warn(`CLI report changed for ${packageDir}`);
|
||||
await fs.writeFile(reportPath, report);
|
||||
} else {
|
||||
logApiReportInstructions();
|
||||
|
||||
if (existingReport) {
|
||||
console.log('');
|
||||
console.log(
|
||||
`The conflicting file is ${relativePath(
|
||||
cliPaths.targetRoot,
|
||||
reportPath,
|
||||
)}, expecting the following content:`,
|
||||
);
|
||||
console.log('');
|
||||
|
||||
console.log(report);
|
||||
|
||||
logApiReportInstructions();
|
||||
}
|
||||
throw new Error(`CLI report changed for ${packageDir}, `);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface KnipExtractionOptions {
|
||||
packageDirs: string[];
|
||||
isLocalBuild: boolean;
|
||||
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
buildDocs,
|
||||
categorizePackageDirs,
|
||||
runApiExtraction,
|
||||
runCliExtraction,
|
||||
} from './api-extractor';
|
||||
|
||||
import { buildApiReports } from './api-reports';
|
||||
import { generateTypeDeclarations } from './generateTypeDeclarations';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { runCliExtraction } from './cli-reports';
|
||||
|
||||
jest.mock('./generateTypeDeclarations');
|
||||
// create mocks for the dependencies of the `buildApiReports` function
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
categorizePackageDirs,
|
||||
createTemporaryTsConfig,
|
||||
runApiExtraction,
|
||||
runCliExtraction,
|
||||
} from './api-extractor';
|
||||
import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths';
|
||||
import { generateTypeDeclarations } from './generateTypeDeclarations';
|
||||
import { runSqlExtraction } from './sql-reports';
|
||||
import { runCliExtraction } from './cli-reports';
|
||||
|
||||
type Options = {
|
||||
ci?: boolean;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { CliModel } from './types';
|
||||
|
||||
export function generateCliReport(options: {
|
||||
packageName: string;
|
||||
models: CliModel[];
|
||||
}): string {
|
||||
const content = [
|
||||
`## CLI Report file for "${options.packageName}"`,
|
||||
'',
|
||||
'> Do not edit this file. It is a report generated by `yarn build:api-reports`',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const model of options.models) {
|
||||
for (const helpPage of model.helpPages) {
|
||||
content.push(
|
||||
`### \`${[model.name, ...helpPage.path].join(' ')}\``,
|
||||
'',
|
||||
'```',
|
||||
`Usage: ${helpPage.usage ?? '<none>'}`,
|
||||
);
|
||||
|
||||
if (helpPage.options.length > 0) {
|
||||
content.push('', 'Options:', ...helpPage.options.map(l => ` ${l}`));
|
||||
}
|
||||
|
||||
if (helpPage.commands.length > 0) {
|
||||
content.push('', 'Commands:', ...helpPage.commands.map(l => ` ${l}`));
|
||||
}
|
||||
content.push('```', '');
|
||||
}
|
||||
}
|
||||
|
||||
return content.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { runCliExtraction } from './runCliExtraction';
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 {
|
||||
basename,
|
||||
resolve as resolvePath,
|
||||
relative as relativePath,
|
||||
} from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { createBinRunner } from '../../util';
|
||||
import { CliHelpPage, CliModel } from './types';
|
||||
import { paths as cliPaths } from '../../../lib/paths';
|
||||
import { generateCliReport } from './generateCliReport';
|
||||
import { logApiReportInstructions } from '../api-extractor';
|
||||
|
||||
function parseHelpPage(helpPageContent: string) {
|
||||
const [, usage] = helpPageContent.match(/^\s*Usage: (.*)$/im) ?? [];
|
||||
const lines = helpPageContent.split(/\r?\n/);
|
||||
|
||||
let options = new Array<string>();
|
||||
let commands = new Array<string>();
|
||||
let commandArguments = new Array<string>();
|
||||
|
||||
while (lines.length > 0) {
|
||||
while (lines.length > 0 && !lines[0].endsWith(':')) {
|
||||
lines.shift();
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
// Start of a new section, e.g. "Options:"
|
||||
const sectionName = lines.shift();
|
||||
// Take lines until we hit the next section or the end
|
||||
const sectionEndIndex = lines.findIndex(
|
||||
line => line && !line.match(/^\s/),
|
||||
);
|
||||
const sectionLines = lines.slice(0, sectionEndIndex);
|
||||
lines.splice(0, sectionLines.length);
|
||||
|
||||
// Trim away documentation
|
||||
const sectionItems = sectionLines
|
||||
.map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1])
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (sectionName?.toLocaleLowerCase('en-US') === 'options:') {
|
||||
options = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') {
|
||||
commands = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') {
|
||||
commandArguments = sectionItems;
|
||||
} else {
|
||||
throw new Error(`Unknown CLI section: ${sectionName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
usage,
|
||||
options,
|
||||
commands,
|
||||
commandArguments,
|
||||
};
|
||||
}
|
||||
|
||||
async function exploreCliHelpPages(
|
||||
run: (...args: string[]) => Promise<string>,
|
||||
): Promise<CliHelpPage[]> {
|
||||
const helpPages = new Array<CliHelpPage>();
|
||||
|
||||
async function exploreHelpPage(...path: string[]) {
|
||||
const content = await run(...path, '--help');
|
||||
const parsed = parseHelpPage(content);
|
||||
helpPages.push({ path, ...parsed });
|
||||
|
||||
await Promise.all(
|
||||
parsed.commands.map(async fullCommand => {
|
||||
const command = fullCommand.split(/[|\s]/)[0];
|
||||
if (command !== 'help') {
|
||||
await exploreHelpPage(...path, command);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await exploreHelpPage();
|
||||
|
||||
helpPages.sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')));
|
||||
|
||||
return helpPages;
|
||||
}
|
||||
|
||||
interface CliExtractionOptions {
|
||||
packageDirs: string[];
|
||||
isLocalBuild: boolean;
|
||||
}
|
||||
|
||||
export async function runCliExtraction({
|
||||
packageDirs,
|
||||
isLocalBuild,
|
||||
}: CliExtractionOptions) {
|
||||
for (const packageDir of packageDirs) {
|
||||
console.log(`## Processing ${packageDir}`);
|
||||
const fullDir = cliPaths.resolveTargetRoot(packageDir);
|
||||
const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json'));
|
||||
|
||||
if (!pkgJson.bin) {
|
||||
throw new Error(`CLI Package in ${packageDir} has no bin field`);
|
||||
}
|
||||
|
||||
const models = new Array<CliModel>();
|
||||
if (typeof pkgJson.bin === 'string') {
|
||||
const run = createBinRunner(fullDir, pkgJson.bin);
|
||||
const helpPages = await exploreCliHelpPages(run);
|
||||
models.push({ name: basename(pkgJson.bin), helpPages });
|
||||
} else {
|
||||
for (const [name, path] of Object.entries<string>(pkgJson.bin)) {
|
||||
const run = createBinRunner(fullDir, path);
|
||||
const helpPages = await exploreCliHelpPages(run);
|
||||
models.push({ name, helpPages });
|
||||
}
|
||||
}
|
||||
|
||||
const report = generateCliReport({ packageName: pkgJson.name, models });
|
||||
|
||||
const reportPath = resolvePath(fullDir, 'cli-report.md');
|
||||
const existingReport = await fs
|
||||
.readFile(reportPath, 'utf8')
|
||||
.catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (existingReport !== report) {
|
||||
if (isLocalBuild) {
|
||||
console.warn(`CLI report changed for ${packageDir}`);
|
||||
await fs.writeFile(reportPath, report);
|
||||
} else {
|
||||
logApiReportInstructions();
|
||||
|
||||
if (existingReport) {
|
||||
console.log('');
|
||||
console.log(
|
||||
`The conflicting file is ${relativePath(
|
||||
cliPaths.targetRoot,
|
||||
reportPath,
|
||||
)}, expecting the following content:`,
|
||||
);
|
||||
console.log('');
|
||||
|
||||
console.log(report);
|
||||
|
||||
logApiReportInstructions();
|
||||
}
|
||||
throw new Error(`CLI report changed for ${packageDir}, `);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// Represents the help page os a CLI command
|
||||
export type CliHelpPage = {
|
||||
// Path of commands to reach this page
|
||||
path: string[];
|
||||
// Parsed content
|
||||
usage: string | undefined;
|
||||
options: string[];
|
||||
commands: string[];
|
||||
commandArguments: string[];
|
||||
};
|
||||
|
||||
// The API model for a CLI entry point
|
||||
export type CliModel = {
|
||||
name: string;
|
||||
helpPages: CliHelpPage[];
|
||||
};
|
||||
Reference in New Issue
Block a user