@supernovaio/cli 2.0.38 → 2.0.40
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/README.md +1 -0
- package/dist/code-analyzer/analyzers/component-usage.d.ts +6 -0
- package/dist/code-analyzer/analyzers/component-usage.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/component-usage.js +344 -0
- package/dist/code-analyzer/analyzers/component-usage.js.map +1 -0
- package/dist/code-analyzer/analyzers/helpers.d.ts +11 -0
- package/dist/code-analyzer/analyzers/helpers.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/helpers.js +59 -0
- package/dist/code-analyzer/analyzers/helpers.js.map +1 -0
- package/dist/code-analyzer/analyzers/index.d.ts +8 -0
- package/dist/code-analyzer/analyzers/index.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/index.js +11 -0
- package/dist/code-analyzer/analyzers/index.js.map +1 -0
- package/dist/code-analyzer/analyzers/jsdoc.d.ts +3 -0
- package/dist/code-analyzer/analyzers/jsdoc.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/jsdoc.js +7 -0
- package/dist/code-analyzer/analyzers/jsdoc.js.map +1 -0
- package/dist/code-analyzer/analyzers/static-components.d.ts +3 -0
- package/dist/code-analyzer/analyzers/static-components.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/static-components.js +399 -0
- package/dist/code-analyzer/analyzers/static-components.js.map +1 -0
- package/dist/code-analyzer/analyzers/storybook-docs.d.ts +3 -0
- package/dist/code-analyzer/analyzers/storybook-docs.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/storybook-docs.js +163 -0
- package/dist/code-analyzer/analyzers/storybook-docs.js.map +1 -0
- package/dist/code-analyzer/analyzers/storybook-stories.d.ts +3 -0
- package/dist/code-analyzer/analyzers/storybook-stories.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/storybook-stories.js +91 -0
- package/dist/code-analyzer/analyzers/storybook-stories.js.map +1 -0
- package/dist/code-analyzer/analyzers/types.d.ts +88 -0
- package/dist/code-analyzer/analyzers/types.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/types.js +12 -0
- package/dist/code-analyzer/analyzers/types.js.map +1 -0
- package/dist/code-analyzer/analyzers/typescript-api.d.ts +3 -0
- package/dist/code-analyzer/analyzers/typescript-api.d.ts.map +1 -0
- package/dist/code-analyzer/analyzers/typescript-api.js +7 -0
- package/dist/code-analyzer/analyzers/typescript-api.js.map +1 -0
- package/dist/code-analyzer/components/analyze.d.ts +8 -0
- package/dist/code-analyzer/components/analyze.d.ts.map +1 -0
- package/dist/code-analyzer/components/analyze.js +38 -0
- package/dist/code-analyzer/components/analyze.js.map +1 -0
- package/dist/code-analyzer/components/mappers/component.d.ts +4 -0
- package/dist/code-analyzer/components/mappers/component.d.ts.map +1 -0
- package/dist/code-analyzer/components/mappers/component.js +20 -0
- package/dist/code-analyzer/components/mappers/component.js.map +1 -0
- package/dist/code-analyzer/components/mappers/property.d.ts +4 -0
- package/dist/code-analyzer/components/mappers/property.d.ts.map +1 -0
- package/dist/code-analyzer/components/mappers/property.js +19 -0
- package/dist/code-analyzer/components/mappers/property.js.map +1 -0
- package/dist/code-analyzer/components/parser/index.d.ts +6 -0
- package/dist/code-analyzer/components/parser/index.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/index.js +9 -0
- package/dist/code-analyzer/components/parser/index.js.map +1 -0
- package/dist/code-analyzer/components/parser/module-parser.d.ts +12 -0
- package/dist/code-analyzer/components/parser/module-parser.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/module-parser.js +116 -0
- package/dist/code-analyzer/components/parser/module-parser.js.map +1 -0
- package/dist/code-analyzer/components/parser/parser.d.ts +46 -0
- package/dist/code-analyzer/components/parser/parser.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/parser.js +904 -0
- package/dist/code-analyzer/components/parser/parser.js.map +1 -0
- package/dist/code-analyzer/components/parser/types.d.ts +123 -0
- package/dist/code-analyzer/components/parser/types.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/types.js +21 -0
- package/dist/code-analyzer/components/parser/types.js.map +1 -0
- package/dist/code-analyzer/components/parser/utils/build-filter.d.ts +3 -0
- package/dist/code-analyzer/components/parser/utils/build-filter.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/utils/build-filter.js +31 -0
- package/dist/code-analyzer/components/parser/utils/build-filter.js.map +1 -0
- package/dist/code-analyzer/components/parser/utils/filter-duplicates.d.ts +3 -0
- package/dist/code-analyzer/components/parser/utils/filter-duplicates.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/utils/filter-duplicates.js +17 -0
- package/dist/code-analyzer/components/parser/utils/filter-duplicates.js.map +1 -0
- package/dist/code-analyzer/components/parser/utils/is-react-component.d.ts +4 -0
- package/dist/code-analyzer/components/parser/utils/is-react-component.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/utils/is-react-component.js +43 -0
- package/dist/code-analyzer/components/parser/utils/is-react-component.js.map +1 -0
- package/dist/code-analyzer/components/parser/utils/trim-file-name.d.ts +2 -0
- package/dist/code-analyzer/components/parser/utils/trim-file-name.d.ts.map +1 -0
- package/dist/code-analyzer/components/parser/utils/trim-file-name.js +22 -0
- package/dist/code-analyzer/components/parser/utils/trim-file-name.js.map +1 -0
- package/dist/code-analyzer/components/types.d.ts +18 -0
- package/dist/code-analyzer/components/types.d.ts.map +1 -0
- package/dist/code-analyzer/components/types.js +5 -0
- package/dist/code-analyzer/components/types.js.map +1 -0
- package/dist/code-analyzer/components/utils/get-module-exports-path.d.ts +2 -0
- package/dist/code-analyzer/components/utils/get-module-exports-path.d.ts.map +1 -0
- package/dist/code-analyzer/components/utils/get-module-exports-path.js +60 -0
- package/dist/code-analyzer/components/utils/get-module-exports-path.js.map +1 -0
- package/dist/code-analyzer/index.d.ts +5 -0
- package/dist/code-analyzer/index.d.ts.map +1 -0
- package/dist/code-analyzer/index.js +8 -0
- package/dist/code-analyzer/index.js.map +1 -0
- package/dist/code-analyzer/orchestrator/index.d.ts +2 -0
- package/dist/code-analyzer/orchestrator/index.d.ts.map +1 -0
- package/dist/code-analyzer/orchestrator/index.js +5 -0
- package/dist/code-analyzer/orchestrator/index.js.map +1 -0
- package/dist/code-analyzer/orchestrator/run-analysis.d.ts +19 -0
- package/dist/code-analyzer/orchestrator/run-analysis.d.ts.map +1 -0
- package/dist/code-analyzer/orchestrator/run-analysis.js +109 -0
- package/dist/code-analyzer/orchestrator/run-analysis.js.map +1 -0
- package/dist/code-analyzer/snapshot/index.d.ts +2 -0
- package/dist/code-analyzer/snapshot/index.d.ts.map +1 -0
- package/dist/code-analyzer/snapshot/index.js +5 -0
- package/dist/code-analyzer/snapshot/index.js.map +1 -0
- package/dist/code-analyzer/snapshot/write-snapshot.d.ts +17 -0
- package/dist/code-analyzer/snapshot/write-snapshot.d.ts.map +1 -0
- package/dist/code-analyzer/snapshot/write-snapshot.js +60 -0
- package/dist/code-analyzer/snapshot/write-snapshot.js.map +1 -0
- package/dist/commands/analyze/adoption.d.ts +15 -0
- package/dist/commands/analyze/adoption.d.ts.map +1 -0
- package/dist/commands/analyze/adoption.js +34 -0
- package/dist/commands/analyze/adoption.js.map +1 -0
- package/dist/commands/analyze/components.d.ts +15 -0
- package/dist/commands/analyze/components.d.ts.map +1 -0
- package/dist/commands/analyze/components.js +34 -0
- package/dist/commands/analyze/components.js.map +1 -0
- package/dist/commands/analyze/status.d.ts +24 -0
- package/dist/commands/analyze/status.d.ts.map +1 -0
- package/dist/commands/analyze/status.js +54 -0
- package/dist/commands/analyze/status.js.map +1 -0
- package/dist/commands/analyze.d.ts +15 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +34 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/components-import.d.ts +3 -3
- package/dist/commands/components-import.d.ts.map +1 -1
- package/dist/commands/components-import.js +9 -3
- package/dist/commands/components-import.js.map +1 -1
- package/dist/commands/publish-documentation.d.ts +2 -2
- package/dist/commands/run-local-exporter.js +2 -2
- package/dist/commands/run-local-exporter.js.map +1 -1
- package/dist/commands/storybook-import.d.ts +2 -2
- package/dist/commands/template-upload.d.ts.map +1 -1
- package/dist/commands/template-upload.js +10 -12
- package/dist/commands/template-upload.js.map +1 -1
- package/dist/types/config.d.ts +41 -5
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +9 -2
- package/dist/types/config.js.map +1 -1
- package/dist/types/types.js +2 -2
- package/dist/types/types.js.map +1 -1
- package/dist/utils/analyze-command.d.ts +42 -0
- package/dist/utils/analyze-command.d.ts.map +1 -0
- package/dist/utils/analyze-command.js +606 -0
- package/dist/utils/analyze-command.js.map +1 -0
- package/dist/utils/analyze-status.d.ts +7 -0
- package/dist/utils/analyze-status.d.ts.map +1 -0
- package/dist/utils/analyze-status.js +94 -0
- package/dist/utils/analyze-status.js.map +1 -0
- package/dist/utils/config.service.d.ts +1 -2
- package/dist/utils/config.service.d.ts.map +1 -1
- package/dist/utils/config.service.js +5 -7
- package/dist/utils/config.service.js.map +1 -1
- package/dist/utils/discover.d.ts +2 -2
- package/dist/utils/discover.d.ts.map +1 -1
- package/dist/utils/discover.js +25 -23
- package/dist/utils/discover.js.map +1 -1
- package/dist/utils/figma-tokens-data-loader.js +2 -2
- package/dist/utils/figma-tokens-data-loader.js.map +1 -1
- package/dist/utils/http-client.d.ts +4 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +18 -0
- package/dist/utils/http-client.js.map +1 -0
- package/dist/utils/run-exporter/exporter-utils.js +2 -2
- package/dist/utils/run-exporter/exporter-utils.js.map +1 -1
- package/dist/utils/validate-templates.d.ts +1 -1
- package/dist/utils/validate-templates.d.ts.map +1 -1
- package/dist/utils/validate-templates.js +4 -4
- package/dist/utils/validate-templates.js.map +1 -1
- package/oclif.manifest.json +216 -1
- package/package.json +12 -4
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="48ae5534-bfe7-5b1e-a760-916b78e71f6d")}catch(e){}}();
|
|
3
|
+
import { Flags } from "@oclif/core";
|
|
4
|
+
import { runCodeAnalysis } from "../code-analyzer/orchestrator/run-analysis.js";
|
|
5
|
+
import AdmZip from "adm-zip";
|
|
6
|
+
import axios, { isAxiosError } from "axios";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { SentryCommand } from "../types/index.js";
|
|
12
|
+
import { watchAnalyzeStatus } from "./analyze-status.js";
|
|
13
|
+
import { createApiClient } from "./http-client.js";
|
|
14
|
+
export const AnalyzeCommandConfigSchema = z.object({
|
|
15
|
+
exclude: z.array(z.string()).optional(),
|
|
16
|
+
designSystemId: z.string().optional(),
|
|
17
|
+
package: z.union([z.string(), z.array(z.string())]).optional(),
|
|
18
|
+
dryRun: z.boolean().optional(),
|
|
19
|
+
noWait: z.boolean().optional(),
|
|
20
|
+
});
|
|
21
|
+
export const analyzeFlags = {
|
|
22
|
+
designSystemId: Flags.string({ char: "d", description: "Design system ID" }),
|
|
23
|
+
package: Flags.string({
|
|
24
|
+
char: "p",
|
|
25
|
+
description: "Package name eg. @design-system-package. Can be repeated.",
|
|
26
|
+
multiple: true,
|
|
27
|
+
}),
|
|
28
|
+
exclude: Flags.string({
|
|
29
|
+
description: "Path to exclude from stories/docs/adoption scan. Can be repeated.",
|
|
30
|
+
multiple: true,
|
|
31
|
+
}),
|
|
32
|
+
dryRun: Flags.boolean({
|
|
33
|
+
description: "Run and write local snapshot only. Never uploads.",
|
|
34
|
+
default: false,
|
|
35
|
+
}),
|
|
36
|
+
noWait: Flags.boolean({
|
|
37
|
+
description: "Start processing and exit without waiting for completion.",
|
|
38
|
+
default: false,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
export class AnalyzeCommandBase extends SentryCommand {
|
|
42
|
+
get commandId() {
|
|
43
|
+
return this.id ?? this.constructor.name;
|
|
44
|
+
}
|
|
45
|
+
get configSchema() {
|
|
46
|
+
return AnalyzeCommandConfigSchema;
|
|
47
|
+
}
|
|
48
|
+
async executeAnalyze(flags, scannerType) {
|
|
49
|
+
const rootDir = process.cwd();
|
|
50
|
+
const config = this.configService.get();
|
|
51
|
+
const analyzeConfig = config?.analyze ?? {};
|
|
52
|
+
const configuredDesignSystemId = config?.designSystemId;
|
|
53
|
+
const packageFlags = normalizeStringList(flags.package);
|
|
54
|
+
const configuredPackages = normalizeStringList(analyzeConfig.packages);
|
|
55
|
+
const componentPackages = packageFlags.length > 0 ? packageFlags : configuredPackages;
|
|
56
|
+
if (componentPackages.length === 0) {
|
|
57
|
+
this.error("Parameter --package is required.");
|
|
58
|
+
}
|
|
59
|
+
const excludedPackages = normalizeStringList(analyzeConfig.excludedPackages);
|
|
60
|
+
const repoId = analyzeConfig.repoId ?? crypto.randomUUID();
|
|
61
|
+
const dryRun = flags.dryRun ?? false;
|
|
62
|
+
const noWait = flags.noWait ?? false;
|
|
63
|
+
const analyzeDesignSystemId = flags.designSystemId ?? configuredDesignSystemId;
|
|
64
|
+
const uploadContext = dryRun
|
|
65
|
+
? null
|
|
66
|
+
: {
|
|
67
|
+
apiClient: await createApiClient(this.env),
|
|
68
|
+
designSystemId: analyzeDesignSystemId ?? (await this.promptDesignSystemId()),
|
|
69
|
+
};
|
|
70
|
+
const executionTargets = resolveExecutionTargets({
|
|
71
|
+
componentPackages,
|
|
72
|
+
excludedPackages,
|
|
73
|
+
rootDir,
|
|
74
|
+
scannerType,
|
|
75
|
+
});
|
|
76
|
+
const plannedTargets = executionTargets.map((executionTarget, index) => createPlannedExecutionTarget({
|
|
77
|
+
executionTarget,
|
|
78
|
+
index,
|
|
79
|
+
rootDir,
|
|
80
|
+
total: executionTargets.length,
|
|
81
|
+
}));
|
|
82
|
+
this.logPhase("Analyze started");
|
|
83
|
+
this.log(`Root: ${path.basename(rootDir) || rootDir}`);
|
|
84
|
+
this.log(`Targets: ${plannedTargets.length}`);
|
|
85
|
+
this.log(`Scan types: ${summarizeScanTypes(executionTargets)}`);
|
|
86
|
+
this.logPhase("Plan");
|
|
87
|
+
for (const plannedTarget of plannedTargets) {
|
|
88
|
+
const discoverySuffix = plannedTarget.executionTarget.discovery ? " discovery" : "";
|
|
89
|
+
this.log(`${plannedTarget.targetId} ${padScanType(plannedTarget.executionTarget.scanType)} pkg=${plannedTarget.packageLabel} path=${plannedTarget.pathLabel}${discoverySuffix}`);
|
|
90
|
+
}
|
|
91
|
+
const preparedUploads = [];
|
|
92
|
+
let skippedTargets = 0;
|
|
93
|
+
let skippedNoAdoption = 0;
|
|
94
|
+
let skippedNoComponents = 0;
|
|
95
|
+
this.logPhase("Analyze phase");
|
|
96
|
+
for (const plannedTarget of plannedTargets) {
|
|
97
|
+
const { analyzeTarget, executionTarget, packageLabel, pathLabel, targetId } = plannedTarget;
|
|
98
|
+
this.log(`${targetId} ${formatStatus("start")} ${padScanType(executionTarget.scanType)} path=${pathLabel} pkg=${packageLabel}`);
|
|
99
|
+
if (executionTarget.scanType === "components" && !analyzeTarget) {
|
|
100
|
+
const nodeModulesMessage = createMissingComponentsSourceMessage(executionTarget.importFrom);
|
|
101
|
+
if (scannerType === "components") {
|
|
102
|
+
this.error(nodeModulesMessage);
|
|
103
|
+
}
|
|
104
|
+
skippedTargets += 1;
|
|
105
|
+
this.log(`${targetId} ${formatStatus("skip")} ${padScanType(executionTarget.scanType)} ${nodeModulesMessage}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!analyzeTarget) {
|
|
109
|
+
this.error(`Analyze target could not be resolved for ${stringifyImportFrom(executionTarget.importFrom)}.`);
|
|
110
|
+
}
|
|
111
|
+
const analysisResult = await runCodeAnalysis({
|
|
112
|
+
designSystemId: analyzeDesignSystemId,
|
|
113
|
+
excludePaths: flags.exclude,
|
|
114
|
+
importFrom: analyzeTarget.importFrom,
|
|
115
|
+
mode: executionTarget.scanType,
|
|
116
|
+
projectRoot: analyzeTarget.rootDir,
|
|
117
|
+
snapshotBaseRoot: shouldWriteSnapshotsToExecutionRoot(analyzeTarget.rootDir) ? rootDir : undefined,
|
|
118
|
+
});
|
|
119
|
+
const hasUsage = executionTarget.scanType === "usage" && hasUsageInSnapshot(analysisResult.snapshotRoot);
|
|
120
|
+
if (executionTarget.scanType === "usage" && !hasUsage) {
|
|
121
|
+
skippedTargets += 1;
|
|
122
|
+
skippedNoAdoption += 1;
|
|
123
|
+
this.log(`${targetId} ${formatStatus("skip")} ${padScanType(executionTarget.scanType)} no adoption detected`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (executionTarget.scanType === "components" && analysisResult.components.length === 0) {
|
|
127
|
+
skippedTargets += 1;
|
|
128
|
+
skippedNoComponents += 1;
|
|
129
|
+
this.log(`${targetId} ${formatStatus("skip")} ${padScanType(executionTarget.scanType)} no components detected`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (executionTarget.scanType === "components") {
|
|
133
|
+
this.log(`${targetId} ${formatStatus("done")} ${padScanType(executionTarget.scanType)} snapshot=${analysisResult.snapshotId} count=${analysisResult.components.length}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.log(`${targetId} ${formatStatus("done")} ${padScanType(executionTarget.scanType)} snapshot=${analysisResult.snapshotId} count=${countUsageRecordsInSnapshot(analysisResult.snapshotRoot)}`);
|
|
137
|
+
}
|
|
138
|
+
preparedUploads.push({
|
|
139
|
+
localSnapshotId: analysisResult.snapshotId,
|
|
140
|
+
packageLabel,
|
|
141
|
+
pathLabel,
|
|
142
|
+
repoId,
|
|
143
|
+
repoPackageName: readPackageName(analyzeTarget.rootDir),
|
|
144
|
+
scannerType: executionTarget.scanType,
|
|
145
|
+
snapshotRoot: analysisResult.snapshotRoot,
|
|
146
|
+
targetId,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.logPhase("Analyze summary");
|
|
150
|
+
this.log(`Local snapshots: ${preparedUploads.length}`);
|
|
151
|
+
this.log(`Skipped: ${skippedTargets}`);
|
|
152
|
+
if (skippedNoAdoption > 0) {
|
|
153
|
+
this.log(`- no adoption: ${skippedNoAdoption}`);
|
|
154
|
+
}
|
|
155
|
+
if (skippedNoComponents > 0) {
|
|
156
|
+
this.log(`- no components: ${skippedNoComponents}`);
|
|
157
|
+
}
|
|
158
|
+
if (preparedUploads.length === 0) {
|
|
159
|
+
this.log("No analyzable results found. You are likely running the command from the wrong directory.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.log(`Ready to upload: ${preparedUploads.length}`);
|
|
163
|
+
if (dryRun) {
|
|
164
|
+
this.persistAnalyzeConfig({
|
|
165
|
+
analyzeConfig,
|
|
166
|
+
designSystemId: analyzeDesignSystemId,
|
|
167
|
+
excludedPackages,
|
|
168
|
+
packages: componentPackages,
|
|
169
|
+
repoId,
|
|
170
|
+
});
|
|
171
|
+
this.log("Upload skipped (dry-run enabled).");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!uploadContext) {
|
|
175
|
+
this.error("Upload context is missing.");
|
|
176
|
+
}
|
|
177
|
+
const { apiClient, designSystemId } = uploadContext;
|
|
178
|
+
this.logPhase("Upload phase");
|
|
179
|
+
const uploadResults = await Promise.allSettled(preparedUploads.map((upload, index) => this.uploadSnapshot({
|
|
180
|
+
apiClient,
|
|
181
|
+
designSystemId,
|
|
182
|
+
uploadId: formatSequenceId("U", index + 1, preparedUploads.length),
|
|
183
|
+
repoId: upload.repoId,
|
|
184
|
+
repoPackageName: upload.repoPackageName,
|
|
185
|
+
scannerType: upload.scannerType,
|
|
186
|
+
snapshotRoot: upload.snapshotRoot,
|
|
187
|
+
localSnapshotId: upload.localSnapshotId,
|
|
188
|
+
packageLabel: upload.packageLabel,
|
|
189
|
+
pathLabel: upload.pathLabel,
|
|
190
|
+
targetId: upload.targetId,
|
|
191
|
+
})));
|
|
192
|
+
const failedUploads = uploadResults.filter((result) => result.status === "rejected");
|
|
193
|
+
if (failedUploads.length > 0) {
|
|
194
|
+
throw new AggregateError(failedUploads.map(failure => failure.reason), `${failedUploads.length} snapshot upload(s) failed.`);
|
|
195
|
+
}
|
|
196
|
+
this.logPhase("Upload summary");
|
|
197
|
+
this.log(`Uploaded snapshots: ${preparedUploads.length}/${preparedUploads.length}`);
|
|
198
|
+
this.log(`- components: ${preparedUploads.filter(upload => upload.scannerType === "components").length}`);
|
|
199
|
+
this.log(`- usage: ${preparedUploads.filter(upload => upload.scannerType === "usage").length}`);
|
|
200
|
+
const processingRun = await this.startProcessingRun({
|
|
201
|
+
apiClient,
|
|
202
|
+
designSystemId,
|
|
203
|
+
});
|
|
204
|
+
this.persistAnalyzeConfig({
|
|
205
|
+
analyzeConfig,
|
|
206
|
+
designSystemId,
|
|
207
|
+
excludedPackages,
|
|
208
|
+
packages: componentPackages,
|
|
209
|
+
repoId,
|
|
210
|
+
});
|
|
211
|
+
if (noWait) {
|
|
212
|
+
this.log("Processing continues in the background. Use `supernova analyze status` to check progress.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.logPhase("Watch phase");
|
|
216
|
+
await watchAnalyzeStatus({
|
|
217
|
+
apiClient,
|
|
218
|
+
designSystemId,
|
|
219
|
+
error: message => this.error(message),
|
|
220
|
+
});
|
|
221
|
+
this.logPhase("Analyze complete");
|
|
222
|
+
this.log(`Processing run: ${processingRun.processingRunId}`);
|
|
223
|
+
this.log(`Uploaded: ${preparedUploads.length}/${preparedUploads.length}`);
|
|
224
|
+
}
|
|
225
|
+
logPhase(title) {
|
|
226
|
+
this.log("");
|
|
227
|
+
this.log(title);
|
|
228
|
+
}
|
|
229
|
+
persistAnalyzeConfig(input) {
|
|
230
|
+
const { analyzeConfig, designSystemId, excludedPackages, packages, repoId } = input;
|
|
231
|
+
this.configService.update({
|
|
232
|
+
analyze: {
|
|
233
|
+
...analyzeConfig,
|
|
234
|
+
repoId,
|
|
235
|
+
packages,
|
|
236
|
+
excludedPackages,
|
|
237
|
+
},
|
|
238
|
+
...(designSystemId ? { designSystemId } : {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async uploadSnapshot(input) {
|
|
242
|
+
const { apiClient, designSystemId, localSnapshotId, packageLabel, pathLabel, repoId, repoPackageName, scannerType, snapshotRoot, targetId, uploadId, } = input;
|
|
243
|
+
const initPath = `/code-snapshots/upload`;
|
|
244
|
+
const local = await this.createArchiveFromSnapshotRoot(snapshotRoot);
|
|
245
|
+
const initPayload = {
|
|
246
|
+
archiveChecksum: local.checksum,
|
|
247
|
+
archiveName: local.archiveName,
|
|
248
|
+
archiveSize: local.sizeBytes,
|
|
249
|
+
designSystemId,
|
|
250
|
+
repoId,
|
|
251
|
+
repoPackageName,
|
|
252
|
+
scannerType: toApiScannerType(scannerType),
|
|
253
|
+
};
|
|
254
|
+
this.log(`${uploadId} ${formatStatus("start")} ${padScanType(scannerType)} source=${targetId} path=${pathLabel} pkg=${packageLabel} snapshot=${localSnapshotId}`);
|
|
255
|
+
let initResponse;
|
|
256
|
+
try {
|
|
257
|
+
const response = await apiClient.post(initPath, initPayload);
|
|
258
|
+
initResponse = response.data.result;
|
|
259
|
+
this.log(`${uploadId} INIT ${formatStatus("done")}`);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
this.log(`${uploadId} INIT ${formatStatus("fail")}`);
|
|
263
|
+
this.logHttpError("upload init", error);
|
|
264
|
+
this.error(`Snapshot upload initialization failed (${scannerType}).`);
|
|
265
|
+
}
|
|
266
|
+
if (!initResponse.uploadUrl) {
|
|
267
|
+
this.error(`Snapshot upload init response does not contain uploadUrl (${scannerType}).`);
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await axios.put(initResponse.uploadUrl, local.archive, {
|
|
271
|
+
headers: {
|
|
272
|
+
"Content-Length": local.archive.length,
|
|
273
|
+
"Content-Type": "application/zip",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
this.log(`${uploadId} PUT ${formatStatus("done")}`);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.log(`${uploadId} PUT ${formatStatus("fail")}`);
|
|
280
|
+
this.logHttpError("signed upload PUT", error);
|
|
281
|
+
this.error(`Snapshot archive upload failed (${scannerType}).`);
|
|
282
|
+
}
|
|
283
|
+
const finalizePath = `/code-snapshots/${initResponse.snapshotId}/finalize`;
|
|
284
|
+
try {
|
|
285
|
+
await apiClient.post(finalizePath, {});
|
|
286
|
+
this.log(`${uploadId} FINAL ${formatStatus("done")} remoteSnapshotId=${initResponse.snapshotId}`);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
this.log(`${uploadId} FINAL ${formatStatus("fail")}`);
|
|
290
|
+
this.logHttpError("finalize", error);
|
|
291
|
+
this.error(`Snapshot finalize failed (${scannerType}).`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async startProcessingRun(input) {
|
|
295
|
+
const { apiClient, designSystemId } = input;
|
|
296
|
+
const processingPath = `/code-snapshots/process-run`;
|
|
297
|
+
this.logPhase("Processing phase");
|
|
298
|
+
this.log(`${formatStatus("start")} processing run`);
|
|
299
|
+
try {
|
|
300
|
+
const response = await apiClient.post(processingPath, {
|
|
301
|
+
designSystemId,
|
|
302
|
+
});
|
|
303
|
+
this.log(`${formatStatus("done")} processing run id=${response.data.result.processingRunId} snapshots=${response.data.result.snapshotIds.length}`);
|
|
304
|
+
return response.data.result;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.log(`${formatStatus("fail")} processing run`);
|
|
308
|
+
this.logHttpError("process-run", error);
|
|
309
|
+
this.error("Failed to start batch processing run.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async createArchiveFromSnapshotRoot(snapshotRoot) {
|
|
313
|
+
const zip = new AdmZip();
|
|
314
|
+
zip.addLocalFolder(snapshotRoot);
|
|
315
|
+
const archive = zip.toBuffer();
|
|
316
|
+
const checksum = crypto.createHash("sha256").update(archive).digest("hex");
|
|
317
|
+
return {
|
|
318
|
+
archive,
|
|
319
|
+
archiveName: `${path.basename(snapshotRoot)}.zip`,
|
|
320
|
+
checksum,
|
|
321
|
+
sizeBytes: archive.byteLength,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
logHttpError(step, error) {
|
|
325
|
+
if (isAxiosError(error)) {
|
|
326
|
+
const status = error.response?.status;
|
|
327
|
+
const data = typeof error.response?.data === "string" ? error.response.data : JSON.stringify(error.response?.data, null, 2);
|
|
328
|
+
this.log(`${step}, http error${status ? ` ${status}` : ""}: ${data ?? error.message}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.log(`${step}, error: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function toApiScannerType(scannerType) {
|
|
335
|
+
return scannerType === "components" ? "Components" : "Usage";
|
|
336
|
+
}
|
|
337
|
+
function readPackageName(rootDir) {
|
|
338
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
339
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
340
|
+
return path.basename(rootDir);
|
|
341
|
+
}
|
|
342
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
343
|
+
return packageJson.name ?? path.basename(rootDir);
|
|
344
|
+
}
|
|
345
|
+
function resolveExecutionTargets(input) {
|
|
346
|
+
const result = [];
|
|
347
|
+
if (input.scannerType === "components" || input.scannerType === "all") {
|
|
348
|
+
result.push(...input.componentPackages.map(importFrom => ({
|
|
349
|
+
discovery: false,
|
|
350
|
+
importFrom,
|
|
351
|
+
rootDir: input.rootDir,
|
|
352
|
+
scanType: "components",
|
|
353
|
+
})));
|
|
354
|
+
}
|
|
355
|
+
if (input.scannerType === "usage" || input.scannerType === "all") {
|
|
356
|
+
result.push(...resolveUsageExecutionTargets({
|
|
357
|
+
componentPackages: input.componentPackages,
|
|
358
|
+
excludedPackages: input.excludedPackages,
|
|
359
|
+
rootDir: input.rootDir,
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
function resolveUsageExecutionTargets(input) {
|
|
365
|
+
const { componentPackages, excludedPackages, rootDir } = input;
|
|
366
|
+
const candidatePackageDirs = resolveCandidatePackageDirs(rootDir);
|
|
367
|
+
const excludedNames = new Set(excludedPackages);
|
|
368
|
+
const componentPackageNames = resolveComponentPackageNames(rootDir, candidatePackageDirs, componentPackages);
|
|
369
|
+
if (candidatePackageDirs.length <= 1) {
|
|
370
|
+
const rootPackageName = readPackageName(rootDir);
|
|
371
|
+
if (excludedNames.has(rootPackageName)) {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
return [{ discovery: false, importFrom: componentPackages, rootDir, scanType: "usage" }];
|
|
375
|
+
}
|
|
376
|
+
return candidatePackageDirs
|
|
377
|
+
.filter(packageDir => {
|
|
378
|
+
const packageName = readPackageName(packageDir);
|
|
379
|
+
return !excludedNames.has(packageName) && !componentPackageNames.has(packageName);
|
|
380
|
+
})
|
|
381
|
+
.map(packageDir => ({
|
|
382
|
+
discovery: true,
|
|
383
|
+
importFrom: componentPackages,
|
|
384
|
+
rootDir: packageDir,
|
|
385
|
+
scanType: "usage",
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
function resolveCandidatePackageDirs(rootDir) {
|
|
389
|
+
const workspacePackageDirs = resolveWorkspacePackageDirs(rootDir);
|
|
390
|
+
if (workspacePackageDirs.length > 0) {
|
|
391
|
+
return workspacePackageDirs;
|
|
392
|
+
}
|
|
393
|
+
return resolveDirectChildPackageDirs(rootDir);
|
|
394
|
+
}
|
|
395
|
+
function resolveWorkspacePackageDirs(rootDir) {
|
|
396
|
+
const rootPackageJsonPath = path.join(rootDir, "package.json");
|
|
397
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
|
|
401
|
+
const workspacePatterns = Array.isArray(rootPackageJson.workspaces)
|
|
402
|
+
? rootPackageJson.workspaces
|
|
403
|
+
: (rootPackageJson.workspaces?.packages ?? []);
|
|
404
|
+
const packageDirs = [];
|
|
405
|
+
for (const pattern of workspacePatterns) {
|
|
406
|
+
const normalized = pattern.replaceAll("\\", "/");
|
|
407
|
+
if (!normalized.includes("*")) {
|
|
408
|
+
const workspaceDir = path.resolve(rootDir, normalized);
|
|
409
|
+
if (fs.existsSync(path.join(workspaceDir, "package.json"))) {
|
|
410
|
+
packageDirs.push(workspaceDir);
|
|
411
|
+
}
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!normalized.endsWith("/*")) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const baseDir = path.resolve(rootDir, normalized.slice(0, -2));
|
|
418
|
+
if (!fs.existsSync(baseDir) || !fs.statSync(baseDir).isDirectory()) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
422
|
+
if (!entry.isDirectory()) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const candidate = path.join(baseDir, entry.name);
|
|
426
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) {
|
|
427
|
+
packageDirs.push(candidate);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return [...new Set(packageDirs)];
|
|
432
|
+
}
|
|
433
|
+
function resolveDirectChildPackageDirs(rootDir) {
|
|
434
|
+
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
const result = [];
|
|
438
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
439
|
+
if (!entry.isDirectory()) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const packageDir = path.join(rootDir, entry.name);
|
|
443
|
+
if (fs.existsSync(path.join(packageDir, "package.json"))) {
|
|
444
|
+
result.push(packageDir);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
function resolveComponentPackageNames(rootDir, candidatePackageDirs, componentPackages) {
|
|
450
|
+
const byName = new Map(candidatePackageDirs.map(packageDir => [readPackageName(packageDir), packageDir]));
|
|
451
|
+
const names = new Set();
|
|
452
|
+
for (const componentPackage of componentPackages) {
|
|
453
|
+
if (byName.has(componentPackage)) {
|
|
454
|
+
names.add(componentPackage);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const explicitPath = path.resolve(rootDir, componentPackage);
|
|
458
|
+
if (!fs.existsSync(explicitPath) || !fs.statSync(explicitPath).isDirectory()) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
names.add(readPackageName(explicitPath));
|
|
462
|
+
}
|
|
463
|
+
return names;
|
|
464
|
+
}
|
|
465
|
+
function hasUsageInSnapshot(snapshotRoot) {
|
|
466
|
+
return countUsageRecordsInSnapshot(snapshotRoot) > 0;
|
|
467
|
+
}
|
|
468
|
+
function countUsageRecordsInSnapshot(snapshotRoot) {
|
|
469
|
+
const usageFilePath = path.join(snapshotRoot, "raw", "component-usage.json");
|
|
470
|
+
if (!fs.existsSync(usageFilePath)) {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const usageJson = JSON.parse(fs.readFileSync(usageFilePath, "utf8"));
|
|
475
|
+
const usageRecords = usageJson.records ?? {};
|
|
476
|
+
return Object.values(usageRecords).filter(item => (item.count ?? 0) > 0).length;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function shouldWriteSnapshotsToExecutionRoot(projectRoot) {
|
|
483
|
+
return projectRoot.split(path.sep).includes("node_modules");
|
|
484
|
+
}
|
|
485
|
+
function resolveAnalyzeTarget(input) {
|
|
486
|
+
if (input.scannerType === "usage") {
|
|
487
|
+
return { importFrom: input.importFrom, rootDir: input.rootDir };
|
|
488
|
+
}
|
|
489
|
+
const importFrom = Array.isArray(input.importFrom) ? input.importFrom[0] : input.importFrom;
|
|
490
|
+
if (!importFrom) {
|
|
491
|
+
return { importFrom: ".", rootDir: input.rootDir };
|
|
492
|
+
}
|
|
493
|
+
if (readPackageName(input.rootDir) === importFrom) {
|
|
494
|
+
if (hasSourceEntry(input.rootDir)) {
|
|
495
|
+
return { importFrom: "src", rootDir: input.rootDir };
|
|
496
|
+
}
|
|
497
|
+
return { importFrom: ".", rootDir: input.rootDir };
|
|
498
|
+
}
|
|
499
|
+
const workspacePackageDir = findPackageDirByName(resolveCandidatePackageDirs(input.rootDir), importFrom);
|
|
500
|
+
if (workspacePackageDir) {
|
|
501
|
+
if (hasSourceEntry(workspacePackageDir)) {
|
|
502
|
+
return { importFrom: "src", rootDir: workspacePackageDir };
|
|
503
|
+
}
|
|
504
|
+
return { importFrom: ".", rootDir: workspacePackageDir };
|
|
505
|
+
}
|
|
506
|
+
const explicitPath = path.resolve(input.rootDir, importFrom);
|
|
507
|
+
if (fs.existsSync(explicitPath) && fs.statSync(explicitPath).isDirectory()) {
|
|
508
|
+
if (hasSourceEntry(explicitPath)) {
|
|
509
|
+
return { importFrom: "src", rootDir: explicitPath };
|
|
510
|
+
}
|
|
511
|
+
return { importFrom: ".", rootDir: explicitPath };
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
function normalizeStringList(value) {
|
|
516
|
+
if (!value) {
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
const list = Array.isArray(value) ? value : [value];
|
|
520
|
+
return [...new Set(list.map(item => item.trim()).filter(Boolean))];
|
|
521
|
+
}
|
|
522
|
+
function stringifyImportFrom(importFrom) {
|
|
523
|
+
return Array.isArray(importFrom) ? importFrom.join(",") : importFrom;
|
|
524
|
+
}
|
|
525
|
+
function findPackageDirByName(packageDirs, packageName) {
|
|
526
|
+
for (const packageDir of packageDirs) {
|
|
527
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
528
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
533
|
+
if (packageJson.name === packageName) {
|
|
534
|
+
return packageDir;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function hasSourceEntry(packageDir) {
|
|
544
|
+
return (fs.existsSync(path.join(packageDir, "src", "index.ts")) || fs.existsSync(path.join(packageDir, "src", "index.tsx")));
|
|
545
|
+
}
|
|
546
|
+
function createMissingComponentsSourceMessage(importFrom) {
|
|
547
|
+
return `Package ${stringifyImportFrom(importFrom)} could not be resolved from local source. Component analysis must run against source code, so the components scan was skipped. Run the command from the package source instead.`;
|
|
548
|
+
}
|
|
549
|
+
function createPlannedExecutionTarget(input) {
|
|
550
|
+
const { executionTarget, index, rootDir, total } = input;
|
|
551
|
+
const analyzeTarget = resolveAnalyzeTarget({
|
|
552
|
+
importFrom: executionTarget.importFrom,
|
|
553
|
+
rootDir: executionTarget.rootDir,
|
|
554
|
+
scannerType: executionTarget.scanType,
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
analyzeTarget,
|
|
558
|
+
executionTarget,
|
|
559
|
+
packageLabel: stringifyImportFrom(executionTarget.importFrom),
|
|
560
|
+
pathLabel: formatPathLabel(rootDir, analyzeTarget?.rootDir ?? executionTarget.rootDir),
|
|
561
|
+
targetId: formatSequenceId("", index + 1, total),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function summarizeScanTypes(executionTargets) {
|
|
565
|
+
const components = executionTargets.filter(target => target.scanType === "components").length;
|
|
566
|
+
const usage = executionTargets.filter(target => target.scanType === "usage").length;
|
|
567
|
+
return `${components} components, ${usage} usage`;
|
|
568
|
+
}
|
|
569
|
+
function padScanType(scanType) {
|
|
570
|
+
return scanType.padEnd("components".length, " ");
|
|
571
|
+
}
|
|
572
|
+
function formatSequenceId(prefix, index, total) {
|
|
573
|
+
const width = String(total).length;
|
|
574
|
+
const left = String(index).padStart(width, "0");
|
|
575
|
+
const right = String(total).padStart(width, "0");
|
|
576
|
+
return `[${prefix}${left}/${right}]`;
|
|
577
|
+
}
|
|
578
|
+
function formatPathLabel(rootDir, targetPath) {
|
|
579
|
+
const relativePath = path.relative(rootDir, targetPath) || ".";
|
|
580
|
+
if (relativePath.length <= 48) {
|
|
581
|
+
return relativePath;
|
|
582
|
+
}
|
|
583
|
+
const segments = relativePath.split(path.sep);
|
|
584
|
+
return segments.length > 2 ? `.../${segments.slice(-2).join("/")}` : relativePath;
|
|
585
|
+
}
|
|
586
|
+
function formatStatus(status) {
|
|
587
|
+
const labels = {
|
|
588
|
+
done: "DONE ",
|
|
589
|
+
fail: "FAIL ",
|
|
590
|
+
skip: "SKIP ",
|
|
591
|
+
start: "START",
|
|
592
|
+
};
|
|
593
|
+
const colorEnabled = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
594
|
+
if (!colorEnabled) {
|
|
595
|
+
return labels[status];
|
|
596
|
+
}
|
|
597
|
+
const colors = {
|
|
598
|
+
done: "\u001B[32m",
|
|
599
|
+
fail: "\u001B[31m",
|
|
600
|
+
skip: "\u001B[33m",
|
|
601
|
+
start: "\u001B[36m",
|
|
602
|
+
};
|
|
603
|
+
return `${colors[status]}${labels[status]}\u001B[0m`;
|
|
604
|
+
}
|
|
605
|
+
//# sourceMappingURL=analyze-command.js.map
|
|
606
|
+
//# debugId=48ae5534-bfe7-5b1e-a760-916b78e71f6d
|