Skip to content

Commit

Permalink
feat: add method for janus-cli package metadata (simpler PR) - RHIDP-…
Browse files Browse the repository at this point in the history
…1502 (janus-idp#1753)

feat: RHIDP-1502 add method for janus-cli package metadata (simpler PR)

Signed-off-by: Nick Boldt <nboldt@redhat.com>
  • Loading branch information
nickboldt authored Jun 3, 2024
1 parent 2ca4f14 commit b81a849
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 11 deletions.
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 @@ -48,6 +48,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 @@ -58,9 +59,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 @@ -83,8 +86,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 @@ -99,6 +101,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.21",
"@types/fs-extra": "9.0.13",
"@types/inquirer": "8.2.10",
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('git@github.com:', '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';
3 changes: 3 additions & 0 deletions packages/cli/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b81a849

Please sign in to comment.