Skip to content

feat: add method for janus-cli package metadata ; generate updates to package.json for all plugins listed in plugins/plugin-metadata.json #1627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ To run the cli in watch mode, use `yarn start <args>`. For example `yarn start l
To try out the command locally, you can execute the following from the parent directory of this repo:

```bash
./backstage/packages/cli/bin/janus-cli --help
./packages/cli/bin/janus-cli --help
```

## Documentation
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"bfj": "^8.0.0",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
"codeowners": "^5.1.1",
"commander": "^9.1.0",
"css-loader": "^6.5.1",
"esbuild": "^0.21.0",
Expand All @@ -62,9 +63,11 @@
"express": "^4.18.2",
"fork-ts-checker-webpack-plugin": "^7.0.0-alpha.8",
"fs-extra": "^10.1.0",
"gitconfiglocal": "2.1.0",
"handlebars": "^4.7.7",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^8.2.0",
"is-native-module": "^1.1.3",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
"node-libs-browser": "^2.2.1",
Expand All @@ -87,8 +90,7 @@
"webpack": "^5.89.0",
"webpack-dev-server": "^4.15.1",
"yml-loader": "^2.1.0",
"yn": "^4.0.0",
"is-native-module": "^1.1.3"
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "0.21.7",
Expand All @@ -103,6 +105,8 @@
"@backstage/plugin-scaffolder-node": "0.4.3",
"@backstage/test-utils": "1.5.4",
"@backstage/theme": "0.5.3",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@spotify/prettier-config": "^15.0.0",
"@types/express": "4.17.20",
"@types/fs-extra": "9.0.1",
"@types/inquirer": "8.1.3",
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ export function registerScriptCommand(program: Command) {
.command('schema')
.description('Print configuration schema for a package')
.action(lazy(() => import('./schema').then(m => m.default)));

command
.command('metadata')
.description('Add metadata to a package.json file')
.option(
'--dir <path/to/folder>',
'Folder in which to make changes to package.json, if not the current directory',
'./',
)
.option('--author <author>', 'Set author', 'Red Hat')
.option('--license <license>', 'Set license', 'Apache-2.0')
.option('--homepage <homepage>', 'Set homepage', 'https://red.ht/rhdh')
.option(
'--bugs <bugs>',
'Set issue tracker URL',
'https://github.com/janus-idp/backstage-plugins/issues',
)
.option(
'--keywords <unique,keywords,to,add>',
'Add or replace keywords; there can be only one `support:` or `lifecycle:` value,\n ' +
'but unlimited other keywords can be added. To remove values, manually edit package.json\n\n ' +
'Valid values for support: alpha, beta, tech-preview, or production.\n ' +
'Valid values for lifecycle: active, maintenance, deprecated, inactive, retired.\n ',
'backstage,plugin,support:production,lifecycle:active',
)
.action(lazy(() => import('./metadata').then(m => m.command)));
}

export function registerCommands(program: Command) {
Expand Down
223 changes: 223 additions & 0 deletions packages/cli/src/commands/metadata/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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.
*/

// test with ./packages/cli/bin/janus-cli package metadata --help

// @ts-ignore
import Codeowners from 'codeowners';
import { OptionValues } from 'commander';
import gitconfig from 'gitconfiglocal';

import * as fs from 'fs';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import process from 'node:process';
import { promisify } from 'node:util';

const pGitconfig = promisify(gitconfig);

export async function command(opts: OptionValues): Promise<void> {
const config = await pGitconfig(process.cwd());
updatePackageMetadata(opts, config?.remote?.origin?.url);
}

interface Repository {
type: string;
url: string;
directory: string;
}

interface PackageJson {
name: string;
version: string;
main: string;
types: string;
license: string;
author: string;
configschema: string;

homepage: URL;
bugs: URL;

sideEffects: boolean;

publishConfig: {};
backstage: {
role: string;
'supported-versions': string;
};
scripts: {};
dependencies: {};
peerDependencies: {};
devDependencies: {};
scalprum: {};
repository: Repository;

files: string[];
maintainers: string[];
keywords: string[];
}

const path = {
/**
* @method resolveRelativeFromAbsolute resolves a relative path from an absolute path
* @param {string} DotDotPath relative path
* @returns {string} resolved absolutePath
*/
resolveRelativeFromAbsolute(DotDotPath: string): string[] {
const pathsArray = DotDotPath.replaceAll(/[/|\\]/g, '/').split('/');
const map = pathsArray.reduce(
(acc, e) => acc.set(e, (acc.get(e) || 0) + 1),
new Map(),
);
// console.log(` @ ${DotDotPath} => ${pathsArray} (${map.get("..")} backs)`)
const rootDir = pathsArray.slice(0, -(map.get('..') * 2)).join('/');
// console.log(` @ root dir: ${rootdir}`)
const relativeDir = process.cwd().replaceAll(`${rootDir}/`, '');
return [rootDir, relativeDir];
},
};

function findCodeowners(element: string) {
if (fs.existsSync(`${element}/.github/CODEOWNERS`)) {
return element; // found the root dir
}
return findCodeowners(`${element}/..`);
}

function separateKeywords(array: string[]): {
keywords: string[];
lifecycle?: string;
support?: string;
} {
return array.reduce(
(prev, keyword) => {
// separate lifecycle keyword
if (keyword.startsWith('lifecycle:')) {
return { ...prev, lifecycle: keyword };
}

// separate support keyword
if (keyword.startsWith('support:')) {
return { ...prev, support: keyword };
}

// keep the remaining keywords together
prev.keywords.push(keyword);
return prev;
},
{ keywords: [] } as {
keywords: string[];
lifecycle?: string;
support?: string;
},
);
}

export function updatePackageMetadata(
opts: OptionValues,
gitconfigRemoteOriginUrl: string,
) {
// load the package.json from the specified (or current) folder
const workingDir = `${process.cwd()}/${opts.dir}`;
console.log(`Updating ${workingDir} / package.json`);

// compute the root dir and relative path to the current dir
const [rootDir, relativePath] = opts.dir
? [process.cwd(), opts.dir]
: path.resolveRelativeFromAbsolute(findCodeowners(`${process.cwd()}`));
// console.log(` @ rootdir = ${rootdir}, relative_path = ${relative_path}`)

const packageJSONPath = join(workingDir, 'package.json');
const packageJSON = JSON.parse(
readFileSync(packageJSONPath, 'utf8'),
) as PackageJson;

/* now let's change some values */

// 1. add backstage version matching the current value of backstage.json in this repo
if (fs.existsSync(join(rootDir, '/backstage.json'))) {
packageJSON.backstage['supported-versions'] = JSON.parse(
readFileSync(join(rootDir, '/backstage.json'), 'utf8'),
).version;
}

// 2. set up repository values and the current path as repo.directory
const repo = {} as Repository;
repo.type = 'git';
repo.url = gitconfigRemoteOriginUrl
.toString()
.replaceAll('[email protected]:', 'https://github.com/')
.replaceAll('.git', '')
.trim();
repo.directory = relativePath;
packageJSON.repository = repo;

// 3. load owners from CODEOWNERS file, using this package.json's repository.directory field to compute maintainer groups
let owners: string[] = [];
if (packageJSON.repository.directory) {
const repos = new Codeowners();
owners = repos.getOwner(relativePath);
} else {
console.log(
` ! Could not load .github/CODEOWNERS file, so cannot update maintainers in package.json`,
);
}
packageJSON.maintainers = owners;

// 4. set some hardcoded values based on commandline flags
packageJSON.author = opts.author;
packageJSON.license = opts.license;
packageJSON.homepage = new URL(opts.homepage);
packageJSON.bugs = new URL(opts.bugs);

// initialize empty string array if not already present
if (!packageJSON.keywords) {
packageJSON.keywords = [];
}

// if already have keywords, replace lifecycle and support with new values (if defined)
// we can only have ONE lifecycle and one support keyword, so remove replace any existing values
const {
keywords: oldKeywords,
lifecycle: oldLifecycle,
support: oldSupport,
} = separateKeywords(packageJSON.keywords);

const {
keywords: optsKeywords,
lifecycle: optsLifecycle,
support: optsSupport,
} = separateKeywords(opts.keywords.split(','));

const newKeywords = oldKeywords.concat(optsKeywords);

// if there is a lifecycle keyword, push to the beginning of the array
if (oldLifecycle || optsLifecycle) {
newKeywords.unshift(optsLifecycle ?? oldLifecycle ?? '');
}

// if there is a support keyword, push to the beginning of the array
if (oldSupport || optsSupport) {
newKeywords.unshift(optsSupport ?? oldSupport ?? '');
}

// dedupe new keywords
packageJSON.keywords = Array.from(new Set(newKeywords));

// write changes to file
writeFileSync(packageJSONPath, JSON.stringify(packageJSON, null, 2), 'utf8');
}
17 changes: 17 additions & 0 deletions packages/cli/src/commands/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -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 { command } from './command';
5 changes: 4 additions & 1 deletion packages/cli/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2020-2024 The Janus Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,9 @@ declare namespace NodeJS {
declare module 'fs' {
export interface StatSyncFn {}
}

declare module 'gitconfiglocal';

declare module 'rollup-plugin-image-files' {
export default function image(options?: any): any;
}
Expand Down
Loading
Loading