diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index a06f426baf..6e0e4c9b85 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -18,6 +18,7 @@ Commands: knip-reports [options] [paths...] package [command] repo [command] + lint [command] help [command] ``` @@ -60,6 +61,28 @@ Options: -h, --help ``` +### `backstage-repo-tools lint` + +``` +Usage: backstage-repo-tools lint [options] [command] [command] + +Options: + -h, --help + +Commands: + legacy-backend-exports [paths...] + help [command] +``` + +### `backstage-repo-tools lint legacy-backend-exports` + +``` +Usage: backstage-repo-tools lint legacy-backend-exports [options] [paths...] + +Options: + -h, --help +``` + ### `backstage-repo-tools package` ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index f337329537..77d0047e60 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -73,6 +73,7 @@ "minimatch": "^9.0.0", "p-limit": "^3.0.2", "portfinder": "^1.0.32", + "ts-morph": "^23.0.0", "yaml-diff-patch": "^2.0.0" }, "devDependencies": { diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 24ec2af411..5116326eea 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -162,6 +162,23 @@ function registerRepoCommand(program: Command) { ); } +function registerLintCommand(program: Command) { + const lintCommand = program + .command('lint [command]') + .description('Tools for linting repository.'); + lintCommand + .command('legacy-backend-exports [paths...]') + .description( + 'Lint backend plugin packages for legacy exports and make sure it conforms to the new export pattern', + ) + .action( + lazy(() => + import( + './lint-legacy-backend-exports/lint-legacy-backend-exports' + ).then(m => m.lint), + ), + ); +} export function registerCommands(program: Command) { program .command('api-reports [paths...]') @@ -238,6 +255,7 @@ export function registerCommands(program: Command) { registerPackageCommand(program); registerRepoCommand(program); + registerLintCommand(program); } // Wraps an action function so that it always exits and handles errors diff --git a/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts b/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts new file mode 100644 index 0000000000..6025defe31 --- /dev/null +++ b/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts @@ -0,0 +1,98 @@ +/* + * 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. + */ +import { Project } from 'ts-morph'; +import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import fs from 'fs-extra'; +import { paths as cliPaths } from '../../lib/paths'; +import path from 'path'; + +const project = new Project({ + tsConfigFilePath: cliPaths.resolveTargetRoot('tsconfig.json'), +}); + +function readPackageJson(pkg: string) { + return JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf-8')); +} + +export async function lint(paths: string[]) { + const pkgs = (await PackageGraph.listTargetPackages()).filter(pkg => { + return ( + pkg.packageJson.backstage?.role === 'backend-plugin' || + pkg.packageJson.backstage?.role === 'backend-plugin-module' + ); + }); + + if (paths.length > 0) { + paths.forEach(pkg => verifyIndex(pkg)); + return; + } + pkgs.forEach(pkg => verifyIndex(pkg.dir, pkg.packageJson)); +} + +function verifyIndex(pkg: string, packageJson?: BackstagePackageJson) { + console.log(`Verifying ${pkg}`); + const tsPath = path.join(pkg, 'src/index.ts'); + const sourceFile = project.getSourceFile(tsPath); + if (!sourceFile) { + console.log(`Could not find ${tsPath}`); + process.exit(1); + } + const symbols = sourceFile?.getExportSymbols(); + + const exportCount = symbols?.length || 0; + if (exportCount > 1) { + console.log( + ` ⚠️ Warning: ${exportCount} exports found, ${symbols + .map(symbol => symbol.getName()) + .join(', ')}`, + ); + } + + const createRouterExport = symbols?.find( + symbol => symbol.getName() === 'createRouter', + ); + + if (!sourceFile.getDefaultExportSymbol()) { + console.log(' ❌ Missing default export'); + } + let createRouterDeprecated = undefined; + if (createRouterExport) { + createRouterDeprecated = createRouterExport + .getJsDocTags() + .find(tag => tag.getName() === 'deprecated'); + } + + if (createRouterExport) { + console.log(' ❌ createRouter is exported'); + if (!createRouterDeprecated) + console.log(' ❌ createRouter is NOT deprecated'); + } + + const pkgJson = packageJson ?? readPackageJson(pkg); + if ( + '@backstage/backend-common' in pkgJson.dependencies || + '@backstage/backend-common' in pkgJson.devDependencies + ) { + console.log(' ❌ Stop depending on @backstage/backend-common'); + } + + if ( + '@backstage/backend-tasks' in pkgJson.dependencies || + '@backstage/backend-tasks' in pkgJson.devDependencies + ) { + console.log(' ❌ Stop depending on @backstage/backend-tasks'); + } +} diff --git a/yarn.lock b/yarn.lock index f09d8e9695..639731d4b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7886,6 +7886,7 @@ __metadata: minimatch: ^9.0.0 p-limit: ^3.0.2 portfinder: ^1.0.32 + ts-morph: ^23.0.0 yaml-diff-patch: ^2.0.0 peerDependencies: "@microsoft/api-extractor-model": "*" @@ -17089,6 +17090,18 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.24.0": + version: 0.24.0 + resolution: "@ts-morph/common@npm:0.24.0" + dependencies: + fast-glob: ^3.3.2 + minimatch: ^9.0.4 + mkdirp: ^3.0.1 + path-browserify: ^1.0.1 + checksum: 793bc8a47c93ab55c6c036f94480d3b0e948661aef4bb7dbc29279b1dda2fc4fce809a88e221537867a313541842e12d1ecbd32b4769688abe1303807ec09db6 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -34347,7 +34360,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3, minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -42881,6 +42894,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^23.0.0": + version: 23.0.0 + resolution: "ts-morph@npm:23.0.0" + dependencies: + "@ts-morph/common": ~0.24.0 + code-block-writer: ^13.0.1 + checksum: 3282eb0f8bd4577770874736c3259b97501da9a86137160b5d68f106b7848ea7b1fbccf9e198a3d930ec40c993e9951d4bfae31e2562dac8f3de0d7bb0e23615 + languageName: node + linkType: hard + "ts-node@npm:^10.9.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2"