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