@theia/cli 1.53.0-next.4 → 1.53.0-next.55

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.
@@ -1,328 +1,328 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2022 STMicroelectronics and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import * as fs from 'fs';
18
- import * as path from 'path';
19
- import { glob } from 'glob';
20
- import { create as logUpdater } from 'log-update';
21
- import * as chalk from 'chalk';
22
-
23
- const NODE_MODULES = 'node_modules';
24
- const PACKAGE_JSON = 'package.json';
25
-
26
- const logUpdate = logUpdater(process.stdout);
27
-
28
- interface CheckDependenciesOptions {
29
- workspaces: string[] | undefined,
30
- include: string[],
31
- exclude: string[],
32
- skipHoisted: boolean,
33
- skipUniqueness: boolean,
34
- skipSingleTheiaVersion: boolean,
35
- onlyTheiaExtensions: boolean,
36
- suppress: boolean
37
- }
38
-
39
- /** NPM package */
40
- interface Package {
41
- /** Name of the package, e.g. `@theia/core`. */
42
- name: string,
43
- /** Actual resolved version of the package, e.g. `1.27.0`. */
44
- version: string,
45
- /** Path of the package relative to the workspace, e.g. `node_modules/@theia/core`. */
46
- path: string,
47
- /** Whether the package is hoisted or not, i.e., whether it is contained in the root `node_modules`. */
48
- hoisted: boolean,
49
- /** Workspace location in which the package was found. */
50
- dependent: string | undefined,
51
- /** Whether the package is a Theia extension or not */
52
- isTheiaExtension?: boolean,
53
- }
54
-
55
- /** Issue found with a specific package. */
56
- interface DependencyIssue {
57
- /** Type of the issue. */
58
- issueType: 'not-hoisted' | 'multiple-versions' | 'theia-version-mix',
59
- /** Package with issue. */
60
- package: Package,
61
- /** Packages related to this issue. */
62
- relatedPackages: Package[],
63
- /** Severity */
64
- severity: 'warning' | 'error'
65
- }
66
-
67
- export default function checkDependencies(options: CheckDependenciesOptions): void {
68
- const workspaces = deriveWorkspaces(options);
69
- logUpdate(`✅ Found ${workspaces.length} workspaces`);
70
-
71
- console.log('🔍 Collecting dependencies...');
72
- const dependencies = findAllDependencies(workspaces, options);
73
- logUpdate(`✅ Found ${dependencies.length} dependencies`);
74
-
75
- console.log('🔍 Analyzing dependencies...');
76
- const issues = analyzeDependencies(dependencies, options);
77
- if (issues.length <= 0) {
78
- logUpdate('✅ No issues were found');
79
- process.exit(0);
80
- }
81
-
82
- logUpdate('🟠 Found ' + issues.length + ' issues');
83
- printIssues(issues);
84
- printHints(issues);
85
- process.exit(options.suppress ? 0 : 1);
86
- }
87
-
88
- function deriveWorkspaces(options: CheckDependenciesOptions): string[] {
89
- const wsGlobs = options.workspaces ?? readWorkspaceGlobsFromPackageJson();
90
- const workspaces: string[] = [];
91
- for (const wsGlob of wsGlobs) {
92
- workspaces.push(...glob.sync(wsGlob + '/'));
93
- }
94
- return workspaces;
95
- }
96
-
97
- function readWorkspaceGlobsFromPackageJson(): string[] {
98
- const rootPackageJson = path.join(process.cwd(), PACKAGE_JSON);
99
- if (!fs.existsSync(rootPackageJson)) {
100
- console.error('Directory does not contain a package.json with defined workspaces');
101
- console.info('Run in the root of a Theia project or specify them via --workspaces');
102
- process.exit(1);
103
- }
104
- return require(rootPackageJson).workspaces ?? [];
105
- }
106
-
107
- function findAllDependencies(workspaces: string[], options: CheckDependenciesOptions): Package[] {
108
- const dependencies: Package[] = [];
109
- dependencies.push(...findDependencies('.', options));
110
- for (const workspace of workspaces) {
111
- dependencies.push(...findDependencies(workspace, options));
112
- }
113
- return dependencies;
114
- }
115
-
116
- function findDependencies(workspace: string, options: CheckDependenciesOptions): Package[] {
117
- const dependent = getPackageName(path.join(process.cwd(), workspace, PACKAGE_JSON));
118
- const nodeModulesDir = path.join(workspace, NODE_MODULES);
119
- const matchingPackageJsons: Package[] = [];
120
- options.include.forEach(include =>
121
- glob.sync(`${include}/${PACKAGE_JSON}`, {
122
- cwd: nodeModulesDir,
123
- ignore: [
124
- `**/${NODE_MODULES}/**`, // node_modules folders within dependencies
125
- `[^@]*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and not in an @org)
126
- `@*/*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and in an @org)
127
- ...options.exclude] // user-specified exclude patterns
128
- }).forEach(packageJsonPath => {
129
- const dependency = toDependency(packageJsonPath, nodeModulesDir, dependent);
130
- if (!options.onlyTheiaExtensions || dependency.isTheiaExtension) {
131
- matchingPackageJsons.push(dependency);
132
- }
133
- const childNodeModules: string = path.join(nodeModulesDir, packageJsonPath, '..');
134
- matchingPackageJsons.push(...findDependencies(childNodeModules, options));
135
- })
136
- );
137
- return matchingPackageJsons;
138
- }
139
-
140
- function toDependency(packageJsonPath: string, nodeModulesDir: string, dependent?: string): Package {
141
- const fullPackageJsonPath = path.join(process.cwd(), nodeModulesDir, packageJsonPath);
142
- const name = getPackageName(fullPackageJsonPath);
143
- const version = getPackageVersion(fullPackageJsonPath);
144
- return {
145
- name: name ?? packageJsonPath.replace('/' + PACKAGE_JSON, ''),
146
- version: version ?? 'unknown',
147
- path: path.relative(process.cwd(), fullPackageJsonPath),
148
- hoisted: nodeModulesDir === NODE_MODULES,
149
- dependent: dependent,
150
- isTheiaExtension: isTheiaExtension(fullPackageJsonPath)
151
- };
152
- }
153
-
154
- function getPackageVersion(fullPackageJsonPath: string): string | undefined {
155
- try {
156
- return require(fullPackageJsonPath).version;
157
- } catch (error) {
158
- return undefined;
159
- }
160
- }
161
-
162
- function getPackageName(fullPackageJsonPath: string): string | undefined {
163
- try {
164
- return require(fullPackageJsonPath).name;
165
- } catch (error) {
166
- return undefined;
167
- }
168
- }
169
-
170
- function isTheiaExtension(fullPackageJsonPath: string): boolean {
171
- try {
172
- const theiaExtension = require(fullPackageJsonPath).theiaExtensions;
173
- return theiaExtension ? true : false;
174
- } catch (error) {
175
- return false;
176
- }
177
- }
178
-
179
- function analyzeDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
180
- const issues: DependencyIssue[] = [];
181
- if (!options.skipHoisted) {
182
- issues.push(...findNotHoistedDependencies(packages, options));
183
- }
184
- if (!options.skipUniqueness) {
185
- issues.push(...findDuplicateDependencies(packages, options));
186
- }
187
- if (!options.skipSingleTheiaVersion) {
188
- issues.push(...findTheiaVersionMix(packages, options));
189
- }
190
- return issues;
191
- }
192
-
193
- function findNotHoistedDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
194
- const issues: DependencyIssue[] = [];
195
- const nonHoistedPackages = packages.filter(p => p.hoisted === false);
196
- for (const nonHoistedPackage of nonHoistedPackages) {
197
- issues.push(createNonHoistedPackageIssue(nonHoistedPackage, options));
198
- }
199
- return issues;
200
- }
201
-
202
- function createNonHoistedPackageIssue(nonHoistedPackage: Package, options: CheckDependenciesOptions): DependencyIssue {
203
- return {
204
- issueType: 'not-hoisted',
205
- package: nonHoistedPackage,
206
- relatedPackages: [getHoistedPackageByName(nonHoistedPackage.name)],
207
- severity: options.suppress ? 'warning' : 'error'
208
- };
209
- }
210
-
211
- function getHoistedPackageByName(name: string): Package {
212
- return toDependency(path.join(name, PACKAGE_JSON), NODE_MODULES);
213
- }
214
-
215
- function findDuplicateDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
216
- const duplicates: string[] = [];
217
- const packagesGroupedByName = new Map<string, Package[]>();
218
- for (const currentPackage of packages) {
219
- const name = currentPackage.name;
220
- if (!packagesGroupedByName.has(name)) {
221
- packagesGroupedByName.set(name, []);
222
- }
223
- const currentPackages = packagesGroupedByName.get(name)!;
224
- currentPackages.push(currentPackage);
225
- if (currentPackages.length > 1 && duplicates.indexOf(name) === -1) {
226
- duplicates.push(name);
227
- }
228
- }
229
-
230
- duplicates.sort();
231
- const issues: DependencyIssue[] = [];
232
- for (const duplicate of duplicates) {
233
- const duplicatePackages = packagesGroupedByName.get(duplicate);
234
- if (duplicatePackages && duplicatePackages.length > 0) {
235
- issues.push({
236
- issueType: 'multiple-versions',
237
- package: duplicatePackages.pop()!,
238
- relatedPackages: duplicatePackages,
239
- severity: options.suppress ? 'warning' : 'error'
240
- });
241
- }
242
- }
243
-
244
- return issues;
245
- }
246
-
247
- function findTheiaVersionMix(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
248
- // @theia/monaco-editor-core is following the versions of Monaco so it can't be part of this check
249
- const theiaPackages = packages.filter(p => p.name.startsWith('@theia/') && !p.name.startsWith('@theia/monaco-editor-core'));
250
- let theiaVersion = undefined;
251
- let referenceTheiaPackage = undefined;
252
- const packagesWithOtherVersion: Package[] = [];
253
- for (const theiaPackage of theiaPackages) {
254
- if (!theiaVersion && theiaPackage.version) {
255
- theiaVersion = theiaPackage.version;
256
- referenceTheiaPackage = theiaPackage;
257
- } else if (theiaVersion !== theiaPackage.version) {
258
- packagesWithOtherVersion.push(theiaPackage);
259
- }
260
- }
261
-
262
- if (referenceTheiaPackage && packagesWithOtherVersion.length > 0) {
263
- return [{
264
- issueType: 'theia-version-mix',
265
- package: referenceTheiaPackage,
266
- relatedPackages: packagesWithOtherVersion,
267
- severity: 'error'
268
- }];
269
- }
270
- return [];
271
- }
272
-
273
- function printIssues(issues: DependencyIssue[]): void {
274
- console.log();
275
- const indent = issues.length.toString().length;
276
- issues.forEach((issue, index) => {
277
- printIssue(issue, index + 1, indent);
278
- });
279
- }
280
-
281
- function printIssue(issue: DependencyIssue, issueNumber: number, indent: number): void {
282
- const remainingIndent = indent - issueNumber.toString().length;
283
- const indentString = ' '.repeat(remainingIndent + 1);
284
- console.log(issueTitle(issue, issueNumber, indentString));
285
- console.log(issueDetails(issue, ' ' + ' '.repeat(indent)));
286
- console.log();
287
- }
288
-
289
- function issueTitle(issue: DependencyIssue, issueNumber: number, indent: string): string {
290
- const dependent = issue.package.dependent ? ` in ${chalk.blueBright(issue.package.dependent ?? 'unknown')}` : '';
291
- return chalk.bgWhiteBright.bold.black(`#${issueNumber}${indent}`) + ' ' + chalk.cyanBright(issue.package.name)
292
- + dependent + chalk.dim(` [${issue.issueType}]`);
293
- }
294
-
295
- function issueDetails(issue: DependencyIssue, indent: string): string {
296
- return indent + severity(issue) + ' ' + issueMessage(issue) + '\n'
297
- + indent + versionLine(issue.package) + '\n'
298
- + issue.relatedPackages.map(p => indent + versionLine(p)).join('\n');
299
- }
300
-
301
- function issueMessage(issue: DependencyIssue): string {
302
- if (issue.issueType === 'multiple-versions') {
303
- return `Multiple versions of dependency ${chalk.bold(issue.package.name)} found.`;
304
- } else if (issue.issueType === 'theia-version-mix') {
305
- return `Mix of ${chalk.bold('@theia/*')} versions found.`;
306
- } else {
307
- return `Dependency ${chalk.bold(issue.package.name)} is not hoisted.`;
308
- }
309
- }
310
-
311
- function severity(issue: DependencyIssue): string {
312
- return issue.severity === 'error' ? chalk.red('error') : chalk.yellow('warning');
313
- }
314
-
315
- function versionLine(pckg: Package): string {
316
- return chalk.bold(pckg.version) + ' in ' + pckg.path;
317
- }
318
-
319
- function printHints(issues: DependencyIssue[]): void {
320
- console.log();
321
- if (issues.find(i => i.issueType === 'theia-version-mix')) {
322
- console.log('⛔ A mix of Theia versions is very likely leading to a broken application.');
323
- }
324
- console.log(`â„šī¸ Use ${chalk.bold('yarn why <package-name>')} to find out why those multiple versions of a package are pulled.`);
325
- console.log('â„šī¸ Try to resolve those issues by finding package versions along the dependency chain that depend on compatible versions.');
326
- console.log(`â„šī¸ Use ${chalk.bold('resolutions')} in your root package.json to force specific versions as a last resort.`);
327
- console.log();
328
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { glob } from 'glob';
20
+ import { create as logUpdater } from 'log-update';
21
+ import * as chalk from 'chalk';
22
+
23
+ const NODE_MODULES = 'node_modules';
24
+ const PACKAGE_JSON = 'package.json';
25
+
26
+ const logUpdate = logUpdater(process.stdout);
27
+
28
+ interface CheckDependenciesOptions {
29
+ workspaces: string[] | undefined,
30
+ include: string[],
31
+ exclude: string[],
32
+ skipHoisted: boolean,
33
+ skipUniqueness: boolean,
34
+ skipSingleTheiaVersion: boolean,
35
+ onlyTheiaExtensions: boolean,
36
+ suppress: boolean
37
+ }
38
+
39
+ /** NPM package */
40
+ interface Package {
41
+ /** Name of the package, e.g. `@theia/core`. */
42
+ name: string,
43
+ /** Actual resolved version of the package, e.g. `1.27.0`. */
44
+ version: string,
45
+ /** Path of the package relative to the workspace, e.g. `node_modules/@theia/core`. */
46
+ path: string,
47
+ /** Whether the package is hoisted or not, i.e., whether it is contained in the root `node_modules`. */
48
+ hoisted: boolean,
49
+ /** Workspace location in which the package was found. */
50
+ dependent: string | undefined,
51
+ /** Whether the package is a Theia extension or not */
52
+ isTheiaExtension?: boolean,
53
+ }
54
+
55
+ /** Issue found with a specific package. */
56
+ interface DependencyIssue {
57
+ /** Type of the issue. */
58
+ issueType: 'not-hoisted' | 'multiple-versions' | 'theia-version-mix',
59
+ /** Package with issue. */
60
+ package: Package,
61
+ /** Packages related to this issue. */
62
+ relatedPackages: Package[],
63
+ /** Severity */
64
+ severity: 'warning' | 'error'
65
+ }
66
+
67
+ export default function checkDependencies(options: CheckDependenciesOptions): void {
68
+ const workspaces = deriveWorkspaces(options);
69
+ logUpdate(`✅ Found ${workspaces.length} workspaces`);
70
+
71
+ console.log('🔍 Collecting dependencies...');
72
+ const dependencies = findAllDependencies(workspaces, options);
73
+ logUpdate(`✅ Found ${dependencies.length} dependencies`);
74
+
75
+ console.log('🔍 Analyzing dependencies...');
76
+ const issues = analyzeDependencies(dependencies, options);
77
+ if (issues.length <= 0) {
78
+ logUpdate('✅ No issues were found');
79
+ process.exit(0);
80
+ }
81
+
82
+ logUpdate('🟠 Found ' + issues.length + ' issues');
83
+ printIssues(issues);
84
+ printHints(issues);
85
+ process.exit(options.suppress ? 0 : 1);
86
+ }
87
+
88
+ function deriveWorkspaces(options: CheckDependenciesOptions): string[] {
89
+ const wsGlobs = options.workspaces ?? readWorkspaceGlobsFromPackageJson();
90
+ const workspaces: string[] = [];
91
+ for (const wsGlob of wsGlobs) {
92
+ workspaces.push(...glob.sync(wsGlob + '/'));
93
+ }
94
+ return workspaces;
95
+ }
96
+
97
+ function readWorkspaceGlobsFromPackageJson(): string[] {
98
+ const rootPackageJson = path.join(process.cwd(), PACKAGE_JSON);
99
+ if (!fs.existsSync(rootPackageJson)) {
100
+ console.error('Directory does not contain a package.json with defined workspaces');
101
+ console.info('Run in the root of a Theia project or specify them via --workspaces');
102
+ process.exit(1);
103
+ }
104
+ return require(rootPackageJson).workspaces ?? [];
105
+ }
106
+
107
+ function findAllDependencies(workspaces: string[], options: CheckDependenciesOptions): Package[] {
108
+ const dependencies: Package[] = [];
109
+ dependencies.push(...findDependencies('.', options));
110
+ for (const workspace of workspaces) {
111
+ dependencies.push(...findDependencies(workspace, options));
112
+ }
113
+ return dependencies;
114
+ }
115
+
116
+ function findDependencies(workspace: string, options: CheckDependenciesOptions): Package[] {
117
+ const dependent = getPackageName(path.join(process.cwd(), workspace, PACKAGE_JSON));
118
+ const nodeModulesDir = path.join(workspace, NODE_MODULES);
119
+ const matchingPackageJsons: Package[] = [];
120
+ options.include.forEach(include =>
121
+ glob.sync(`${include}/${PACKAGE_JSON}`, {
122
+ cwd: nodeModulesDir,
123
+ ignore: [
124
+ `**/${NODE_MODULES}/**`, // node_modules folders within dependencies
125
+ `[^@]*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and not in an @org)
126
+ `@*/*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and in an @org)
127
+ ...options.exclude] // user-specified exclude patterns
128
+ }).forEach(packageJsonPath => {
129
+ const dependency = toDependency(packageJsonPath, nodeModulesDir, dependent);
130
+ if (!options.onlyTheiaExtensions || dependency.isTheiaExtension) {
131
+ matchingPackageJsons.push(dependency);
132
+ }
133
+ const childNodeModules: string = path.join(nodeModulesDir, packageJsonPath, '..');
134
+ matchingPackageJsons.push(...findDependencies(childNodeModules, options));
135
+ })
136
+ );
137
+ return matchingPackageJsons;
138
+ }
139
+
140
+ function toDependency(packageJsonPath: string, nodeModulesDir: string, dependent?: string): Package {
141
+ const fullPackageJsonPath = path.join(process.cwd(), nodeModulesDir, packageJsonPath);
142
+ const name = getPackageName(fullPackageJsonPath);
143
+ const version = getPackageVersion(fullPackageJsonPath);
144
+ return {
145
+ name: name ?? packageJsonPath.replace('/' + PACKAGE_JSON, ''),
146
+ version: version ?? 'unknown',
147
+ path: path.relative(process.cwd(), fullPackageJsonPath),
148
+ hoisted: nodeModulesDir === NODE_MODULES,
149
+ dependent: dependent,
150
+ isTheiaExtension: isTheiaExtension(fullPackageJsonPath)
151
+ };
152
+ }
153
+
154
+ function getPackageVersion(fullPackageJsonPath: string): string | undefined {
155
+ try {
156
+ return require(fullPackageJsonPath).version;
157
+ } catch (error) {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ function getPackageName(fullPackageJsonPath: string): string | undefined {
163
+ try {
164
+ return require(fullPackageJsonPath).name;
165
+ } catch (error) {
166
+ return undefined;
167
+ }
168
+ }
169
+
170
+ function isTheiaExtension(fullPackageJsonPath: string): boolean {
171
+ try {
172
+ const theiaExtension = require(fullPackageJsonPath).theiaExtensions;
173
+ return theiaExtension ? true : false;
174
+ } catch (error) {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ function analyzeDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
180
+ const issues: DependencyIssue[] = [];
181
+ if (!options.skipHoisted) {
182
+ issues.push(...findNotHoistedDependencies(packages, options));
183
+ }
184
+ if (!options.skipUniqueness) {
185
+ issues.push(...findDuplicateDependencies(packages, options));
186
+ }
187
+ if (!options.skipSingleTheiaVersion) {
188
+ issues.push(...findTheiaVersionMix(packages, options));
189
+ }
190
+ return issues;
191
+ }
192
+
193
+ function findNotHoistedDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
194
+ const issues: DependencyIssue[] = [];
195
+ const nonHoistedPackages = packages.filter(p => p.hoisted === false);
196
+ for (const nonHoistedPackage of nonHoistedPackages) {
197
+ issues.push(createNonHoistedPackageIssue(nonHoistedPackage, options));
198
+ }
199
+ return issues;
200
+ }
201
+
202
+ function createNonHoistedPackageIssue(nonHoistedPackage: Package, options: CheckDependenciesOptions): DependencyIssue {
203
+ return {
204
+ issueType: 'not-hoisted',
205
+ package: nonHoistedPackage,
206
+ relatedPackages: [getHoistedPackageByName(nonHoistedPackage.name)],
207
+ severity: options.suppress ? 'warning' : 'error'
208
+ };
209
+ }
210
+
211
+ function getHoistedPackageByName(name: string): Package {
212
+ return toDependency(path.join(name, PACKAGE_JSON), NODE_MODULES);
213
+ }
214
+
215
+ function findDuplicateDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
216
+ const duplicates: string[] = [];
217
+ const packagesGroupedByName = new Map<string, Package[]>();
218
+ for (const currentPackage of packages) {
219
+ const name = currentPackage.name;
220
+ if (!packagesGroupedByName.has(name)) {
221
+ packagesGroupedByName.set(name, []);
222
+ }
223
+ const currentPackages = packagesGroupedByName.get(name)!;
224
+ currentPackages.push(currentPackage);
225
+ if (currentPackages.length > 1 && duplicates.indexOf(name) === -1) {
226
+ duplicates.push(name);
227
+ }
228
+ }
229
+
230
+ duplicates.sort();
231
+ const issues: DependencyIssue[] = [];
232
+ for (const duplicate of duplicates) {
233
+ const duplicatePackages = packagesGroupedByName.get(duplicate);
234
+ if (duplicatePackages && duplicatePackages.length > 0) {
235
+ issues.push({
236
+ issueType: 'multiple-versions',
237
+ package: duplicatePackages.pop()!,
238
+ relatedPackages: duplicatePackages,
239
+ severity: options.suppress ? 'warning' : 'error'
240
+ });
241
+ }
242
+ }
243
+
244
+ return issues;
245
+ }
246
+
247
+ function findTheiaVersionMix(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
248
+ // @theia/monaco-editor-core is following the versions of Monaco so it can't be part of this check
249
+ const theiaPackages = packages.filter(p => p.name.startsWith('@theia/') && !p.name.startsWith('@theia/monaco-editor-core'));
250
+ let theiaVersion = undefined;
251
+ let referenceTheiaPackage = undefined;
252
+ const packagesWithOtherVersion: Package[] = [];
253
+ for (const theiaPackage of theiaPackages) {
254
+ if (!theiaVersion && theiaPackage.version) {
255
+ theiaVersion = theiaPackage.version;
256
+ referenceTheiaPackage = theiaPackage;
257
+ } else if (theiaVersion !== theiaPackage.version) {
258
+ packagesWithOtherVersion.push(theiaPackage);
259
+ }
260
+ }
261
+
262
+ if (referenceTheiaPackage && packagesWithOtherVersion.length > 0) {
263
+ return [{
264
+ issueType: 'theia-version-mix',
265
+ package: referenceTheiaPackage,
266
+ relatedPackages: packagesWithOtherVersion,
267
+ severity: 'error'
268
+ }];
269
+ }
270
+ return [];
271
+ }
272
+
273
+ function printIssues(issues: DependencyIssue[]): void {
274
+ console.log();
275
+ const indent = issues.length.toString().length;
276
+ issues.forEach((issue, index) => {
277
+ printIssue(issue, index + 1, indent);
278
+ });
279
+ }
280
+
281
+ function printIssue(issue: DependencyIssue, issueNumber: number, indent: number): void {
282
+ const remainingIndent = indent - issueNumber.toString().length;
283
+ const indentString = ' '.repeat(remainingIndent + 1);
284
+ console.log(issueTitle(issue, issueNumber, indentString));
285
+ console.log(issueDetails(issue, ' ' + ' '.repeat(indent)));
286
+ console.log();
287
+ }
288
+
289
+ function issueTitle(issue: DependencyIssue, issueNumber: number, indent: string): string {
290
+ const dependent = issue.package.dependent ? ` in ${chalk.blueBright(issue.package.dependent ?? 'unknown')}` : '';
291
+ return chalk.bgWhiteBright.bold.black(`#${issueNumber}${indent}`) + ' ' + chalk.cyanBright(issue.package.name)
292
+ + dependent + chalk.dim(` [${issue.issueType}]`);
293
+ }
294
+
295
+ function issueDetails(issue: DependencyIssue, indent: string): string {
296
+ return indent + severity(issue) + ' ' + issueMessage(issue) + '\n'
297
+ + indent + versionLine(issue.package) + '\n'
298
+ + issue.relatedPackages.map(p => indent + versionLine(p)).join('\n');
299
+ }
300
+
301
+ function issueMessage(issue: DependencyIssue): string {
302
+ if (issue.issueType === 'multiple-versions') {
303
+ return `Multiple versions of dependency ${chalk.bold(issue.package.name)} found.`;
304
+ } else if (issue.issueType === 'theia-version-mix') {
305
+ return `Mix of ${chalk.bold('@theia/*')} versions found.`;
306
+ } else {
307
+ return `Dependency ${chalk.bold(issue.package.name)} is not hoisted.`;
308
+ }
309
+ }
310
+
311
+ function severity(issue: DependencyIssue): string {
312
+ return issue.severity === 'error' ? chalk.red('error') : chalk.yellow('warning');
313
+ }
314
+
315
+ function versionLine(pckg: Package): string {
316
+ return chalk.bold(pckg.version) + ' in ' + pckg.path;
317
+ }
318
+
319
+ function printHints(issues: DependencyIssue[]): void {
320
+ console.log();
321
+ if (issues.find(i => i.issueType === 'theia-version-mix')) {
322
+ console.log('⛔ A mix of Theia versions is very likely leading to a broken application.');
323
+ }
324
+ console.log(`â„šī¸ Use ${chalk.bold('yarn why <package-name>')} to find out why those multiple versions of a package are pulled.`);
325
+ console.log('â„šī¸ Try to resolve those issues by finding package versions along the dependency chain that depend on compatible versions.');
326
+ console.log(`â„šī¸ Use ${chalk.bold('resolutions')} in your root package.json to force specific versions as a last resort.`);
327
+ console.log();
328
+ }