cli: introduce repo fix command
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added a new `repo fix` command that fixes auto-fixable problems in all packages.
|
||||
@@ -67,6 +67,15 @@ export function registerRepoCommand(program: Command) {
|
||||
.option('--fix', 'Attempt to automatically fix violations')
|
||||
.action(lazy(() => import('./repo/lint').then(m => m.command)));
|
||||
|
||||
command
|
||||
.command('fix')
|
||||
.description('Automatically fix packages in the project')
|
||||
.option(
|
||||
'--check',
|
||||
'Fail if any packages would have been changed by the command',
|
||||
)
|
||||
.action(lazy(() => import('./repo/fix').then(m => m.command)));
|
||||
|
||||
command
|
||||
.command('clean')
|
||||
.description('Delete cache and output directories')
|
||||
|
||||
@@ -14,108 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
|
||||
function trimRelative(path: string): string {
|
||||
if (path.startsWith('./')) {
|
||||
return path.slice(2);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
import {
|
||||
fixPackageExports,
|
||||
readFixablePackages,
|
||||
writeFixedPackages,
|
||||
} from '../repo/fix';
|
||||
|
||||
export async function command() {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson }) => {
|
||||
let changed = false;
|
||||
let newPackageJson = packageJson;
|
||||
|
||||
let { exports: exp } = newPackageJson;
|
||||
if (!exp) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(exp)) {
|
||||
throw new Error('Unexpected array in package.json exports field');
|
||||
}
|
||||
|
||||
// If exports is a string we rewrite it to an object to add package.json
|
||||
if (typeof exp === 'string') {
|
||||
changed = true;
|
||||
exp = { '.': exp };
|
||||
newPackageJson.exports = exp;
|
||||
} else if (typeof exp !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!exp['./package.json']) {
|
||||
changed = true;
|
||||
exp['./package.json'] = './package.json';
|
||||
}
|
||||
|
||||
const existingTypesVersions = JSON.stringify(packageJson.typesVersions);
|
||||
|
||||
const typeEntries: Record<string, [string]> = {};
|
||||
for (const [path, value] of Object.entries(exp)) {
|
||||
// Main entry point does not need to be listed
|
||||
if (path === '.') {
|
||||
continue;
|
||||
}
|
||||
const newPath = trimRelative(path);
|
||||
|
||||
if (typeof value === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value)];
|
||||
} else if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
if (typeof value.types === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value.types)];
|
||||
} else if (typeof value.default === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value.default)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typesVersions = { '*': typeEntries };
|
||||
if (existingTypesVersions !== JSON.stringify(typesVersions)) {
|
||||
console.log(`Synchronizing exports in ${packageJson.name}`);
|
||||
const newPkgEntries = Object.entries(newPackageJson).filter(
|
||||
([name]) => name !== 'typesVersions',
|
||||
);
|
||||
newPkgEntries.splice(
|
||||
newPkgEntries.findIndex(([name]) => name === 'exports') + 1,
|
||||
0,
|
||||
['typesVersions', typesVersions],
|
||||
);
|
||||
|
||||
newPackageJson = Object.fromEntries(
|
||||
newPkgEntries,
|
||||
) as BackstagePackageJson;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Remove the legacy fields from publishConfig, which are no longer needed
|
||||
const publishConfig = newPackageJson.publishConfig as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
if (publishConfig) {
|
||||
for (const field of ['main', 'module', 'browser', 'types']) {
|
||||
if (publishConfig[field]) {
|
||||
delete publishConfig[field];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), newPackageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}),
|
||||
console.log(
|
||||
'The `migrate package-exports` command is deprecated, use `repo fix` instead.',
|
||||
);
|
||||
const packages = await readFixablePackages();
|
||||
|
||||
for (const pkg of packages) {
|
||||
fixPackageExports(pkg);
|
||||
}
|
||||
|
||||
await writeFixedPackages(packages);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 {
|
||||
BackstagePackage,
|
||||
BackstagePackageJson,
|
||||
PackageGraph,
|
||||
} from '@backstage/cli-node';
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
/**
|
||||
* A mutable object representing a package.json file with potential fixes.
|
||||
*/
|
||||
export interface FixablePackage extends BackstagePackage {
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
export async function readFixablePackages(): Promise<FixablePackage[]> {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
return packages.map(pkg => ({ ...pkg, changed: false }));
|
||||
}
|
||||
|
||||
export function printPackageFixHint(packages: FixablePackage[]) {
|
||||
const changed = packages.filter(pkg => pkg.changed);
|
||||
if (changed.length > 0) {
|
||||
console.log(
|
||||
'The following packages are out of sync, run `yarn fix` to fix them:',
|
||||
);
|
||||
for (const pkg of changed) {
|
||||
console.log(` ${pkg.packageJson.name}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function writeFixedPackages(
|
||||
packages: FixablePackage[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
packages.map(async pkg => {
|
||||
if (!pkg.changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeJson(
|
||||
resolvePath(pkg.dir, 'package.json'),
|
||||
pkg.packageJson,
|
||||
{
|
||||
spaces: 2,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function trimRelative(path: string): string {
|
||||
if (path.startsWith('./')) {
|
||||
return path.slice(2);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function fixPackageExports(pkg: FixablePackage) {
|
||||
let { exports: exp } = pkg.packageJson;
|
||||
if (!exp) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(exp)) {
|
||||
throw new Error('Unexpected array in package.json exports field');
|
||||
}
|
||||
|
||||
// If exports is a string we rewrite it to an object to add package.json
|
||||
if (typeof exp === 'string') {
|
||||
pkg.changed = true;
|
||||
exp = { '.': exp };
|
||||
pkg.packageJson.exports = exp;
|
||||
} else if (typeof exp !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!exp['./package.json']) {
|
||||
pkg.changed = true;
|
||||
exp['./package.json'] = './package.json';
|
||||
}
|
||||
|
||||
const existingTypesVersions = JSON.stringify(pkg.packageJson.typesVersions);
|
||||
|
||||
const typeEntries: Record<string, [string]> = {};
|
||||
for (const [path, value] of Object.entries(exp)) {
|
||||
// Main entry point does not need to be listed
|
||||
if (path === '.') {
|
||||
continue;
|
||||
}
|
||||
const newPath = trimRelative(path);
|
||||
|
||||
if (typeof value === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value)];
|
||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (typeof value.types === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value.types)];
|
||||
} else if (typeof value.default === 'string') {
|
||||
typeEntries[newPath] = [trimRelative(value.default)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typesVersions = { '*': typeEntries };
|
||||
if (existingTypesVersions !== JSON.stringify(typesVersions)) {
|
||||
const newPkgEntries = Object.entries(pkg.packageJson).filter(
|
||||
([name]) => name !== 'typesVersions',
|
||||
);
|
||||
newPkgEntries.splice(
|
||||
newPkgEntries.findIndex(([name]) => name === 'exports') + 1,
|
||||
0,
|
||||
['typesVersions', typesVersions],
|
||||
);
|
||||
|
||||
pkg.packageJson = Object.fromEntries(newPkgEntries) as BackstagePackageJson;
|
||||
pkg.changed = true;
|
||||
}
|
||||
|
||||
// Remove the legacy fields from publishConfig, which are no longer needed
|
||||
const publishConfig = pkg.packageJson.publishConfig as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
if (publishConfig) {
|
||||
for (const field of ['main', 'module', 'browser', 'types']) {
|
||||
if (publishConfig[field]) {
|
||||
delete publishConfig[field];
|
||||
pkg.changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
const packages = await readFixablePackages();
|
||||
|
||||
for (const pkg of packages) {
|
||||
fixPackageExports(pkg);
|
||||
}
|
||||
|
||||
if (opts.check) {
|
||||
if (printPackageFixHint(packages)) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
await writeFixedPackages(packages);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user