cli-node: refactor role utils into PackageRoles + document, move stuff back

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-04-06 11:57:07 +02:00
parent 9b3b4e6aef
commit 011217c8e0
9 changed files with 318 additions and 257 deletions
+1
View File
@@ -21,3 +21,4 @@
*/
export * from './git';
export * from './roles';
@@ -17,7 +17,7 @@
import path from 'path';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../role';
import { PackageRole } from '../roles';
import { GitUtils } from '../git';
import { Lockfile } from './Lockfile';
import { JsonValue } from '@backstage/types';
-188
View File
@@ -1,188 +0,0 @@
/*
* Copyright 2022 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 { z } from 'zod';
import fs from 'fs-extra';
import { OptionValues } from 'commander';
import { paths } from '../paths';
import { PackageRole, PackageRoleInfo } from './types';
const packageRoleInfos: PackageRoleInfo[] = [
{
role: 'frontend',
platform: 'web',
output: ['bundle'],
},
{
role: 'backend',
platform: 'node',
output: ['bundle'],
},
{
role: 'cli',
platform: 'node',
output: ['cjs'],
},
{
role: 'web-library',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'node-library',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'common-library',
platform: 'common',
output: ['types', 'esm', 'cjs'],
},
{
role: 'frontend-plugin',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'frontend-plugin-module',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'backend-plugin',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'backend-plugin-module',
platform: 'node',
output: ['types', 'cjs'],
},
];
export function getRoleInfo(role: string): PackageRoleInfo {
const roleInfo = packageRoleInfos.find(r => r.role === role);
if (!roleInfo) {
throw new Error(`Unknown package role '${role}'`);
}
return roleInfo;
}
const readSchema = z.object({
name: z.string().optional(),
backstage: z
.object({
role: z.string().optional(),
})
.optional(),
});
export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
const pkg = readSchema.parse(pkgJson);
// If there's an explicit role, use that.
if (pkg.backstage) {
const { role } = pkg.backstage;
if (!role) {
throw new Error(
`Package ${pkg.name} must specify a role in the "backstage" field`,
);
}
return getRoleInfo(role).role;
}
return undefined;
}
export async function findRoleFromCommand(
opts: OptionValues,
): Promise<PackageRole> {
if (opts.role) {
return getRoleInfo(opts.role)?.role;
}
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
const info = getRoleFromPackage(pkg);
if (!info) {
throw new Error(`Target package must have 'backstage.role' set`);
}
return info;
}
const detectionSchema = z.object({
name: z.string().optional(),
scripts: z
.object({
start: z.string().optional(),
build: z.string().optional(),
})
.optional(),
publishConfig: z
.object({
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
})
.optional(),
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
});
export function detectRoleFromPackage(
pkgJson: unknown,
): PackageRole | undefined {
const pkg = detectionSchema.parse(pkgJson);
if (pkg.scripts?.start?.includes('app:serve')) {
return 'frontend';
}
if (pkg.scripts?.build?.includes('backend:bundle')) {
return 'backend';
}
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) {
return 'backend-plugin-module';
}
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {
return 'frontend-plugin-module';
}
if (pkg.scripts?.start?.includes('plugin:serve')) {
return 'frontend-plugin';
}
if (pkg.scripts?.start?.includes('backend:dev')) {
return 'backend-plugin';
}
const mainEntry = pkg.publishConfig?.main || pkg.main;
const moduleEntry = pkg.publishConfig?.module || pkg.module;
const typesEntry = pkg.publishConfig?.types || pkg.types;
if (typesEntry) {
if (mainEntry && moduleEntry) {
return 'common-library';
}
if (moduleEntry || mainEntry?.endsWith('.esm.js')) {
return 'web-library';
}
if (mainEntry) {
return 'node-library';
}
} else if (mainEntry) {
return 'cli';
}
return undefined;
}
@@ -14,30 +14,23 @@
* limitations under the License.
*/
import mockFs from 'mock-fs';
import { Command } from 'commander';
import {
getRoleInfo,
getRoleFromPackage,
findRoleFromCommand,
detectRoleFromPackage,
} from './PackageRoles';
import { PackageRoles } from './PackageRoles';
describe('getRoleInfo', () => {
it('provides role info by role', () => {
expect(getRoleInfo('web-library')).toEqual({
expect(PackageRoles.getRoleInfo('web-library')).toEqual({
role: 'web-library',
platform: 'web',
output: ['types', 'esm'],
});
expect(getRoleInfo('frontend')).toEqual({
expect(PackageRoles.getRoleInfo('frontend')).toEqual({
role: 'frontend',
platform: 'web',
output: ['bundle'],
});
expect(() => getRoleInfo('invalid')).toThrow(
expect(() => PackageRoles.getRoleInfo('invalid')).toThrow(
`Unknown package role 'invalid'`,
);
});
@@ -46,7 +39,7 @@ describe('getRoleInfo', () => {
describe('getRoleFromPackage', () => {
it('reads explicit package roles', () => {
expect(
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
backstage: {
role: 'web-library',
},
@@ -54,7 +47,7 @@ describe('getRoleFromPackage', () => {
).toEqual('web-library');
expect(
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
backstage: {
role: 'frontend',
},
@@ -62,14 +55,14 @@ describe('getRoleFromPackage', () => {
).toEqual('frontend');
expect(() =>
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
name: 'test',
backstage: {},
}),
).toThrow('Package test must specify a role in the "backstage" field');
expect(() =>
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
name: 'test',
backstage: { role: 'invalid' },
}),
@@ -77,48 +70,10 @@ describe('getRoleFromPackage', () => {
});
});
describe('findRoleFromCommand', () => {
function mkCommand(args: string) {
const parsed = new Command()
.option('--role <role>', 'test role')
.parse(['node', 'entry.js', ...args.split(' ')]) as Command;
return parsed.opts();
}
beforeEach(() => {
mockFs({
'package.json': JSON.stringify({
name: 'test',
backstage: {
role: 'web-library',
},
}),
});
});
afterEach(() => {
mockFs.restore();
});
it('provides role info by role', async () => {
await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual(
'web-library',
);
await expect(
findRoleFromCommand(mkCommand('--role node-library')),
).resolves.toEqual('node-library');
await expect(
findRoleFromCommand(mkCommand('--role invalid')),
).rejects.toThrow(`Unknown package role 'invalid'`);
});
});
describe('detectRoleFromPackage', () => {
it('detects the role of example-app', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: 'example-app',
private: true,
bundled: true,
@@ -141,7 +96,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of example-backend', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: 'example-backend',
main: 'dist/index.cjs.js',
types: 'src/index.ts',
@@ -160,7 +115,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -185,7 +140,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-backend', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-backend',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -209,7 +164,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-react', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-react',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -232,7 +187,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-common', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-common',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -256,7 +211,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-backend-module-ldap',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -279,7 +234,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-permission-node', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-permission-node',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -303,7 +258,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-analytics-module-ga', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-analytics-module-ga',
main: 'src/index.ts',
types: 'src/index.ts',
+186
View File
@@ -0,0 +1,186 @@
/*
* Copyright 2022 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 { z } from 'zod';
import { PackageRole, PackageRoleInfo } from './types';
const packageRoleInfos: PackageRoleInfo[] = [
{
role: 'frontend',
platform: 'web',
output: ['bundle'],
},
{
role: 'backend',
platform: 'node',
output: ['bundle'],
},
{
role: 'cli',
platform: 'node',
output: ['cjs'],
},
{
role: 'web-library',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'node-library',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'common-library',
platform: 'common',
output: ['types', 'esm', 'cjs'],
},
{
role: 'frontend-plugin',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'frontend-plugin-module',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'backend-plugin',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'backend-plugin-module',
platform: 'node',
output: ['types', 'cjs'],
},
];
const readSchema = z.object({
name: z.string().optional(),
backstage: z
.object({
role: z.string().optional(),
})
.optional(),
});
const detectionSchema = z.object({
name: z.string().optional(),
scripts: z
.object({
start: z.string().optional(),
build: z.string().optional(),
})
.optional(),
publishConfig: z
.object({
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
})
.optional(),
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
});
/**
* Utilities for working with Backstage package roles.
*
* @public
*/
export class PackageRoles {
/**
* Get the associated info for a package role.
*/
static getRoleInfo(role: string): PackageRoleInfo {
const roleInfo = packageRoleInfos.find(r => r.role === role);
if (!roleInfo) {
throw new Error(`Unknown package role '${role}'`);
}
return roleInfo;
}
/**
* Given package JSON data, get the package role.
*/
static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
const pkg = readSchema.parse(pkgJson);
if (pkg.backstage) {
const { role } = pkg.backstage;
if (!role) {
throw new Error(
`Package ${pkg.name} must specify a role in the "backstage" field`,
);
}
return this.getRoleInfo(role).role;
}
return undefined;
}
/**
* Attempt to detect the role of a package from its package.json.
*/
static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
const pkg = detectionSchema.parse(pkgJson);
if (pkg.scripts?.start?.includes('app:serve')) {
return 'frontend';
}
if (pkg.scripts?.build?.includes('backend:bundle')) {
return 'backend';
}
if (
pkg.name?.includes('plugin-') &&
pkg.name?.includes('-backend-module-')
) {
return 'backend-plugin-module';
}
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {
return 'frontend-plugin-module';
}
if (pkg.scripts?.start?.includes('plugin:serve')) {
return 'frontend-plugin';
}
if (pkg.scripts?.start?.includes('backend:dev')) {
return 'backend-plugin';
}
const mainEntry = pkg.publishConfig?.main || pkg.main;
const moduleEntry = pkg.publishConfig?.module || pkg.module;
const typesEntry = pkg.publishConfig?.types || pkg.types;
if (typesEntry) {
if (mainEntry && moduleEntry) {
return 'common-library';
}
if (moduleEntry || mainEntry?.endsWith('.esm.js')) {
return 'web-library';
}
if (mainEntry) {
return 'node-library';
}
} else if (mainEntry) {
return 'cli';
}
return undefined;
}
}
@@ -20,9 +20,4 @@ export type {
PackageOutputType,
PackageRole,
} from './types';
export {
getRoleInfo,
getRoleFromPackage,
findRoleFromCommand,
detectRoleFromPackage,
} from './PackageRoles';
export { PackageRoles } from './PackageRoles';
@@ -14,6 +14,11 @@
* limitations under the License.
*/
/**
* Backstage package role, see {@link https://backstage.io/docs/local-dev/cli-build-system#package-roles | docs}.
*
* @public
*/
export type PackageRole =
| 'frontend'
| 'backend'
@@ -26,9 +31,25 @@ export type PackageRole =
| 'backend-plugin'
| 'backend-plugin-module';
/**
* A type of platform that a package can be built for.
*
* @public
*/
export type PackagePlatform = 'node' | 'web' | 'common';
/**
* The type of output that a package can produce.
*
* @public
*/
export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs';
/**
* Information about a package role.
*
* @public
*/
export interface PackageRoleInfo {
role: PackageRole;
platform: PackagePlatform;
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright 2022 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 mockFs from 'mock-fs';
import { Command } from 'commander';
import { findRoleFromCommand } from './role';
describe('findRoleFromCommand', () => {
function mkCommand(args: string) {
const parsed = new Command()
.option('--role <role>', 'test role')
.parse(['node', 'entry.js', ...args.split(' ')]) as Command;
return parsed.opts();
}
beforeEach(() => {
mockFs({
'package.json': JSON.stringify({
name: 'test',
backstage: {
role: 'web-library',
},
}),
});
});
afterEach(() => {
mockFs.restore();
});
it('provides role info by role', async () => {
await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual(
'web-library',
);
await expect(
findRoleFromCommand(mkCommand('--role node-library')),
).resolves.toEqual('node-library');
await expect(
findRoleFromCommand(mkCommand('--role invalid')),
).rejects.toThrow(`Unknown package role 'invalid'`);
});
});
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2023 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 { paths } from './paths';
export async function findRoleFromCommand(
opts: OptionValues,
): Promise<PackageRole> {
if (opts.role) {
return getRoleInfo(opts.role)?.role;
}
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
const info = getRoleFromPackage(pkg);
if (!info) {
throw new Error(`Target package must have 'backstage.role' set`);
}
return info;
}