Path: blob/master/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts
34646 views
/*1* Copyright 2024 The Backstage Authors2*3* Licensed under the Apache License, Version 2.0 (the "License");4* you may not use this file except in compliance with the License.5* You may obtain a copy of the License at6*7* http://www.apache.org/licenses/LICENSE-2.08*9* Unless required by applicable law or agreed to in writing, software10* distributed under the License is distributed on an "AS IS" BASIS,11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12* See the License for the specific language governing permissions and13* limitations under the License.14*/15import { targetPaths } from '@backstage/cli-common';16import pLimit from 'p-limit';17import os from 'node:os';18import { relative as relativePath, resolve as resolvePath } from 'node:path';19import fs from 'fs-extra';20import type { KnipConfig } from 'knip';21import { createBinRunner } from '../util';2223interface KnipExtractionOptions {24packageDirs: string[];25isLocalBuild: boolean;26}2728interface KnipConfigOptions {29knipConfigPath: string;30}3132interface KnipPackageOptions {33packageDir: string;34knipDir: string;35isLocalBuild: boolean;36}3738function logKnipReportInstructions() {39console.log('');40console.log(41'*************************************************************************************',42);43console.log(44'* You have uncommitted changes to the knip reports of a package. *',45);46console.log(47'* To solve this, run `yarn build:knip-reports` and commit all md file changes. *',48);49console.log(50'*************************************************************************************',51);52console.log('');53}5455async function generateKnipConfig({ knipConfigPath }: KnipConfigOptions) {56const knipConfig: KnipConfig = {57workspaces: {58'.': {},59'{packages,plugins}/*': {60entry: ['dev/**/*.{ts,tsx}', 'src/index.{ts,tsx}'],61ignore: [62'.eslintrc.js',63'config.d.ts',64'knexfile.js',65'node_modules/**',66'dist/**',67'{fixtures,migrations,templates}/**',68'src/tests/transforms/__fixtures__/**', // cli packaging tests69],70},71},72jest: {73entry: ['src/setupTests.ts', 'src/**/*.test.{ts,tsx}'],74},75storybook: { entry: 'src/components/**/*.stories.tsx' },76ignoreDependencies: [77// these is reported as a referenced optional peerDependencies78// TBD: investigate what triggers these79'@types/react',80'@types/jest',81'@internal/.*', // internal packages are not published and inlined82'@backstage/cli', // everything depends on this for its package.json commands83'@backstage/theme', // this uses `declare module` in .d.ts so is implicitly used whenever extensions are needed84],85};86await fs.writeFile(knipConfigPath, JSON.stringify(knipConfig, null, 2));87}8889function cleanKnipConfig({ knipConfigPath }: KnipConfigOptions) {90if (fs.existsSync(knipConfigPath)) {91fs.rmSync(knipConfigPath);92}93}9495async function handlePackage({96packageDir,97knipDir,98isLocalBuild,99}: KnipPackageOptions) {100console.log(`## Processing ${packageDir}`);101102const fullDir = targetPaths.resolveRoot(packageDir);103const reportPath = resolvePath(fullDir, 'knip-report.md');104const run = createBinRunner(targetPaths.rootDir, '');105106let report = await run(107`${knipDir}/knip.js`,108'-W', // Run the desired workspace109packageDir,110'--config',111'knip.json',112'--no-exit-code', // Removing this will end the process in case there are findings by knip113'--no-progress', // Remove unnecessary debugging from output114// TODO: Add more checks when dependencies start to look ok, see https://knip.dev/reference/cli#--include115'--include',116'dependencies,unlisted',117'--reporter',118'markdown',119);120121// Adjust report paths to be relative to workspace122report = report.replaceAll(`| ${packageDir}/`, '| ');123// Adjust table separators124report = report.replaceAll(125new RegExp(`(\\| :-+ \\| :)-{${packageDir.length + 1}}`, 'g'),126(_, p1) => p1,127);128report = report.replaceAll(129new RegExp(` \\| Location {1,${packageDir.length + 2}}`, 'g'),130' | Location ',131);132133const existingReport = await fs.readFile(reportPath, 'utf8').catch(error => {134if (error.code === 'ENOENT') {135return undefined;136}137throw error;138});139140if (existingReport !== report) {141if (isLocalBuild) {142console.warn(`Knip report changed for ${packageDir}`);143await fs.writeFile(reportPath, report);144} else {145logKnipReportInstructions();146147if (existingReport) {148console.log('');149console.log(150`The conflicting file is ${relativePath(151targetPaths.rootDir,152reportPath,153)}, expecting the following content:`,154);155console.log('');156157console.log(report);158159logKnipReportInstructions();160}161throw new Error(`Knip report changed for ${packageDir}, `);162}163}164}165166export async function runKnipReports({167packageDirs,168isLocalBuild,169}: KnipExtractionOptions) {170const knipDir = targetPaths.resolveRoot('./node_modules/knip/bin/');171const knipConfigPath = targetPaths.resolveRoot('./knip.json');172const limiter = pLimit(os.cpus().length);173174await generateKnipConfig({ knipConfigPath });175try {176await Promise.all(177packageDirs.map(packageDir =>178limiter(async () =>179handlePackage({ packageDir, knipDir, isLocalBuild }),180),181),182);183await cleanKnipConfig({ knipConfigPath });184} catch (e) {185console.log(`Error occurred during knip reporting: ${e}`);186throw e;187}188}189190191