cli: remove deprecated commands

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-21 12:33:08 +02:00
parent 68715dffac
commit 6129076bbd
9 changed files with 12 additions and 763 deletions
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/cli': minor
---
**BREAKING**: Removed the following deprecated commands:
- `create`: Use `backstage-cli new` instead
- `create-plugin`: Use `backstage-cli new` instead
- `plugin:diff`: Use `backstage-cli fix` instead
- `test`: Use `backstage-cli repo test` or `backstage-cli package test` instead
- `versions:check`: Use `yarn dedupe` or `yarn-deduplicate` instead
- `clean`: Use `backstage-cli package clean` instead
-125
View File
@@ -13,7 +13,6 @@ Options:
Commands:
new [options]
test
config:docs [options]
config:print [options]
config:check [options]
@@ -23,7 +22,6 @@ Commands:
migrate [command]
versions:bump [options]
versions:migrate [options]
clean
build-workspace [options] <workspace-dir> [packages...]
create-github-app <github-org>
info
@@ -40,15 +38,6 @@ Options:
-h, --help
```
### `backstage-cli clean`
```
Usage: backstage-cli clean [options]
Options:
-h, --help
```
### `backstage-cli config:check`
```
@@ -481,120 +470,6 @@ Options:
-h, --help
```
### `backstage-cli test`
```
Usage: backstage-cli [--config=<pathToConfigFile>] [TestPathPattern]
Options:
-h, --help
--version
--all
--automock
-b, --bail
--cache
--cacheDirectory
--changedFilesWithAncestor
--changedSince
--ci
--clearCache
--clearMocks
--collectCoverage
--collectCoverageFrom
--color
--colors
-c, --config
--coverage
--coverageDirectory
--coveragePathIgnorePatterns
--coverageProvider
--coverageReporters
--coverageThreshold
--debug
--detectLeaks
--detectOpenHandles
--env
--errorOnDeprecated
-e, --expand
--filter
--findRelatedTests
--forceExit
--globalSetup
--globalTeardown
--globals
--haste
--ignoreProjects
--init
--injectGlobals
--json
--lastCommit
--listTests
--logHeapUsage
--maxConcurrency
-w, --maxWorkers
--moduleDirectories
--moduleFileExtensions
--moduleNameMapper
--modulePathIgnorePatterns
--modulePaths
--noStackTrace
--notify
--notifyMode
-o, --onlyChanged
-f, --onlyFailures
--openHandlesTimeout
--outputFile
--passWithNoTests
--preset
--prettierPath
--projects
--randomize
--reporters
--resetMocks
--resetModules
--resolver
--restoreMocks
--rootDir
--roots
-i, --runInBand
--runTestsByPath
--runner
--seed
--selectProjects
--setupFiles
--setupFilesAfterEnv
--shard
--showConfig
--showSeed
--silent
--skipFilter
--snapshotSerializers
--testEnvironment
--testEnvironmentOptions
--testFailureExitCode
--testLocationInResults
--testMatch
-t, --testNamePattern
--testPathIgnorePatterns
--testPathPattern
--testRegex
--testResultsProcessor
--testRunner
--testSequencer
--testTimeout
--transform
--transformIgnorePatterns
--unmockedModulePathPatterns
-u, --updateSnapshot
--useStderr
--verbose
--watch
--watchAll
--watchPathIgnorePatterns
--watchman
--workerThreads
```
### `backstage-cli versions:bump`
```
@@ -1,40 +0,0 @@
/*
* Copyright 2020 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 path from 'path';
import { movePlugin } from './createPlugin';
import { createMockDirectory } from '@backstage/backend-test-utils';
const id = 'testPluginMock';
describe('createPlugin', () => {
const mockDir = createMockDirectory();
describe('movePlugin', () => {
it('should move the temporary plugin directory to its final place', async () => {
mockDir.setContent({
[id]: {},
});
const tempDir = mockDir.resolve(id);
const pluginDir = mockDir.resolve('test-temp/plugins', id);
await movePlugin(tempDir, pluginDir, id);
await expect(fs.pathExists(pluginDir)).resolves.toBe(true);
expect(pluginDir).toMatch(path.join('', 'plugins', id));
});
});
});
@@ -1,343 +0,0 @@
/*
* Copyright 2020 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 { promisify } from 'util';
import chalk from 'chalk';
import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { join as joinPath, resolve as resolvePath } from 'path';
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import os from 'os';
import { OptionValues } from 'commander';
import { assertError } from '@backstage/errors';
import {
addCodeownersEntry,
getCodeownersFilePath,
parseOwnerIds,
} from '../../lib/codeowners';
import { paths } from '../../lib/paths';
import { Task, templatingTask } from '../../lib/tasks';
import { Lockfile } from '../../lib/versioning';
import { createPackageVersionProvider } from '../../lib/version';
const exec = promisify(execCb);
async function checkExists(destination: string) {
await Task.forItem('checking', destination, async () => {
if (await fs.pathExists(destination)) {
const existing = chalk.cyan(
destination.replace(`${paths.targetRoot}/`, ''),
);
throw new Error(
`A plugin with the same name already exists: ${existing}\nPlease try again with a different plugin ID`,
);
}
});
}
const sortObjectByKeys = (obj: { [name in string]: string }) => {
return Object.keys(obj)
.sort()
.reduce((result, key: string) => {
result[key] = obj[key];
return result;
}, {} as { [name in string]: string });
};
export const capitalize = (str: string): string =>
str.charAt(0).toUpperCase() + str.slice(1);
export const addExportStatement = async (
file: string,
exportStatement: string,
) => {
const newContents = fs
.readFileSync(file, 'utf8')
.split('\n')
.filter(Boolean) // get rid of empty lines
.concat([exportStatement])
.concat(['']) // newline at end of file
.join('\n');
await fs.writeFile(file, newContents, 'utf8');
};
export async function addPluginDependencyToApp(
rootDir: string,
pluginPackage: string,
versionStr: string,
) {
const packageFilePath = 'packages/app/package.json';
const packageFile = resolvePath(rootDir, packageFilePath);
await Task.forItem('processing', packageFilePath, async () => {
const packageFileContent = await fs.readFile(packageFile, 'utf-8');
const packageFileJson = JSON.parse(packageFileContent);
const dependencies = packageFileJson.dependencies;
if (dependencies[pluginPackage]) {
throw new Error(
`Plugin ${pluginPackage} already exists in ${packageFile}`,
);
}
dependencies[pluginPackage] = `^${versionStr}`;
packageFileJson.dependencies = sortObjectByKeys(dependencies);
const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`;
await fs.writeFile(packageFile, newContents, 'utf-8').catch(error => {
throw new Error(
`Failed to add plugin as dependency to app: ${packageFile}: ${error.message}`,
);
});
});
}
export async function addPluginExtensionToApp(
pluginId: string,
extensionName: string,
pluginPackage: string,
) {
const pluginsFilePath = paths.resolveTargetRoot('packages/app/src/App.tsx');
if (!(await fs.pathExists(pluginsFilePath))) {
return;
}
await Task.forItem('processing', pluginsFilePath, async () => {
const content = await fs.readFile(pluginsFilePath, 'utf8');
const revLines = content.split('\n').reverse();
const lastImportIndex = revLines.findIndex(line =>
line.match(/ from ("|').*("|')/),
);
const lastRouteIndex = revLines.findIndex(line =>
line.match(/<\/FlatRoutes/),
);
if (lastImportIndex !== -1 && lastRouteIndex !== -1) {
revLines.splice(
lastImportIndex,
0,
`import { ${extensionName} } from '${pluginPackage}';`,
);
const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? [];
revLines.splice(
lastRouteIndex + 1,
0,
`${indentation}<Route path="/${pluginId}" element={<${extensionName} />}/>`,
);
const newContent = revLines.reverse().join('\n');
await fs.writeFile(pluginsFilePath, newContent, 'utf8');
}
});
}
async function cleanUp(tempDir: string) {
await Task.forItem('remove', 'temporary directory', async () => {
await fs.remove(tempDir);
});
}
async function buildPlugin(pluginFolder: string) {
const commands = [
'yarn install',
'yarn lint --fix',
'yarn tsc',
'yarn build',
];
for (const command of commands) {
try {
await Task.forItem('executing', command, async () => {
process.chdir(pluginFolder);
await exec(command);
}).catch(error => {
process.stdout.write(error.stderr);
process.stdout.write(error.stdout);
throw new Error(
`Warning: Could not execute command ${chalk.cyan(command)}`,
);
});
} catch (error) {
assertError(error);
Task.error(error.message);
break;
}
}
}
export async function movePlugin(
tempDir: string,
destination: string,
id: string,
) {
await Task.forItem('moving', id, async () => {
await fs.move(tempDir, destination).catch(error => {
throw new Error(
`Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`,
);
});
});
}
export default async (opts: OptionValues) => {
const codeownersPath = await getCodeownersFilePath(paths.targetRoot);
const questions: Question[] = [
{
type: 'input',
name: 'id',
message: chalk.blue('Enter an ID for the plugin [required]'),
validate: (value: any) => {
if (!value) {
return chalk.red('Please enter an ID for the plugin');
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return chalk.red(
'Plugin IDs must be lowercase and contain only letters, digits, and dashes.',
);
}
return true;
},
},
];
if (codeownersPath) {
questions.push({
type: 'input',
name: 'owner',
message: chalk.blue(
'Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]',
),
validate: (value: any) => {
if (!value) {
return true;
}
const ownerIds = parseOwnerIds(value);
if (!ownerIds) {
return chalk.red(
'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses of users (e.g. user@example.com).',
);
}
return true;
},
});
}
const answers: Answers = await inquirer.prompt(questions);
const pluginId =
opts.backend && !answers.id.endsWith('-backend')
? `${answers.id}-backend`
: answers.id;
const name = opts.scope
? `@${opts.scope.replace(/^@/, '')}/plugin-${pluginId}`
: `plugin-${pluginId}`;
const pluginVar = `${camelCase(answers.id)}Plugin`;
const extensionName = `${upperFirst(camelCase(answers.id))}Page`;
const npmRegistry = opts.npmRegistry && opts.scope ? opts.npmRegistry : '';
const privatePackage = opts.private === false ? false : true;
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
const appPackage = paths.resolveTargetRoot('packages/app');
const templateDir = paths.resolveOwn(
opts.backend
? 'templates/default-backend-plugin'
: 'templates/default-plugin',
);
const pluginDir = isMonoRepo
? paths.resolveTargetRoot('plugins', pluginId)
: paths.resolveTargetRoot(pluginId);
const { version: pluginVersion } = isMonoRepo
? await fs.readJson(paths.resolveTargetRoot('lerna.json'))
: { version: '0.1.0' };
const license = opts.license ?? 'Apache-2.0';
let lockfile: Lockfile | undefined;
try {
lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
} catch (error) {
console.warn(`No yarn.lock available, ${error}`);
}
Task.log();
Task.log('Creating the plugin...');
Task.section('Checking if the plugin ID is available');
await checkExists(pluginDir);
Task.section('Creating a temporary plugin directory');
const tempDir = await fs.mkdtemp(
joinPath(os.tmpdir(), `backstage-plugin-${pluginId}`),
);
try {
Task.section('Preparing files');
await templatingTask(
templateDir,
tempDir,
{
...answers,
pluginVar,
pluginVersion,
extensionName,
name,
privatePackage,
npmRegistry,
license,
},
createPackageVersionProvider(lockfile),
isMonoRepo,
);
Task.section('Moving to final location');
await movePlugin(tempDir, pluginDir, pluginId);
Task.section('Building the plugin');
await buildPlugin(pluginDir);
if ((await fs.pathExists(appPackage)) && !opts.backend) {
Task.section('Adding plugin as dependency in app');
await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion);
Task.section('Import plugin in app');
await addPluginExtensionToApp(pluginId, extensionName, name);
}
if (answers.owner) {
await addCodeownersEntry(`/plugins/${pluginId}`, answers.owner);
}
Task.log();
Task.log(`🥇 Successfully created ${chalk.cyan(`${name}`)}`);
Task.log();
Task.exit();
} catch (error) {
assertError(error);
Task.error(error.message);
Task.log('It seems that something went wrong when creating the plugin 🤔');
Task.log('We are going to clean up, and then you can try again.');
Task.section('Cleanup');
await cleanUp(tempDir);
Task.error('🔥 Failed to create plugin!');
Task.exit(1);
}
};
-69
View File
@@ -265,63 +265,6 @@ export function registerCommands(program: Command) {
.option('--no-private', 'Do not mark new packages as private')
.action(lazy(() => import('./new/new').then(m => m.default)));
program
.command('create', { hidden: true })
.storeOptionsAsProperties(false)
.description(
'Open up an interactive guide to creating new things in your app [DEPRECATED]',
)
.option(
'--select <name>',
'Select the thing you want to be creating upfront',
)
.option(
'--option <name>=<value>',
'Pre-fill options for the creation process',
(opt, arr: string[]) => [...arr, opt],
[],
)
.option('--scope <scope>', 'The scope to use for new packages')
.option(
'--npm-registry <URL>',
'The package registry to use for new packages',
)
.option('--no-private', 'Do not mark new packages as private')
.action(lazy(() => import('./new/new').then(m => m.default)));
program
.command('create-plugin', { hidden: true })
.option(
'--backend',
'Create plugin with the backend dependencies as default',
)
.description('Creates a new plugin in the current repository [DEPRECATED]')
.option('--scope <scope>', 'npm scope')
.option('--npm-registry <URL>', 'npm registry URL')
.option('--no-private', 'Public npm package')
.action(
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
);
program
.command('plugin:diff', { hidden: true })
.option('--check', 'Fail if changes are required')
.option('--yes', 'Apply all changes')
.description(
'Diff an existing plugin with the creation template [DEPRECATED]',
)
.action(lazy(() => import('./plugin/diff').then(m => m.default)));
// TODO(Rugvip): Deprecate in favor of package variant
program
.command('test')
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
.helpOption(', --backstage-cli-help') // Let Jest handle help
.description(
'Run tests, forwarding args to Jest, defaulting to watch mode [DEPRECATED]',
)
.action(lazy(() => import('./test').then(m => m.default)));
program
.command('config:docs')
.option(
@@ -403,12 +346,6 @@ export function registerCommands(program: Command) {
.description('Bump Backstage packages to the latest versions')
.action(lazy(() => import('./versions/bump').then(m => m.default)));
program
.command('versions:check', { hidden: true })
.option('--fix', 'Fix any auto-fixable versioning problems')
.description('Check Backstage package versioning')
.action(lazy(() => import('./versions/lint').then(m => m.default)));
program
.command('versions:migrate')
.option(
@@ -424,12 +361,6 @@ export function registerCommands(program: Command) {
)
.action(lazy(() => import('./versions/migrate').then(m => m.default)));
// TODO(Rugvip): Deprecate in favor of package variant
program
.command('clean')
.description('Delete cache directories [DEPRECATED]')
.action(lazy(() => import('./clean/clean').then(m => m.default)));
program
.command('build-workspace <workspace-dir> [packages...]')
.option(
-102
View File
@@ -1,102 +0,0 @@
/*
* Copyright 2020 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 { OptionValues } from 'commander';
import {
diffTemplateFiles,
handlers,
handleAllFiles,
inquirerPromptFunc,
makeCheckPromptFunc,
yesPromptFunc,
} from '../../lib/diff';
import { paths } from '../../lib/paths';
export type PluginData = {
id: string;
name: string;
privatePackage: string;
pluginVersion: string;
npmRegistry: string;
};
const fileHandlers = [
{
patterns: ['package.json'],
handler: handlers.packageJson,
},
{
// make sure files in 1st level of src/ and dev/ exist
patterns: ['.eslintrc.js'],
handler: handlers.exists,
},
{
patterns: ['README.md', 'tsconfig.json', /^src\//, /^dev\//],
handler: handlers.skip,
},
];
export default async (opts: OptionValues) => {
let promptFunc = inquirerPromptFunc;
let finalize = () => {};
if (opts.check) {
[promptFunc, finalize] = makeCheckPromptFunc();
} else if (opts.yes) {
promptFunc = yesPromptFunc;
}
const data = await readPluginData();
const templateFiles = await diffTemplateFiles('default-plugin', data);
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
finalize();
};
// Reads templating data from the existing plugin
async function readPluginData(): Promise<PluginData> {
let name: string;
let privatePackage: string;
let pluginVersion: string;
let npmRegistry: string;
try {
const pkg = require(paths.resolveTarget('package.json'));
name = pkg.name;
privatePackage = pkg.private;
pluginVersion = pkg.version;
const scope = name.split('/')[0];
if (`${scope}:registry` in pkg.publishConfig) {
const registryURL = pkg.publishConfig[`${scope}:registry`];
npmRegistry = `"${scope}:registry" : "${registryURL}"`;
} else npmRegistry = '';
} catch (error) {
throw new Error(`Failed to read target package, ${error}`);
}
const pluginTsContents = await fs.readFile(
paths.resolveTarget('src/plugin.ts'),
'utf8',
);
// TODO: replace with some proper parsing logic or plugin metadata file
const pluginIdMatch = pluginTsContents.match(/id: ['"`](.+?)['"`]/);
if (!pluginIdMatch) {
throw new Error(`Failed to parse plugin.ts, no plugin ID found`);
}
const id = pluginIdMatch[1];
return { id, name, privatePackage, pluginVersion, npmRegistry };
}
-31
View File
@@ -1,31 +0,0 @@
/*
* Copyright 2020 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 { OptionValues } from 'commander';
import { run } from '../../lib/run';
export default async (opts: OptionValues) => {
const args = ['test'];
if (opts.watch) {
args.push('--watch');
}
if (opts.coverage) {
args.push('--coverage');
}
await run('web-scripts', args);
};
@@ -1,32 +0,0 @@
/*
* Copyright 2020 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 const pluginsFileContent = `
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';`;
export const codeownersFileContent = `
* @spotify/backstage-core
/docs/features/techdocs @spotify/techdocs-core
/plugins/cost-insights @spotify/silver-lining
`;
export const packageFileContent = {
name: 'example-app',
version: '0.1.1',
dependencies: {},
devDependencies: {},
scripts: {},
};
@@ -1,21 +0,0 @@
/*
* Copyright 2020 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 default async () => {
throw new Error(
'This command has been removed, please consider alternatives such as `yarn dedupe` instead.',
);
};