@backstage/repo-tools 0.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @backstage/repo-tools
2
+
3
+ ## 0.1.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 99713fd671: Introducing repo-tools package
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @backstage/errors@1.1.4-next.0
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @backstage/repo-tools
2
+
3
+ This package provides a CLI for backstage repo tooling.
4
+
5
+ ## Installation
6
+
7
+ Install the package via Yarn:
8
+
9
+ ```sh
10
+ yarn add @backstage/repo-tools
11
+ ```
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright 2022 The Backstage Authors
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ const path = require('path');
19
+
20
+ // Figure out whether we're running inside the backstage repo or as an installed dependency
21
+ /* eslint-disable-next-line no-restricted-syntax */
22
+ const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
23
+
24
+ if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
25
+ require('..');
26
+ } else {
27
+ require('ts-node').register({
28
+ transpileOnly: true,
29
+ /* eslint-disable-next-line no-restricted-syntax */
30
+ project: path.resolve(__dirname, '../../../tsconfig.json'),
31
+ compilerOptions: {
32
+ module: 'CommonJS',
33
+ },
34
+ });
35
+
36
+ require('../src');
37
+ }
@@ -0,0 +1,1018 @@
1
+ 'use strict';
2
+
3
+ var path = require('path');
4
+ var fs = require('fs-extra');
5
+ var child_process = require('child_process');
6
+ var prettier = require('prettier');
7
+ var apiExtractor = require('@microsoft/api-extractor');
8
+ var tsdoc = require('@microsoft/tsdoc');
9
+ var tsdocConfig = require('@microsoft/tsdoc-config');
10
+ var apiExtractorModel = require('@microsoft/api-extractor-model');
11
+ var MarkdownDocumenter = require('@microsoft/api-documenter/lib/documenters/MarkdownDocumenter');
12
+ var DocTable = require('@microsoft/api-documenter/lib/nodes/DocTable');
13
+ var DocTableRow = require('@microsoft/api-documenter/lib/nodes/DocTableRow');
14
+ var DocHeading = require('@microsoft/api-documenter/lib/nodes/DocHeading');
15
+ var CustomMarkdownEmitter = require('@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter');
16
+
17
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
18
+
19
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
20
+ var prettier__default = /*#__PURE__*/_interopDefaultLegacy(prettier);
21
+
22
+ var __accessCheck = (obj, member, msg) => {
23
+ if (!member.has(obj))
24
+ throw TypeError("Cannot " + msg);
25
+ };
26
+ var __privateGet = (obj, member, getter) => {
27
+ __accessCheck(obj, member, "read from private field");
28
+ return getter ? getter.call(obj) : member.get(obj);
29
+ };
30
+ var __privateAdd = (obj, member, value) => {
31
+ if (member.has(obj))
32
+ throw TypeError("Cannot add the same private member more than once");
33
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
34
+ };
35
+ var __privateSet = (obj, member, value, setter) => {
36
+ __accessCheck(obj, member, "write to private field");
37
+ setter ? setter.call(obj, value) : member.set(obj, value);
38
+ return value;
39
+ };
40
+ var _tokens;
41
+ const tmpDir = path.resolve(
42
+ process.cwd(),
43
+ "./node_modules/.cache/api-extractor"
44
+ );
45
+ const {
46
+ PackageJsonLookup
47
+ } = require("@rushstack/node-core-library/lib/PackageJsonLookup");
48
+ const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor;
49
+ PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = function tryGetPackageJsonFilePathForPatch(path$1) {
50
+ if (path$1.includes("@material-ui") && !path.dirname(path$1).endsWith("@material-ui")) {
51
+ return void 0;
52
+ }
53
+ return old.call(this, path$1);
54
+ };
55
+ const {
56
+ ApiReportGenerator
57
+ } = require("@microsoft/api-extractor/lib/generators/ApiReportGenerator");
58
+ function patchFileMessageFetcher(router, transform) {
59
+ const {
60
+ fetchAssociatedMessagesForReviewFile,
61
+ fetchUnassociatedMessagesForReviewFile
62
+ } = router;
63
+ router.fetchAssociatedMessagesForReviewFile = function patchedFetchAssociatedMessagesForReviewFile(ast) {
64
+ const messages = fetchAssociatedMessagesForReviewFile.call(this, ast);
65
+ return transform(messages, ast);
66
+ };
67
+ router.fetchUnassociatedMessagesForReviewFile = function patchedFetchUnassociatedMessagesForReviewFile() {
68
+ const messages = fetchUnassociatedMessagesForReviewFile.call(this);
69
+ return transform(messages);
70
+ };
71
+ }
72
+ const originalGenerateReviewFileContent = ApiReportGenerator.generateReviewFileContent;
73
+ ApiReportGenerator.generateReviewFileContent = function decoratedGenerateReviewFileContent(collector, ...moreArgs) {
74
+ const program = collector.program;
75
+ patchFileMessageFetcher(
76
+ collector.messageRouter,
77
+ (messages) => {
78
+ return messages.filter((message) => {
79
+ var _a, _b, _c;
80
+ if (message.messageId !== "ae-forgotten-export") {
81
+ return true;
82
+ }
83
+ const symbolMatch = message.text.match(/The symbol "([^"]+)"/);
84
+ if (!symbolMatch) {
85
+ throw new Error(
86
+ `Failed to extract symbol name from message "${message.text}"`
87
+ );
88
+ }
89
+ const [, symbolName] = symbolMatch;
90
+ const sourceFile = message.sourceFilePath && program.getSourceFile(message.sourceFilePath);
91
+ if (!sourceFile) {
92
+ throw new Error(
93
+ `Failed to find source file in program at path "${message.sourceFilePath}"`
94
+ );
95
+ }
96
+ const localName = (_a = sourceFile.identifiers) == null ? void 0 : _a.get(symbolName);
97
+ if (!localName) {
98
+ throw new Error(
99
+ `Unable to find local name of "${symbolName}" in ${sourceFile.fileName}`
100
+ );
101
+ }
102
+ const local = (_b = sourceFile.locals) == null ? void 0 : _b.get(localName);
103
+ if (!local) {
104
+ return true;
105
+ }
106
+ const type = program.getTypeChecker().getDeclaredTypeOfSymbol(local);
107
+ if (!type) {
108
+ throw new Error(
109
+ `Unable to find type declaration of "${symbolName}" in ${sourceFile.fileName}`
110
+ );
111
+ }
112
+ const declarations = (_c = type.aliasSymbol) == null ? void 0 : _c.declarations;
113
+ if (!declarations || declarations.length === 0) {
114
+ return true;
115
+ }
116
+ const isIgnored = declarations.some((declaration) => {
117
+ const tags = [declaration.jsDoc].flat().filter(Boolean).flatMap((tagNode) => tagNode.tags);
118
+ return tags.some((tag) => (tag == null ? void 0 : tag.tagName.text) === "ignore");
119
+ });
120
+ return !isIgnored;
121
+ });
122
+ }
123
+ );
124
+ const content = originalGenerateReviewFileContent.call(
125
+ this,
126
+ collector,
127
+ ...moreArgs
128
+ );
129
+ return prettier__default["default"].format(content, {
130
+ ...require("@spotify/prettier-config"),
131
+ parser: "markdown"
132
+ });
133
+ };
134
+ const PACKAGE_ROOTS = ["packages", "plugins"];
135
+ const ALLOW_WARNINGS = [
136
+ "packages/core-components",
137
+ "plugins/catalog",
138
+ "plugins/catalog-import",
139
+ "plugins/git-release-manager",
140
+ "plugins/jenkins",
141
+ "plugins/kubernetes"
142
+ ];
143
+ async function resolvePackagePath(packagePath) {
144
+ const projectRoot = path.resolve(process.cwd());
145
+ const fullPackageDir = path.resolve(projectRoot, packagePath);
146
+ const stat = await fs__default["default"].stat(fullPackageDir);
147
+ if (!stat.isDirectory()) {
148
+ return void 0;
149
+ }
150
+ try {
151
+ const packageJsonPath = path.join(fullPackageDir, "package.json");
152
+ await fs__default["default"].access(packageJsonPath);
153
+ } catch (_) {
154
+ return void 0;
155
+ }
156
+ return path.relative(projectRoot, fullPackageDir);
157
+ }
158
+ async function findSpecificPackageDirs(unresolvedPackageDirs) {
159
+ const packageDirs = new Array();
160
+ for (const unresolvedPackageDir of unresolvedPackageDirs) {
161
+ const packageDir = await resolvePackagePath(unresolvedPackageDir);
162
+ if (!packageDir) {
163
+ throw new Error(`'${unresolvedPackageDir}' is not a valid package path`);
164
+ }
165
+ packageDirs.push(packageDir);
166
+ }
167
+ if (packageDirs.length === 0) {
168
+ return void 0;
169
+ }
170
+ return packageDirs;
171
+ }
172
+ async function findPackageDirs() {
173
+ const packageDirs = new Array();
174
+ const projectRoot = path.resolve(process.cwd());
175
+ for (const packageRoot of PACKAGE_ROOTS) {
176
+ const dirs = await fs__default["default"].readdir(path.resolve(projectRoot, packageRoot));
177
+ for (const dir of dirs) {
178
+ const packageDir = await resolvePackagePath(path.join(packageRoot, dir));
179
+ if (!packageDir) {
180
+ continue;
181
+ }
182
+ packageDirs.push(packageDir);
183
+ }
184
+ }
185
+ return packageDirs;
186
+ }
187
+ async function createTemporaryTsConfig(includedPackageDirs) {
188
+ const path$1 = path.resolve(process.cwd(), "tsconfig.tmp.json");
189
+ process.once("exit", () => {
190
+ fs__default["default"].removeSync(path$1);
191
+ });
192
+ await fs__default["default"].writeJson(path$1, {
193
+ extends: "./tsconfig.json",
194
+ include: [
195
+ "packages/cli/asset-types/asset-types.d.ts",
196
+ ...includedPackageDirs.map((dir) => path.join(dir, "src"))
197
+ ]
198
+ });
199
+ return path$1;
200
+ }
201
+ async function countApiReportWarnings(projectFolder) {
202
+ const path$1 = path.resolve(projectFolder, "api-report.md");
203
+ try {
204
+ const content = await fs__default["default"].readFile(path$1, "utf8");
205
+ const lines = content.split("\n");
206
+ const lineWarnings = lines.filter(
207
+ (line) => line.includes("// Warning:")
208
+ ).length;
209
+ const trailerStart = lines.findIndex(
210
+ (line) => line === "// Warnings were encountered during analysis:"
211
+ );
212
+ const trailerWarnings = trailerStart === -1 ? 0 : lines.length - trailerStart - 4;
213
+ return lineWarnings + trailerWarnings;
214
+ } catch (error) {
215
+ if (error.code === "ENOENT") {
216
+ return 0;
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ async function getTsDocConfig() {
222
+ const tsdocConfigFile = await tsdocConfig.TSDocConfigFile.loadFile(
223
+ require.resolve("@microsoft/api-extractor/extends/tsdoc-base.json")
224
+ );
225
+ tsdocConfigFile.addTagDefinition({
226
+ tagName: "@ignore",
227
+ syntaxKind: tsdoc.TSDocTagSyntaxKind.ModifierTag
228
+ });
229
+ tsdocConfigFile.setSupportForTag("@ignore", true);
230
+ return tsdocConfigFile;
231
+ }
232
+ function logApiReportInstructions() {
233
+ console.log("");
234
+ console.log(
235
+ "*************************************************************************************"
236
+ );
237
+ console.log(
238
+ "* You have uncommitted changes to the public API of a package. *"
239
+ );
240
+ console.log(
241
+ "* To solve this, run `yarn build:api-reports` and commit all api-report.md changes. *"
242
+ );
243
+ console.log(
244
+ "*************************************************************************************"
245
+ );
246
+ console.log("");
247
+ }
248
+ async function runApiExtraction({
249
+ packageDirs,
250
+ outputDir,
251
+ isLocalBuild,
252
+ tsconfigFilePath
253
+ }) {
254
+ await fs__default["default"].remove(outputDir);
255
+ const entryPoints = packageDirs.map((packageDir) => {
256
+ return path.resolve(
257
+ process.cwd(),
258
+ `./dist-types/${packageDir}/src/index.d.ts`
259
+ );
260
+ });
261
+ let compilerState = void 0;
262
+ const warnings = new Array();
263
+ for (const packageDir of packageDirs) {
264
+ console.log(`## Processing ${packageDir}`);
265
+ const projectFolder = path.resolve(process.cwd(), packageDir);
266
+ const packageFolder = path.resolve(
267
+ process.cwd(),
268
+ "./dist-types",
269
+ packageDir
270
+ );
271
+ const warningCountBefore = await countApiReportWarnings(projectFolder);
272
+ const extractorConfig = apiExtractor.ExtractorConfig.prepare({
273
+ configObject: {
274
+ mainEntryPointFilePath: path.resolve(packageFolder, "src/index.d.ts"),
275
+ bundledPackages: [],
276
+ compiler: {
277
+ tsconfigFilePath
278
+ },
279
+ apiReport: {
280
+ enabled: true,
281
+ reportFileName: "api-report.md",
282
+ reportFolder: projectFolder,
283
+ reportTempFolder: path.resolve(outputDir, "<unscopedPackageName>")
284
+ },
285
+ docModel: {
286
+ enabled: true,
287
+ apiJsonFilePath: path.resolve(
288
+ outputDir,
289
+ "<unscopedPackageName>.api.json"
290
+ )
291
+ },
292
+ dtsRollup: {
293
+ enabled: false
294
+ },
295
+ tsdocMetadata: {
296
+ enabled: false
297
+ },
298
+ messages: {
299
+ compilerMessageReporting: {
300
+ default: {
301
+ logLevel: "none"
302
+ }
303
+ },
304
+ extractorMessageReporting: {
305
+ default: {
306
+ logLevel: "warning",
307
+ addToApiReportFile: true
308
+ }
309
+ },
310
+ tsdocMessageReporting: {
311
+ default: {
312
+ logLevel: "warning",
313
+ addToApiReportFile: true
314
+ }
315
+ }
316
+ },
317
+ newlineKind: "lf",
318
+ projectFolder
319
+ },
320
+ configObjectFullPath: projectFolder,
321
+ packageJsonFullPath: path.resolve(projectFolder, "package.json"),
322
+ tsdocConfigFile: await getTsDocConfig()
323
+ });
324
+ extractorConfig.packageFolder = packageFolder;
325
+ if (!compilerState) {
326
+ compilerState = apiExtractor.CompilerState.create(extractorConfig, {
327
+ additionalEntryPoints: entryPoints
328
+ });
329
+ }
330
+ apiExtractor.Extractor._checkCompilerCompatibility = () => {
331
+ };
332
+ let shouldLogInstructions = false;
333
+ let conflictingFile = void 0;
334
+ const extractorResult = apiExtractor.Extractor.invoke(extractorConfig, {
335
+ localBuild: isLocalBuild,
336
+ showVerboseMessages: false,
337
+ showDiagnostics: false,
338
+ messageCallback(message) {
339
+ if (message.text.includes(
340
+ "You have changed the public API signature for this project."
341
+ )) {
342
+ shouldLogInstructions = true;
343
+ const match = message.text.match(
344
+ /Please copy the file "(.*)" to "api-report\.md"/
345
+ );
346
+ if (match) {
347
+ conflictingFile = match[1];
348
+ }
349
+ }
350
+ },
351
+ compilerState
352
+ });
353
+ if (!extractorResult.succeeded) {
354
+ if (shouldLogInstructions) {
355
+ logApiReportInstructions();
356
+ if (conflictingFile) {
357
+ console.log("");
358
+ console.log(
359
+ `The conflicting file is ${path.relative(
360
+ tmpDir,
361
+ conflictingFile
362
+ )}, with the following content:`
363
+ );
364
+ console.log("");
365
+ const content = await fs__default["default"].readFile(conflictingFile, "utf8");
366
+ console.log(content);
367
+ logApiReportInstructions();
368
+ }
369
+ }
370
+ throw new Error(
371
+ `API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings`
372
+ );
373
+ }
374
+ const warningCountAfter = await countApiReportWarnings(projectFolder);
375
+ if (warningCountAfter > 0 && !ALLOW_WARNINGS.includes(packageDir)) {
376
+ throw new Error(
377
+ `The API Report for ${packageDir} is not allowed to have warnings`
378
+ );
379
+ }
380
+ if (warningCountAfter === 0 && ALLOW_WARNINGS.includes(packageDir)) {
381
+ console.log(
382
+ `No need to allow warnings for ${packageDir}, it does not have any`
383
+ );
384
+ }
385
+ if (warningCountAfter > warningCountBefore) {
386
+ warnings.push(
387
+ `The API Report for ${packageDir} introduces new warnings. Please fix these warnings in order to keep the API Reports tidy.`
388
+ );
389
+ }
390
+ }
391
+ if (warnings.length > 0) {
392
+ console.warn();
393
+ for (const warning of warnings) {
394
+ console.warn(warning);
395
+ }
396
+ console.warn();
397
+ }
398
+ }
399
+ class ExcerptTokenMatcher {
400
+ constructor(tokens) {
401
+ __privateAdd(this, _tokens, void 0);
402
+ __privateSet(this, _tokens, tokens.slice());
403
+ }
404
+ nextContent() {
405
+ const token = __privateGet(this, _tokens).shift();
406
+ if ((token == null ? void 0 : token.kind) === "Content") {
407
+ return token.text;
408
+ }
409
+ return void 0;
410
+ }
411
+ matchContent(expectedText) {
412
+ const text = this.nextContent();
413
+ return text !== expectedText;
414
+ }
415
+ getTokensUntilArrow() {
416
+ const tokens = [];
417
+ for (; ; ) {
418
+ const token = __privateGet(this, _tokens).shift();
419
+ if (token === void 0) {
420
+ return void 0;
421
+ }
422
+ if (token.kind === "Content" && token.text === ") => ") {
423
+ return tokens;
424
+ }
425
+ tokens.push(token);
426
+ }
427
+ }
428
+ getComponentReturnTokens() {
429
+ const first = __privateGet(this, _tokens).shift();
430
+ if (!first) {
431
+ return void 0;
432
+ }
433
+ const second = __privateGet(this, _tokens).shift();
434
+ if (__privateGet(this, _tokens).length !== 0) {
435
+ return void 0;
436
+ }
437
+ if (first.kind !== "Reference" || first.text !== "JSX.Element") {
438
+ return void 0;
439
+ }
440
+ if (!second) {
441
+ return [first];
442
+ } else if (second.kind === "Content" && second.text === " | null") {
443
+ return [first, second];
444
+ }
445
+ return void 0;
446
+ }
447
+ }
448
+ _tokens = new WeakMap();
449
+ const _ApiModelTransforms = class {
450
+ static deserializeWithTransforms(serialized, transforms) {
451
+ if (serialized.kind !== "Package") {
452
+ throw new Error(
453
+ `Unexpected root kind in serialized ApiPackage, ${serialized.kind}`
454
+ );
455
+ }
456
+ if (serialized.members.length !== 1) {
457
+ throw new Error(
458
+ `Unexpected members in serialized ApiPackage, [${serialized.members.map((m) => m.kind).join(" ")}]`
459
+ );
460
+ }
461
+ const [entryPoint] = serialized.members;
462
+ if (entryPoint.kind !== "EntryPoint") {
463
+ throw new Error(
464
+ `Unexpected kind in serialized ApiPackage member, ${entryPoint.kind}`
465
+ );
466
+ }
467
+ const transformed = {
468
+ ...serialized,
469
+ members: [
470
+ {
471
+ ...entryPoint,
472
+ members: entryPoint.members.map(
473
+ (member) => transforms.reduce((m, t) => t(m), member)
474
+ )
475
+ }
476
+ ]
477
+ };
478
+ return apiExtractorModel.ApiPackage.deserialize(
479
+ transformed,
480
+ transformed.metadata
481
+ );
482
+ }
483
+ static makeComponentMember(member, ret, props) {
484
+ var _a;
485
+ const declTokens = props ? [
486
+ {
487
+ kind: "Content",
488
+ text: `export declare function ${member.name}(props: `
489
+ },
490
+ ...props,
491
+ {
492
+ kind: "Content",
493
+ text: "): "
494
+ }
495
+ ] : [
496
+ {
497
+ kind: "Content",
498
+ text: `export declare function ${member.name}(): `
499
+ }
500
+ ];
501
+ return {
502
+ kind: "Function",
503
+ name: member.name,
504
+ releaseTag: member.releaseTag,
505
+ docComment: (_a = member.docComment) != null ? _a : "",
506
+ canonicalReference: member.canonicalReference,
507
+ excerptTokens: [...declTokens, ...ret],
508
+ returnTypeTokenRange: {
509
+ startIndex: declTokens.length,
510
+ endIndex: declTokens.length + ret.length
511
+ },
512
+ parameters: props ? [
513
+ {
514
+ parameterName: "props",
515
+ parameterTypeTokenRange: {
516
+ startIndex: 1,
517
+ endIndex: 1 + props.length
518
+ }
519
+ }
520
+ ] : [],
521
+ overloadIndex: 1
522
+ };
523
+ }
524
+ };
525
+ let ApiModelTransforms = _ApiModelTransforms;
526
+ ApiModelTransforms.transformArrowComponents = (member) => {
527
+ if (member.kind !== "Variable") {
528
+ return member;
529
+ }
530
+ const { name, excerptTokens } = member;
531
+ const [firstChar] = name;
532
+ if (firstChar.toLocaleUpperCase("en-US") !== firstChar) {
533
+ return member;
534
+ }
535
+ const tokens = new ExcerptTokenMatcher(excerptTokens);
536
+ if (tokens.nextContent() !== `${name}: `) {
537
+ return member;
538
+ }
539
+ const declStart = tokens.nextContent();
540
+ if (declStart === "(props: " || declStart === "(_props: ") {
541
+ const props = tokens.getTokensUntilArrow();
542
+ const ret = tokens.getComponentReturnTokens();
543
+ if (props && ret) {
544
+ return _ApiModelTransforms.makeComponentMember(member, ret, props);
545
+ }
546
+ } else if (declStart === "() => ") {
547
+ const ret = tokens.getComponentReturnTokens();
548
+ if (ret) {
549
+ return _ApiModelTransforms.makeComponentMember(member, ret);
550
+ }
551
+ }
552
+ return member;
553
+ };
554
+ ApiModelTransforms.transformTrimDeclare = (member) => {
555
+ const { excerptTokens } = member;
556
+ const firstContent = new ExcerptTokenMatcher(excerptTokens).nextContent();
557
+ if (firstContent && firstContent.startsWith("export declare ")) {
558
+ return {
559
+ ...member,
560
+ excerptTokens: [
561
+ {
562
+ kind: "Content",
563
+ text: firstContent.slice("export declare ".length)
564
+ },
565
+ ...excerptTokens.slice(1)
566
+ ]
567
+ };
568
+ }
569
+ return member;
570
+ };
571
+ async function buildDocs({
572
+ inputDir,
573
+ outputDir
574
+ }) {
575
+ const parseFile = async (filename) => {
576
+ console.log(`Reading ${filename}`);
577
+ return fs__default["default"].readJson(path.resolve(inputDir, filename));
578
+ };
579
+ const filenames = await fs__default["default"].readdir(inputDir);
580
+ const serializedPackages = await Promise.all(
581
+ filenames.filter((filename) => filename.match(/\.api\.json$/i)).map(parseFile)
582
+ );
583
+ const newModel = new apiExtractorModel.ApiModel();
584
+ for (const serialized of serializedPackages) {
585
+ newModel.addMember(
586
+ ApiModelTransforms.deserializeWithTransforms(serialized, [
587
+ ApiModelTransforms.transformArrowComponents,
588
+ ApiModelTransforms.transformTrimDeclare
589
+ ])
590
+ );
591
+ }
592
+ const _DocFrontMatter = class extends tsdoc.DocNode {
593
+ constructor(parameters) {
594
+ super(parameters);
595
+ this.values = parameters.values;
596
+ }
597
+ get kind() {
598
+ return _DocFrontMatter.kind;
599
+ }
600
+ };
601
+ let DocFrontMatter = _DocFrontMatter;
602
+ DocFrontMatter.kind = "DocFrontMatter";
603
+ class CustomCustomMarkdownEmitter extends CustomMarkdownEmitter.CustomMarkdownEmitter {
604
+ getEscapedText(text) {
605
+ return text.replace(/\\/g, "\\\\").replace(/[*#[\]_`~]/g, (x) => `\\${x}`).replace(/---/g, "\\-\\-\\-").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\|/g, "&#124;");
606
+ }
607
+ writeNode(docNode, context, docNodeSiblings) {
608
+ switch (docNode.kind) {
609
+ case DocFrontMatter.kind: {
610
+ const node = docNode;
611
+ context.writer.writeLine("---");
612
+ for (const [name, value] of Object.entries(node.values)) {
613
+ if (value) {
614
+ context.writer.writeLine(`${name}: ${value}`);
615
+ }
616
+ }
617
+ context.writer.writeLine("---");
618
+ context.writer.writeLine();
619
+ break;
620
+ }
621
+ default:
622
+ super.writeNode(docNode, context, docNodeSiblings);
623
+ }
624
+ }
625
+ emit(stringBuilder, docNode, options) {
626
+ stringBuilder._chunks.length = 0;
627
+ return super.emit(stringBuilder, docNode, options);
628
+ }
629
+ }
630
+ class CustomMarkdownDocumenter extends MarkdownDocumenter.MarkdownDocumenter {
631
+ constructor(options) {
632
+ super(options);
633
+ this._tsdocConfiguration.docNodeManager.registerDocNodes(
634
+ "@backstage/docs",
635
+ [{ docNodeKind: DocFrontMatter.kind, constructor: DocFrontMatter }]
636
+ );
637
+ this._tsdocConfiguration.docNodeManager.registerAllowableChildren(
638
+ "Paragraph",
639
+ [DocFrontMatter.kind]
640
+ );
641
+ this._markdownEmitter = new CustomCustomMarkdownEmitter(newModel);
642
+ }
643
+ _getFilenameForApiItem(apiItem) {
644
+ const filename = super._getFilenameForApiItem(apiItem);
645
+ if (filename.includes(".html.")) {
646
+ return filename.replace(/\.html\./g, "._html.");
647
+ }
648
+ return filename;
649
+ }
650
+ _writeBreadcrumb(output, apiItem) {
651
+ let title;
652
+ let description;
653
+ const name = apiItem.getScopedNameWithinPackage();
654
+ if (name) {
655
+ title = name;
656
+ description = `API reference for ${apiItem.getScopedNameWithinPackage()}`;
657
+ } else if (apiItem.kind === "Model") {
658
+ title = "Package Index";
659
+ description = "Index of all Backstage Packages";
660
+ } else if (apiItem.name) {
661
+ title = apiItem.name;
662
+ description = `API Reference for ${apiItem.name}`;
663
+ } else {
664
+ title = apiItem.displayName;
665
+ description = `API Reference for ${apiItem.displayName}`;
666
+ }
667
+ output.appendNodeInParagraph(
668
+ new DocFrontMatter({
669
+ configuration: this._tsdocConfiguration,
670
+ values: {
671
+ id: this._getFilenameForApiItem(apiItem).slice(0, -3),
672
+ title,
673
+ description
674
+ }
675
+ })
676
+ );
677
+ super._writeBreadcrumb(output, apiItem);
678
+ const oldAppendNode = output.appendNode;
679
+ output.appendNode = () => {
680
+ output.appendNode = oldAppendNode;
681
+ };
682
+ }
683
+ _writeModelTable(output, apiModel) {
684
+ const configuration = this._tsdocConfiguration;
685
+ const packagesTable = new DocTable.DocTable({
686
+ configuration,
687
+ headerTitles: ["Package", "Description"]
688
+ });
689
+ const pluginsTable = new DocTable.DocTable({
690
+ configuration,
691
+ headerTitles: ["Package", "Description"]
692
+ });
693
+ for (const apiMember of apiModel.members) {
694
+ const row = new DocTableRow.DocTableRow({ configuration }, [
695
+ this._createTitleCell(apiMember),
696
+ this._createDescriptionCell(apiMember)
697
+ ]);
698
+ if (apiMember.kind === "Package") {
699
+ this._writeApiItemPage(apiMember);
700
+ if (apiMember.name.startsWith("@backstage/plugin-")) {
701
+ pluginsTable.addRow(row);
702
+ } else {
703
+ packagesTable.addRow(row);
704
+ }
705
+ }
706
+ }
707
+ if (packagesTable.rows.length > 0) {
708
+ output.appendNode(
709
+ new DocHeading.DocHeading({
710
+ configuration: this._tsdocConfiguration,
711
+ title: "Packages"
712
+ })
713
+ );
714
+ output.appendNode(packagesTable);
715
+ }
716
+ if (pluginsTable.rows.length > 0) {
717
+ output.appendNode(
718
+ new DocHeading.DocHeading({
719
+ configuration: this._tsdocConfiguration,
720
+ title: "Plugins"
721
+ })
722
+ );
723
+ output.appendNode(pluginsTable);
724
+ }
725
+ }
726
+ }
727
+ const documenter = new CustomMarkdownDocumenter({
728
+ apiModel: newModel,
729
+ documenterConfig: {
730
+ outputTarget: "markdown",
731
+ newlineKind: "\n",
732
+ configFilePath: "",
733
+ configFile: {}
734
+ },
735
+ outputFolder: outputDir
736
+ });
737
+ await fs__default["default"].remove(outputDir);
738
+ await fs__default["default"].ensureDir(outputDir);
739
+ documenter.generateFiles();
740
+ }
741
+ async function categorizePackageDirs(projectRoot, packageDirs) {
742
+ const dirs = packageDirs.slice();
743
+ const tsPackageDirs = new Array();
744
+ const cliPackageDirs = new Array();
745
+ await Promise.all(
746
+ Array(10).fill(0).map(async () => {
747
+ var _a;
748
+ for (; ; ) {
749
+ const dir = dirs.pop();
750
+ if (!dir) {
751
+ return;
752
+ }
753
+ const pkgJson = await fs__default["default"].readJson(path.resolve(projectRoot, dir, "package.json")).catch((error) => {
754
+ if (error.code === "ENOENT") {
755
+ return void 0;
756
+ }
757
+ throw error;
758
+ });
759
+ const role = (_a = pkgJson == null ? void 0 : pkgJson.backstage) == null ? void 0 : _a.role;
760
+ if (!role) {
761
+ throw new Error(`No backstage.role in ${dir}/package.json`);
762
+ }
763
+ if (role === "cli") {
764
+ cliPackageDirs.push(dir);
765
+ } else if (role !== "frontend" && role !== "backend") {
766
+ tsPackageDirs.push(dir);
767
+ }
768
+ }
769
+ })
770
+ );
771
+ return { tsPackageDirs, cliPackageDirs };
772
+ }
773
+ function createBinRunner(cwd, path) {
774
+ return async (...command) => new Promise((resolve, reject) => {
775
+ child_process.execFile(
776
+ "node",
777
+ [path, ...command],
778
+ {
779
+ cwd,
780
+ shell: true,
781
+ timeout: 6e4,
782
+ maxBuffer: 1024 * 1024
783
+ },
784
+ (err, stdout, stderr) => {
785
+ if (err) {
786
+ reject(new Error(`${err.message}
787
+ ${stderr}`));
788
+ } else if (stderr) {
789
+ reject(new Error(`Command printed error output: ${stderr}`));
790
+ } else {
791
+ resolve(stdout);
792
+ }
793
+ }
794
+ );
795
+ });
796
+ }
797
+ function parseHelpPage(helpPageContent) {
798
+ var _a;
799
+ const [, usage] = (_a = helpPageContent.match(/^\s*Usage: (.*)$/im)) != null ? _a : [];
800
+ const lines = helpPageContent.split(/\r?\n/);
801
+ let options = new Array();
802
+ let commands = new Array();
803
+ while (lines.length > 0) {
804
+ while (lines.length > 0 && !lines[0].endsWith(":")) {
805
+ lines.shift();
806
+ }
807
+ if (lines.length > 0) {
808
+ const sectionName = lines.shift();
809
+ const sectionEndIndex = lines.findIndex(
810
+ (line) => line && !line.match(/^\s/)
811
+ );
812
+ const sectionLines = lines.slice(0, sectionEndIndex);
813
+ lines.splice(0, sectionLines.length);
814
+ const sectionItems = sectionLines.map((line) => {
815
+ var _a2;
816
+ return (_a2 = line.match(/^\s{1,8}(.*?)\s\s+/)) == null ? void 0 : _a2[1];
817
+ }).filter(Boolean);
818
+ if ((sectionName == null ? void 0 : sectionName.toLocaleLowerCase("en-US")) === "options:") {
819
+ options = sectionItems;
820
+ } else if ((sectionName == null ? void 0 : sectionName.toLocaleLowerCase("en-US")) === "commands:") {
821
+ commands = sectionItems;
822
+ } else {
823
+ throw new Error(`Unknown CLI section: ${sectionName}`);
824
+ }
825
+ }
826
+ }
827
+ return {
828
+ usage,
829
+ options,
830
+ commands
831
+ };
832
+ }
833
+ async function exploreCliHelpPages(run) {
834
+ const helpPages = new Array();
835
+ async function exploreHelpPage(...path) {
836
+ const content = await run(...path, "--help");
837
+ const parsed = parseHelpPage(content);
838
+ helpPages.push({ path, ...parsed });
839
+ await Promise.all(
840
+ parsed.commands.map(async (fullCommand) => {
841
+ const command = fullCommand.split(/[|\s]/)[0];
842
+ if (command !== "help") {
843
+ await exploreHelpPage(...path, command);
844
+ }
845
+ })
846
+ );
847
+ }
848
+ await exploreHelpPage();
849
+ helpPages.sort((a, b) => a.path.join(" ").localeCompare(b.path.join(" ")));
850
+ return helpPages;
851
+ }
852
+ function generateCliReport(name, models) {
853
+ var _a;
854
+ const content = [
855
+ `## CLI Report file for "${name}"`,
856
+ "",
857
+ "> Do not edit this file. It is a report generated by `yarn build:api-reports`",
858
+ ""
859
+ ];
860
+ for (const model of models) {
861
+ for (const helpPage of model.helpPages) {
862
+ content.push(
863
+ `### \`${[model.name, ...helpPage.path].join(" ")}\``,
864
+ "",
865
+ "```",
866
+ `Usage: ${(_a = helpPage.usage) != null ? _a : "<none>"}`
867
+ );
868
+ if (helpPage.options.length > 0) {
869
+ content.push("", "Options:", ...helpPage.options.map((l) => ` ${l}`));
870
+ }
871
+ if (helpPage.commands.length > 0) {
872
+ content.push("", "Commands:", ...helpPage.commands.map((l) => ` ${l}`));
873
+ }
874
+ content.push("```", "");
875
+ }
876
+ }
877
+ return content.join("\n");
878
+ }
879
+ async function runCliExtraction({
880
+ projectRoot,
881
+ packageDirs,
882
+ isLocalBuild
883
+ }) {
884
+ for (const packageDir of packageDirs) {
885
+ console.log(`## Processing ${packageDir}`);
886
+ const fullDir = path.resolve(projectRoot, packageDir);
887
+ const pkgJson = await fs__default["default"].readJson(path.resolve(fullDir, "package.json"));
888
+ if (!pkgJson.bin) {
889
+ throw new Error(`CLI Package in ${packageDir} has no bin field`);
890
+ }
891
+ const models = new Array();
892
+ if (typeof pkgJson.bin === "string") {
893
+ const run = createBinRunner(fullDir, pkgJson.bin);
894
+ const helpPages = await exploreCliHelpPages(run);
895
+ models.push({ name: path.basename(pkgJson.bin), helpPages });
896
+ } else {
897
+ for (const [name, path] of Object.entries(pkgJson.bin)) {
898
+ const run = createBinRunner(fullDir, path);
899
+ const helpPages = await exploreCliHelpPages(run);
900
+ models.push({ name, helpPages });
901
+ }
902
+ }
903
+ const report = generateCliReport(pkgJson.name, models);
904
+ const reportPath = path.resolve(fullDir, "cli-report.md");
905
+ const existingReport = await fs__default["default"].readFile(reportPath, "utf8").catch((error) => {
906
+ if (error.code === "ENOENT") {
907
+ return void 0;
908
+ }
909
+ throw error;
910
+ });
911
+ if (existingReport !== report) {
912
+ if (isLocalBuild) {
913
+ console.warn(`CLI report changed for ${packageDir}`);
914
+ await fs__default["default"].writeFile(reportPath, report);
915
+ } else {
916
+ logApiReportInstructions();
917
+ if (existingReport) {
918
+ console.log("");
919
+ console.log(
920
+ `The conflicting file is ${path.relative(
921
+ projectRoot,
922
+ reportPath
923
+ )}, expecting the following content:`
924
+ );
925
+ console.log("");
926
+ console.log(report);
927
+ logApiReportInstructions();
928
+ }
929
+ throw new Error(`CLI report changed for ${packageDir}, `);
930
+ }
931
+ }
932
+ }
933
+ }
934
+
935
+ var apiReports = async (paths, opts) => {
936
+ const tmpDir = path.resolve(
937
+ process.cwd(),
938
+ "./node_modules/.cache/api-extractor"
939
+ );
940
+ const projectRoot = path.resolve(process.cwd());
941
+ const isCiBuild = opts.ci;
942
+ const isDocsBuild = opts.docs;
943
+ const runTsc = opts.tsc;
944
+ const selectedPackageDirs = await findSpecificPackageDirs(paths);
945
+ if (selectedPackageDirs && isCiBuild) {
946
+ throw new Error(
947
+ "Package path arguments are not supported together with the --ci flag"
948
+ );
949
+ }
950
+ if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) {
951
+ console.log("");
952
+ console.log(
953
+ "TIP: You can generate api-reports for select packages by passing package paths:"
954
+ );
955
+ console.log("");
956
+ console.log(
957
+ " yarn build:api-reports packages/config packages/core-plugin-api"
958
+ );
959
+ console.log("");
960
+ }
961
+ let temporaryTsConfigPath;
962
+ if (selectedPackageDirs) {
963
+ temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs);
964
+ }
965
+ const tsconfigFilePath = temporaryTsConfigPath != null ? temporaryTsConfigPath : path.resolve(projectRoot, "tsconfig.json");
966
+ if (runTsc) {
967
+ await fs__default["default"].remove(path.resolve(projectRoot, "dist-types"));
968
+ const { status } = child_process.spawnSync(
969
+ "yarn",
970
+ [
971
+ "tsc",
972
+ ["--project", tsconfigFilePath],
973
+ ["--skipLibCheck", "false"],
974
+ ["--incremental", "false"]
975
+ ].flat(),
976
+ {
977
+ stdio: "inherit",
978
+ shell: true,
979
+ cwd: projectRoot
980
+ }
981
+ );
982
+ if (status !== 0) {
983
+ process.exit(status || void 0);
984
+ }
985
+ }
986
+ const packageDirs = selectedPackageDirs != null ? selectedPackageDirs : await findPackageDirs();
987
+ const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs(
988
+ projectRoot,
989
+ packageDirs
990
+ );
991
+ if (tsPackageDirs.length > 0) {
992
+ console.log("# Generating package API reports");
993
+ await runApiExtraction({
994
+ packageDirs: tsPackageDirs,
995
+ outputDir: tmpDir,
996
+ isLocalBuild: !isCiBuild,
997
+ tsconfigFilePath
998
+ });
999
+ }
1000
+ if (cliPackageDirs.length > 0) {
1001
+ console.log("# Generating package CLI reports");
1002
+ await runCliExtraction({
1003
+ projectRoot,
1004
+ packageDirs: cliPackageDirs,
1005
+ isLocalBuild: !isCiBuild
1006
+ });
1007
+ }
1008
+ if (isDocsBuild) {
1009
+ console.log("# Generating package documentation");
1010
+ await buildDocs({
1011
+ inputDir: tmpDir,
1012
+ outputDir: path.resolve(projectRoot, "docs/reference")
1013
+ });
1014
+ }
1015
+ };
1016
+
1017
+ exports["default"] = apiReports;
1018
+ //# sourceMappingURL=api-reports-3c013a5f.cjs.js.map
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ var commander = require('commander');
4
+ var chalk = require('chalk');
5
+ var errors = require('@backstage/errors');
6
+
7
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
+
9
+ var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk);
10
+
11
+ class CustomError extends Error {
12
+ get name() {
13
+ return this.constructor.name;
14
+ }
15
+ }
16
+ class ExitCodeError extends CustomError {
17
+ constructor(code, command) {
18
+ super(
19
+ command ? `Command '${command}' exited with code ${code}` : `Child exited with code ${code}`
20
+ );
21
+ this.code = code;
22
+ }
23
+ }
24
+ function exitWithError(error) {
25
+ if (error instanceof ExitCodeError) {
26
+ process.stderr.write(`
27
+ ${chalk__default["default"].red(error.message)}
28
+
29
+ `);
30
+ process.exit(error.code);
31
+ } else {
32
+ process.stderr.write(`
33
+ ${chalk__default["default"].red(`${error}`)}
34
+
35
+ `);
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ function registerCommands(program) {
41
+ program.command("api-reports [path...]").option("--ci", "CI run checks that there is no changes on API reports").option("--tsc", "executes the tsc compilation before extracting the APIs").option("--docs", "generates the api documentation").description("Generate an API report for selected packages").action(
42
+ lazy(() => Promise.resolve().then(function () { return require('./cjs/api-reports-3c013a5f.cjs.js'); }).then((m) => m.default))
43
+ );
44
+ }
45
+ function lazy(getActionFunc) {
46
+ return async (...args) => {
47
+ try {
48
+ const actionFunc = await getActionFunc();
49
+ await actionFunc(...args);
50
+ process.exit(0);
51
+ } catch (error) {
52
+ errors.assertError(error);
53
+ exitWithError(error);
54
+ }
55
+ };
56
+ }
57
+
58
+ const main = (argv) => {
59
+ commander.program.name("backstage-repo-tools").version("1.0");
60
+ registerCommands(commander.program);
61
+ commander.program.on("command:*", () => {
62
+ console.log();
63
+ console.log(chalk__default["default"].red(`Invalid command: ${commander.program.args.join(" ")}`));
64
+ console.log();
65
+ commander.program.outputHelp();
66
+ process.exit(1);
67
+ });
68
+ commander.program.parse(argv);
69
+ };
70
+ process.on("unhandledRejection", (rejection) => {
71
+ if (rejection instanceof Error) {
72
+ exitWithError(rejection);
73
+ } else {
74
+ exitWithError(new Error(`Unknown rejection: '${rejection}'`));
75
+ }
76
+ });
77
+ main(process.argv);
78
+ //# sourceMappingURL=index.cjs.js.map
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@backstage/repo-tools",
3
+ "description": "CLI for Backstage repo tooling ",
4
+ "version": "0.1.0-next.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "backstage": {
9
+ "role": "cli"
10
+ },
11
+ "homepage": "https://backstage.io",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/backstage/backstage",
15
+ "directory": "packages/repo-tools"
16
+ },
17
+ "keywords": [
18
+ "backstage"
19
+ ],
20
+ "license": "Apache-2.0",
21
+ "main": "dist/index.cjs.js",
22
+ "scripts": {
23
+ "build": "backstage-cli package build",
24
+ "lint": "backstage-cli package lint",
25
+ "test": "backstage-cli package test",
26
+ "clean": "backstage-cli package clean",
27
+ "start": "nodemon --"
28
+ },
29
+ "bin": {
30
+ "backstage-repo-tools": "bin/backstage-repo-tools"
31
+ },
32
+ "dependencies": {
33
+ "@backstage/errors": "^1.1.4-next.0",
34
+ "@microsoft/api-documenter": "^7.17.11",
35
+ "@microsoft/api-extractor": "^7.23.0",
36
+ "@microsoft/api-extractor-model": "^7.17.2",
37
+ "@microsoft/tsdoc": "0.14.1",
38
+ "chalk": "^4.0.0",
39
+ "commander": "^9.1.0",
40
+ "fs-extra": "10.1.0",
41
+ "ts-node": "^10.0.0"
42
+ },
43
+ "files": [
44
+ "bin",
45
+ "dist/**/*.js"
46
+ ],
47
+ "nodemonConfig": {
48
+ "watch": "./src",
49
+ "exec": "bin/backstage-repo-tools",
50
+ "ext": "ts"
51
+ }
52
+ }