@theia/cli 1.53.0-next.56 → 1.53.0-next.64

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/src/theia.ts CHANGED
@@ -1,691 +1,691 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2017 TypeFox 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 * as temp from 'temp';
20
- import * as yargs from 'yargs';
21
- import yargsFactory = require('yargs/yargs');
22
- import { ApplicationPackageManager, rebuild } from '@theia/application-manager';
23
- import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package';
24
- import checkDependencies from './check-dependencies';
25
- import downloadPlugins from './download-plugins';
26
- import runTest from './run-test';
27
- import { RateLimiter } from 'limiter';
28
- import { LocalizationManager, extract } from '@theia/localization-manager';
29
- import { NodeRequestService } from '@theia/request/lib/node-request-service';
30
- import { ExtensionIdMatchesFilterFactory, OVSX_RATE_LIMIT, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
31
-
32
- const { executablePath } = require('puppeteer');
33
-
34
- process.on('unhandledRejection', (reason, promise) => {
35
- throw reason;
36
- });
37
- process.on('uncaughtException', error => {
38
- if (error) {
39
- console.error('Uncaught Exception: ', error.toString());
40
- if (error.stack) {
41
- console.error(error.stack);
42
- }
43
- }
44
- process.exit(1);
45
- });
46
- theiaCli();
47
-
48
- function toStringArray(argv: (string | number)[]): string[];
49
- function toStringArray(argv?: (string | number)[]): string[] | undefined;
50
- function toStringArray(argv?: (string | number)[]): string[] | undefined {
51
- return argv?.map(arg => String(arg));
52
- }
53
-
54
- function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule<unknown, {
55
- modules: string[]
56
- cacheRoot?: string
57
- forceAbi?: number,
58
- }> {
59
- return {
60
- command,
61
- describe: `Rebuild/revert native node modules for "${target}"`,
62
- builder: {
63
- 'cacheRoot': {
64
- type: 'string',
65
- describe: 'Root folder where to store the .browser_modules cache'
66
- },
67
- 'modules': {
68
- alias: 'm',
69
- type: 'array', // === `--modules/-m` can be specified multiple times
70
- describe: 'List of modules to rebuild/revert'
71
- },
72
- 'forceAbi': {
73
- type: 'number',
74
- describe: 'The Node ABI version to rebuild for'
75
- }
76
- },
77
- handler: ({ cacheRoot, modules, forceAbi }) => {
78
- // Note: `modules` is actually `string[] | undefined`.
79
- if (modules) {
80
- // It is ergonomic to pass arguments as --modules="a,b,c,..."
81
- // but yargs doesn't parse it this way by default.
82
- const flattened: string[] = [];
83
- for (const value of modules) {
84
- if (value.includes(',')) {
85
- flattened.push(...value.split(',').map(mod => mod.trim()));
86
- } else {
87
- flattened.push(value);
88
- }
89
- }
90
- modules = flattened;
91
- }
92
- rebuild(target, { cacheRoot, modules, forceAbi });
93
- }
94
- };
95
- }
96
-
97
- function defineCommonOptions<T>(cli: yargs.Argv<T>): yargs.Argv<T & {
98
- appTarget?: 'browser' | 'electron' | 'browser-only'
99
- }> {
100
- return cli
101
- .option('app-target', {
102
- description: 'The target application type. Overrides `theia.target` in the application\'s package.json',
103
- choices: ['browser', 'electron', 'browser-only'] as const,
104
- });
105
- }
106
-
107
- async function theiaCli(): Promise<void> {
108
- const { version } = await fs.promises.readFile(path.join(__dirname, '../package.json'), 'utf8').then(JSON.parse);
109
- yargs.scriptName('theia').version(version);
110
- const projectPath = process.cwd();
111
- // Create a sub `yargs` parser to read `app-target` without
112
- // affecting the global `yargs` instance used by the CLI.
113
- const { appTarget } = defineCommonOptions(yargsFactory()).help(false).parse();
114
- const manager = new ApplicationPackageManager({ projectPath, appTarget });
115
- const localizationManager = new LocalizationManager();
116
- const { target } = manager.pck;
117
- defineCommonOptions(yargs)
118
- .command<{
119
- theiaArgs?: (string | number)[]
120
- }>({
121
- command: 'start [theia-args...]',
122
- describe: `Start the ${target} backend`,
123
- // Disable this command's `--help` option so that it is forwarded to Theia's CLI
124
- builder: cli => cli.help(false) as yargs.Argv,
125
- handler: async ({ theiaArgs }) => {
126
- manager.start(toStringArray(theiaArgs));
127
- }
128
- })
129
- .command({
130
- command: 'clean',
131
- describe: `Clean for the ${target} target`,
132
- handler: async () => {
133
- await manager.clean();
134
- }
135
- })
136
- .command({
137
- command: 'copy',
138
- describe: 'Copy various files from `src-gen` to `lib`',
139
- handler: async () => {
140
- await manager.copy();
141
- }
142
- })
143
- .command<{
144
- mode: 'development' | 'production',
145
- splitFrontend?: boolean
146
- }>({
147
- command: 'generate',
148
- describe: `Generate various files for the ${target} target`,
149
- builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli),
150
- handler: async ({ mode, splitFrontend }) => {
151
- await manager.generate({ mode, splitFrontend });
152
- }
153
- })
154
- .command<{
155
- mode: 'development' | 'production',
156
- webpackHelp: boolean
157
- splitFrontend?: boolean
158
- webpackArgs?: (string | number)[]
159
- }>({
160
- command: 'build [webpack-args...]',
161
- describe: `Generate and bundle the ${target} frontend using webpack`,
162
- builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli)
163
- .option('webpack-help' as 'webpackHelp', {
164
- boolean: true,
165
- description: 'Display Webpack\'s help',
166
- default: false
167
- }),
168
- handler: async ({ mode, splitFrontend, webpackHelp, webpackArgs = [] }) => {
169
- await manager.build(
170
- webpackHelp
171
- ? ['--help']
172
- : [
173
- // Forward the `mode` argument to Webpack too:
174
- '--mode', mode,
175
- ...toStringArray(webpackArgs)
176
- ],
177
- { mode, splitFrontend }
178
- );
179
- }
180
- })
181
- .command(rebuildCommand('rebuild', target))
182
- .command(rebuildCommand('rebuild:browser', 'browser'))
183
- .command(rebuildCommand('rebuild:electron', 'electron'))
184
- .command<{
185
- suppress: boolean
186
- }>({
187
- command: 'check:hoisted',
188
- describe: 'Check that all dependencies are hoisted',
189
- builder: {
190
- 'suppress': {
191
- alias: 's',
192
- describe: 'Suppress exiting with failure code',
193
- boolean: true,
194
- default: false
195
- }
196
- },
197
- handler: ({ suppress }) => {
198
- checkDependencies({
199
- workspaces: ['packages/*'],
200
- include: ['**'],
201
- exclude: ['.bin/**', '.cache/**'],
202
- skipHoisted: false,
203
- skipUniqueness: true,
204
- skipSingleTheiaVersion: true,
205
- onlyTheiaExtensions: false,
206
- suppress
207
- });
208
- }
209
- })
210
- .command<{
211
- suppress: boolean
212
- }>({
213
- command: 'check:theia-version',
214
- describe: 'Check that all dependencies have been resolved to the same Theia version',
215
- builder: {
216
- 'suppress': {
217
- alias: 's',
218
- describe: 'Suppress exiting with failure code',
219
- boolean: true,
220
- default: false
221
- }
222
- },
223
- handler: ({ suppress }) => {
224
- checkDependencies({
225
- workspaces: undefined,
226
- include: ['@theia/**'],
227
- exclude: [],
228
- skipHoisted: true,
229
- skipUniqueness: false,
230
- skipSingleTheiaVersion: false,
231
- onlyTheiaExtensions: false,
232
- suppress
233
- });
234
- }
235
- })
236
- .command<{
237
- suppress: boolean
238
- }>({
239
- command: 'check:theia-extensions',
240
- describe: 'Check uniqueness of Theia extension versions or whether they are hoisted',
241
- builder: {
242
- 'suppress': {
243
- alias: 's',
244
- describe: 'Suppress exiting with failure code',
245
- boolean: true,
246
- default: false
247
- }
248
- },
249
- handler: ({ suppress }) => {
250
- checkDependencies({
251
- workspaces: undefined,
252
- include: ['**'],
253
- exclude: [],
254
- skipHoisted: true,
255
- skipUniqueness: false,
256
- skipSingleTheiaVersion: true,
257
- onlyTheiaExtensions: true,
258
- suppress
259
- });
260
- }
261
- })
262
- .command<{
263
- workspaces: string[] | undefined,
264
- include: string[],
265
- exclude: string[],
266
- skipHoisted: boolean,
267
- skipUniqueness: boolean,
268
- skipSingleTheiaVersion: boolean,
269
- onlyTheiaExtensions: boolean,
270
- suppress: boolean
271
- }>({
272
- command: 'check:dependencies',
273
- describe: 'Check uniqueness of dependency versions or whether they are hoisted',
274
- builder: {
275
- 'workspaces': {
276
- alias: 'w',
277
- describe: 'Glob patterns of workspaces to analyze, relative to `cwd`',
278
- array: true,
279
- defaultDescription: 'All glob patterns listed in the package.json\'s workspaces',
280
- demandOption: false
281
- },
282
- 'include': {
283
- alias: 'i',
284
- describe: 'Glob pattern of dependencies\' package names to be included, e.g. -i "@theia/**"',
285
- array: true,
286
- default: ['**']
287
- },
288
- 'exclude': {
289
- alias: 'e',
290
- describe: 'Glob pattern of dependencies\' package names to be excluded',
291
- array: true,
292
- defaultDescription: 'None',
293
- default: []
294
- },
295
- 'skip-hoisted': {
296
- alias: 'h',
297
- describe: 'Skip checking whether dependencies are hoisted',
298
- boolean: true,
299
- default: false
300
- },
301
- 'skip-uniqueness': {
302
- alias: 'u',
303
- describe: 'Skip checking whether all dependencies are resolved to a unique version',
304
- boolean: true,
305
- default: false
306
- },
307
- 'skip-single-theia-version': {
308
- alias: 't',
309
- describe: 'Skip checking whether all @theia/* dependencies are resolved to a single version',
310
- boolean: true,
311
- default: false
312
- },
313
- 'only-theia-extensions': {
314
- alias: 'o',
315
- describe: 'Only check dependencies which are Theia extensions',
316
- boolean: true,
317
- default: false
318
- },
319
- 'suppress': {
320
- alias: 's',
321
- describe: 'Suppress exiting with failure code',
322
- boolean: true,
323
- default: false
324
- }
325
- },
326
- handler: ({
327
- workspaces,
328
- include,
329
- exclude,
330
- skipHoisted,
331
- skipUniqueness,
332
- skipSingleTheiaVersion,
333
- onlyTheiaExtensions,
334
- suppress
335
- }) => {
336
- checkDependencies({
337
- workspaces,
338
- include,
339
- exclude,
340
- skipHoisted,
341
- skipUniqueness,
342
- skipSingleTheiaVersion,
343
- onlyTheiaExtensions,
344
- suppress
345
- });
346
- }
347
- })
348
- .command<{
349
- packed: boolean
350
- ignoreErrors: boolean
351
- apiVersion: string
352
- apiUrl: string
353
- parallel: boolean
354
- proxyUrl?: string
355
- proxyAuthorization?: string
356
- strictSsl: boolean
357
- rateLimit: number
358
- ovsxRouterConfig?: string
359
- }>({
360
- command: 'download:plugins',
361
- describe: 'Download defined external plugins',
362
- builder: {
363
- 'packed': {
364
- alias: 'p',
365
- describe: 'Controls whether to pack or unpack plugins',
366
- boolean: true,
367
- default: false,
368
- },
369
- 'ignore-errors': {
370
- alias: 'i',
371
- describe: 'Ignore errors while downloading plugins',
372
- boolean: true,
373
- default: false,
374
- },
375
- 'api-version': {
376
- alias: 'v',
377
- describe: 'Supported API version for plugins',
378
- default: DEFAULT_SUPPORTED_API_VERSION
379
- },
380
- 'api-url': {
381
- alias: 'u',
382
- describe: 'Open-VSX Registry API URL',
383
- default: 'https://open-vsx.org/api'
384
- },
385
- 'parallel': {
386
- describe: 'Download in parallel',
387
- boolean: true,
388
- default: true
389
- },
390
- 'rate-limit': {
391
- describe: 'Amount of maximum open-vsx requests per second',
392
- number: true,
393
- default: OVSX_RATE_LIMIT
394
- },
395
- 'proxy-url': {
396
- describe: 'Proxy URL'
397
- },
398
- 'proxy-authorization': {
399
- describe: 'Proxy authorization information'
400
- },
401
- 'strict-ssl': {
402
- describe: 'Whether to enable strict SSL mode',
403
- boolean: true,
404
- default: false
405
- },
406
- 'ovsx-router-config': {
407
- describe: 'JSON configuration file for the OVSX router client',
408
- type: 'string'
409
- }
410
- },
411
- handler: async ({ apiUrl, proxyUrl, proxyAuthorization, strictSsl, ovsxRouterConfig, ...options }) => {
412
- const requestService = new NodeRequestService();
413
- await requestService.configure({
414
- proxyUrl,
415
- proxyAuthorization,
416
- strictSSL: strictSsl
417
- });
418
- let client: OVSXClient | undefined;
419
- const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
420
- if (ovsxRouterConfig) {
421
- const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
422
- console.error(error);
423
- });
424
- if (routerConfig) {
425
- client = await OVSXRouterClient.FromConfig(
426
- routerConfig,
427
- OVSXHttpClient.createClientFactory(requestService, rateLimiter),
428
- [RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
429
- );
430
- }
431
- }
432
- if (!client) {
433
- client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
434
- }
435
- await downloadPlugins(client, rateLimiter, requestService, options);
436
- },
437
- })
438
- .command<{
439
- freeApi?: boolean,
440
- deeplKey: string,
441
- file: string,
442
- languages: string[],
443
- sourceLanguage?: string
444
- }>({
445
- command: 'nls-localize [languages...]',
446
- describe: 'Localize json files using the DeepL API',
447
- builder: {
448
- 'file': {
449
- alias: 'f',
450
- describe: 'The source file which should be translated',
451
- demandOption: true
452
- },
453
- 'deepl-key': {
454
- alias: 'k',
455
- describe: 'DeepL key used for API access. See https://www.deepl.com/docs-api for more information',
456
- demandOption: true
457
- },
458
- 'free-api': {
459
- describe: 'Indicates whether the specified DeepL API key belongs to the free API',
460
- boolean: true,
461
- default: false,
462
- },
463
- 'source-language': {
464
- alias: 's',
465
- describe: 'The source language of the translation file'
466
- }
467
- },
468
- handler: async ({ freeApi, deeplKey, file, sourceLanguage, languages = [] }) => {
469
- const success = await localizationManager.localize({
470
- sourceFile: file,
471
- freeApi: freeApi ?? true,
472
- authKey: deeplKey,
473
- targetLanguages: languages,
474
- sourceLanguage
475
- });
476
- if (!success) {
477
- process.exit(1);
478
- }
479
- }
480
- })
481
- .command<{
482
- root: string,
483
- output: string,
484
- merge: boolean,
485
- exclude?: string,
486
- logs?: string,
487
- files?: string[],
488
- quiet: boolean
489
- }>({
490
- command: 'nls-extract',
491
- describe: 'Extract translation key/value pairs from source code',
492
- builder: {
493
- 'output': {
494
- alias: 'o',
495
- describe: 'Output file for the extracted translations',
496
- demandOption: true
497
- },
498
- 'root': {
499
- alias: 'r',
500
- describe: 'The directory which contains the source code',
501
- default: '.'
502
- },
503
- 'merge': {
504
- alias: 'm',
505
- describe: 'Whether to merge new with existing translation values',
506
- boolean: true,
507
- default: false
508
- },
509
- 'exclude': {
510
- alias: 'e',
511
- describe: 'Allows to exclude translation keys starting with this value'
512
- },
513
- 'files': {
514
- alias: 'f',
515
- describe: 'Glob pattern matching the files to extract from (starting from --root).',
516
- array: true
517
- },
518
- 'logs': {
519
- alias: 'l',
520
- describe: 'File path to a log file'
521
- },
522
- 'quiet': {
523
- alias: 'q',
524
- describe: 'Prevents errors from being logged to console',
525
- boolean: true,
526
- default: false
527
- }
528
- },
529
- handler: async options => {
530
- await extract(options);
531
- }
532
- })
533
- .command<{
534
- testInspect: boolean,
535
- testExtension: string[],
536
- testFile: string[],
537
- testIgnore: string[],
538
- testRecursive: boolean,
539
- testSort: boolean,
540
- testSpec: string[],
541
- testCoverage: boolean
542
- theiaArgs?: (string | number)[]
543
- }>({
544
- command: 'test [theia-args...]',
545
- builder: {
546
- 'test-inspect': {
547
- describe: 'Whether to auto-open a DevTools panel for test page.',
548
- boolean: true,
549
- default: false
550
- },
551
- 'test-extension': {
552
- describe: 'Test file extension(s) to load',
553
- array: true,
554
- default: ['js']
555
- },
556
- 'test-file': {
557
- describe: 'Specify test file(s) to be loaded prior to root suite execution',
558
- array: true,
559
- default: []
560
- },
561
- 'test-ignore': {
562
- describe: 'Ignore test file(s) or glob pattern(s)',
563
- array: true,
564
- default: []
565
- },
566
- 'test-recursive': {
567
- describe: 'Look for tests in subdirectories',
568
- boolean: true,
569
- default: false
570
- },
571
- 'test-sort': {
572
- describe: 'Sort test files',
573
- boolean: true,
574
- default: false
575
- },
576
- 'test-spec': {
577
- describe: 'One or more test files, directories, or globs to test',
578
- array: true,
579
- default: ['test']
580
- },
581
- 'test-coverage': {
582
- describe: 'Report test coverage consumable by istanbul',
583
- boolean: true,
584
- default: false
585
- }
586
- },
587
- handler: async ({ testInspect, testExtension, testFile, testIgnore, testRecursive, testSort, testSpec, testCoverage, theiaArgs }) => {
588
- if (!process.env.THEIA_CONFIG_DIR) {
589
- process.env.THEIA_CONFIG_DIR = temp.track().mkdirSync('theia-test-config-dir');
590
- }
591
- await runTest({
592
- start: () => new Promise((resolve, reject) => {
593
- const serverProcess = manager.start(toStringArray(theiaArgs));
594
- serverProcess.on('message', resolve);
595
- serverProcess.on('error', reject);
596
- serverProcess.on('close', (code, signal) => reject(`Server process exited unexpectedly: ${code ?? signal}`));
597
- }),
598
- launch: {
599
- args: ['--no-sandbox'],
600
- // eslint-disable-next-line no-null/no-null
601
- defaultViewport: null, // view port can take available space instead of 800x600 default
602
- devtools: testInspect,
603
- executablePath: executablePath()
604
- },
605
- files: {
606
- extension: testExtension,
607
- file: testFile,
608
- ignore: testIgnore,
609
- recursive: testRecursive,
610
- sort: testSort,
611
- spec: testSpec
612
- },
613
- coverage: testCoverage
614
- });
615
- }
616
- })
617
- .command<{
618
- electronVersion?: string
619
- electronDist?: string
620
- ffmpegPath?: string
621
- platform?: NodeJS.Platform
622
- }>({
623
- command: 'ffmpeg:replace [ffmpeg-path]',
624
- describe: '',
625
- builder: {
626
- 'electronDist': {
627
- description: 'Electron distribution location.',
628
- },
629
- 'electronVersion': {
630
- description: 'Electron version for which to pull the "clean" ffmpeg library.',
631
- },
632
- 'ffmpegPath': {
633
- description: 'Absolute path to the ffmpeg shared library.',
634
- },
635
- 'platform': {
636
- description: 'Dictates where the library is located within the Electron distribution.',
637
- choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
638
- },
639
- },
640
- handler: async options => {
641
- const ffmpeg = await import('@theia/ffmpeg');
642
- await ffmpeg.replaceFfmpeg(options);
643
- },
644
- })
645
- .command<{
646
- electronDist?: string
647
- ffmpegPath?: string
648
- json?: boolean
649
- platform?: NodeJS.Platform
650
- }>({
651
- command: 'ffmpeg:check [ffmpeg-path]',
652
- describe: '(electron-only) Check that ffmpeg doesn\'t contain proprietary codecs',
653
- builder: {
654
- 'electronDist': {
655
- description: 'Electron distribution location',
656
- },
657
- 'ffmpegPath': {
658
- describe: 'Absolute path to the ffmpeg shared library',
659
- },
660
- 'json': {
661
- description: 'Output the found codecs as JSON on stdout',
662
- boolean: true,
663
- },
664
- 'platform': {
665
- description: 'Dictates where the library is located within the Electron distribution',
666
- choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
667
- },
668
- },
669
- handler: async options => {
670
- const ffmpeg = await import('@theia/ffmpeg');
671
- await ffmpeg.checkFfmpeg(options);
672
- },
673
- })
674
- .parserConfiguration({
675
- 'unknown-options-as-args': true,
676
- })
677
- .strictCommands()
678
- .demandCommand(1, 'Please run a command')
679
- .fail((msg, err, cli) => {
680
- process.exitCode = 1;
681
- if (err) {
682
- // One of the handlers threw an error:
683
- console.error(err);
684
- } else {
685
- // Yargs detected a problem with commands and/or arguments while parsing:
686
- cli.showHelp();
687
- console.error(msg);
688
- }
689
- })
690
- .parse();
691
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017 TypeFox 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 * as temp from 'temp';
20
+ import * as yargs from 'yargs';
21
+ import yargsFactory = require('yargs/yargs');
22
+ import { ApplicationPackageManager, rebuild } from '@theia/application-manager';
23
+ import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package';
24
+ import checkDependencies from './check-dependencies';
25
+ import downloadPlugins from './download-plugins';
26
+ import runTest from './run-test';
27
+ import { RateLimiter } from 'limiter';
28
+ import { LocalizationManager, extract } from '@theia/localization-manager';
29
+ import { NodeRequestService } from '@theia/request/lib/node-request-service';
30
+ import { ExtensionIdMatchesFilterFactory, OVSX_RATE_LIMIT, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
31
+
32
+ const { executablePath } = require('puppeteer');
33
+
34
+ process.on('unhandledRejection', (reason, promise) => {
35
+ throw reason;
36
+ });
37
+ process.on('uncaughtException', error => {
38
+ if (error) {
39
+ console.error('Uncaught Exception: ', error.toString());
40
+ if (error.stack) {
41
+ console.error(error.stack);
42
+ }
43
+ }
44
+ process.exit(1);
45
+ });
46
+ theiaCli();
47
+
48
+ function toStringArray(argv: (string | number)[]): string[];
49
+ function toStringArray(argv?: (string | number)[]): string[] | undefined;
50
+ function toStringArray(argv?: (string | number)[]): string[] | undefined {
51
+ return argv?.map(arg => String(arg));
52
+ }
53
+
54
+ function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule<unknown, {
55
+ modules: string[]
56
+ cacheRoot?: string
57
+ forceAbi?: number,
58
+ }> {
59
+ return {
60
+ command,
61
+ describe: `Rebuild/revert native node modules for "${target}"`,
62
+ builder: {
63
+ 'cacheRoot': {
64
+ type: 'string',
65
+ describe: 'Root folder where to store the .browser_modules cache'
66
+ },
67
+ 'modules': {
68
+ alias: 'm',
69
+ type: 'array', // === `--modules/-m` can be specified multiple times
70
+ describe: 'List of modules to rebuild/revert'
71
+ },
72
+ 'forceAbi': {
73
+ type: 'number',
74
+ describe: 'The Node ABI version to rebuild for'
75
+ }
76
+ },
77
+ handler: ({ cacheRoot, modules, forceAbi }) => {
78
+ // Note: `modules` is actually `string[] | undefined`.
79
+ if (modules) {
80
+ // It is ergonomic to pass arguments as --modules="a,b,c,..."
81
+ // but yargs doesn't parse it this way by default.
82
+ const flattened: string[] = [];
83
+ for (const value of modules) {
84
+ if (value.includes(',')) {
85
+ flattened.push(...value.split(',').map(mod => mod.trim()));
86
+ } else {
87
+ flattened.push(value);
88
+ }
89
+ }
90
+ modules = flattened;
91
+ }
92
+ rebuild(target, { cacheRoot, modules, forceAbi });
93
+ }
94
+ };
95
+ }
96
+
97
+ function defineCommonOptions<T>(cli: yargs.Argv<T>): yargs.Argv<T & {
98
+ appTarget?: 'browser' | 'electron' | 'browser-only'
99
+ }> {
100
+ return cli
101
+ .option('app-target', {
102
+ description: 'The target application type. Overrides `theia.target` in the application\'s package.json',
103
+ choices: ['browser', 'electron', 'browser-only'] as const,
104
+ });
105
+ }
106
+
107
+ async function theiaCli(): Promise<void> {
108
+ const { version } = await fs.promises.readFile(path.join(__dirname, '../package.json'), 'utf8').then(JSON.parse);
109
+ yargs.scriptName('theia').version(version);
110
+ const projectPath = process.cwd();
111
+ // Create a sub `yargs` parser to read `app-target` without
112
+ // affecting the global `yargs` instance used by the CLI.
113
+ const { appTarget } = defineCommonOptions(yargsFactory()).help(false).parse();
114
+ const manager = new ApplicationPackageManager({ projectPath, appTarget });
115
+ const localizationManager = new LocalizationManager();
116
+ const { target } = manager.pck;
117
+ defineCommonOptions(yargs)
118
+ .command<{
119
+ theiaArgs?: (string | number)[]
120
+ }>({
121
+ command: 'start [theia-args...]',
122
+ describe: `Start the ${target} backend`,
123
+ // Disable this command's `--help` option so that it is forwarded to Theia's CLI
124
+ builder: cli => cli.help(false) as yargs.Argv,
125
+ handler: async ({ theiaArgs }) => {
126
+ manager.start(toStringArray(theiaArgs));
127
+ }
128
+ })
129
+ .command({
130
+ command: 'clean',
131
+ describe: `Clean for the ${target} target`,
132
+ handler: async () => {
133
+ await manager.clean();
134
+ }
135
+ })
136
+ .command({
137
+ command: 'copy',
138
+ describe: 'Copy various files from `src-gen` to `lib`',
139
+ handler: async () => {
140
+ await manager.copy();
141
+ }
142
+ })
143
+ .command<{
144
+ mode: 'development' | 'production',
145
+ splitFrontend?: boolean
146
+ }>({
147
+ command: 'generate',
148
+ describe: `Generate various files for the ${target} target`,
149
+ builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli),
150
+ handler: async ({ mode, splitFrontend }) => {
151
+ await manager.generate({ mode, splitFrontend });
152
+ }
153
+ })
154
+ .command<{
155
+ mode: 'development' | 'production',
156
+ webpackHelp: boolean
157
+ splitFrontend?: boolean
158
+ webpackArgs?: (string | number)[]
159
+ }>({
160
+ command: 'build [webpack-args...]',
161
+ describe: `Generate and bundle the ${target} frontend using webpack`,
162
+ builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli)
163
+ .option('webpack-help' as 'webpackHelp', {
164
+ boolean: true,
165
+ description: 'Display Webpack\'s help',
166
+ default: false
167
+ }),
168
+ handler: async ({ mode, splitFrontend, webpackHelp, webpackArgs = [] }) => {
169
+ await manager.build(
170
+ webpackHelp
171
+ ? ['--help']
172
+ : [
173
+ // Forward the `mode` argument to Webpack too:
174
+ '--mode', mode,
175
+ ...toStringArray(webpackArgs)
176
+ ],
177
+ { mode, splitFrontend }
178
+ );
179
+ }
180
+ })
181
+ .command(rebuildCommand('rebuild', target))
182
+ .command(rebuildCommand('rebuild:browser', 'browser'))
183
+ .command(rebuildCommand('rebuild:electron', 'electron'))
184
+ .command<{
185
+ suppress: boolean
186
+ }>({
187
+ command: 'check:hoisted',
188
+ describe: 'Check that all dependencies are hoisted',
189
+ builder: {
190
+ 'suppress': {
191
+ alias: 's',
192
+ describe: 'Suppress exiting with failure code',
193
+ boolean: true,
194
+ default: false
195
+ }
196
+ },
197
+ handler: ({ suppress }) => {
198
+ checkDependencies({
199
+ workspaces: ['packages/*'],
200
+ include: ['**'],
201
+ exclude: ['.bin/**', '.cache/**'],
202
+ skipHoisted: false,
203
+ skipUniqueness: true,
204
+ skipSingleTheiaVersion: true,
205
+ onlyTheiaExtensions: false,
206
+ suppress
207
+ });
208
+ }
209
+ })
210
+ .command<{
211
+ suppress: boolean
212
+ }>({
213
+ command: 'check:theia-version',
214
+ describe: 'Check that all dependencies have been resolved to the same Theia version',
215
+ builder: {
216
+ 'suppress': {
217
+ alias: 's',
218
+ describe: 'Suppress exiting with failure code',
219
+ boolean: true,
220
+ default: false
221
+ }
222
+ },
223
+ handler: ({ suppress }) => {
224
+ checkDependencies({
225
+ workspaces: undefined,
226
+ include: ['@theia/**'],
227
+ exclude: [],
228
+ skipHoisted: true,
229
+ skipUniqueness: false,
230
+ skipSingleTheiaVersion: false,
231
+ onlyTheiaExtensions: false,
232
+ suppress
233
+ });
234
+ }
235
+ })
236
+ .command<{
237
+ suppress: boolean
238
+ }>({
239
+ command: 'check:theia-extensions',
240
+ describe: 'Check uniqueness of Theia extension versions or whether they are hoisted',
241
+ builder: {
242
+ 'suppress': {
243
+ alias: 's',
244
+ describe: 'Suppress exiting with failure code',
245
+ boolean: true,
246
+ default: false
247
+ }
248
+ },
249
+ handler: ({ suppress }) => {
250
+ checkDependencies({
251
+ workspaces: undefined,
252
+ include: ['**'],
253
+ exclude: [],
254
+ skipHoisted: true,
255
+ skipUniqueness: false,
256
+ skipSingleTheiaVersion: true,
257
+ onlyTheiaExtensions: true,
258
+ suppress
259
+ });
260
+ }
261
+ })
262
+ .command<{
263
+ workspaces: string[] | undefined,
264
+ include: string[],
265
+ exclude: string[],
266
+ skipHoisted: boolean,
267
+ skipUniqueness: boolean,
268
+ skipSingleTheiaVersion: boolean,
269
+ onlyTheiaExtensions: boolean,
270
+ suppress: boolean
271
+ }>({
272
+ command: 'check:dependencies',
273
+ describe: 'Check uniqueness of dependency versions or whether they are hoisted',
274
+ builder: {
275
+ 'workspaces': {
276
+ alias: 'w',
277
+ describe: 'Glob patterns of workspaces to analyze, relative to `cwd`',
278
+ array: true,
279
+ defaultDescription: 'All glob patterns listed in the package.json\'s workspaces',
280
+ demandOption: false
281
+ },
282
+ 'include': {
283
+ alias: 'i',
284
+ describe: 'Glob pattern of dependencies\' package names to be included, e.g. -i "@theia/**"',
285
+ array: true,
286
+ default: ['**']
287
+ },
288
+ 'exclude': {
289
+ alias: 'e',
290
+ describe: 'Glob pattern of dependencies\' package names to be excluded',
291
+ array: true,
292
+ defaultDescription: 'None',
293
+ default: []
294
+ },
295
+ 'skip-hoisted': {
296
+ alias: 'h',
297
+ describe: 'Skip checking whether dependencies are hoisted',
298
+ boolean: true,
299
+ default: false
300
+ },
301
+ 'skip-uniqueness': {
302
+ alias: 'u',
303
+ describe: 'Skip checking whether all dependencies are resolved to a unique version',
304
+ boolean: true,
305
+ default: false
306
+ },
307
+ 'skip-single-theia-version': {
308
+ alias: 't',
309
+ describe: 'Skip checking whether all @theia/* dependencies are resolved to a single version',
310
+ boolean: true,
311
+ default: false
312
+ },
313
+ 'only-theia-extensions': {
314
+ alias: 'o',
315
+ describe: 'Only check dependencies which are Theia extensions',
316
+ boolean: true,
317
+ default: false
318
+ },
319
+ 'suppress': {
320
+ alias: 's',
321
+ describe: 'Suppress exiting with failure code',
322
+ boolean: true,
323
+ default: false
324
+ }
325
+ },
326
+ handler: ({
327
+ workspaces,
328
+ include,
329
+ exclude,
330
+ skipHoisted,
331
+ skipUniqueness,
332
+ skipSingleTheiaVersion,
333
+ onlyTheiaExtensions,
334
+ suppress
335
+ }) => {
336
+ checkDependencies({
337
+ workspaces,
338
+ include,
339
+ exclude,
340
+ skipHoisted,
341
+ skipUniqueness,
342
+ skipSingleTheiaVersion,
343
+ onlyTheiaExtensions,
344
+ suppress
345
+ });
346
+ }
347
+ })
348
+ .command<{
349
+ packed: boolean
350
+ ignoreErrors: boolean
351
+ apiVersion: string
352
+ apiUrl: string
353
+ parallel: boolean
354
+ proxyUrl?: string
355
+ proxyAuthorization?: string
356
+ strictSsl: boolean
357
+ rateLimit: number
358
+ ovsxRouterConfig?: string
359
+ }>({
360
+ command: 'download:plugins',
361
+ describe: 'Download defined external plugins',
362
+ builder: {
363
+ 'packed': {
364
+ alias: 'p',
365
+ describe: 'Controls whether to pack or unpack plugins',
366
+ boolean: true,
367
+ default: false,
368
+ },
369
+ 'ignore-errors': {
370
+ alias: 'i',
371
+ describe: 'Ignore errors while downloading plugins',
372
+ boolean: true,
373
+ default: false,
374
+ },
375
+ 'api-version': {
376
+ alias: 'v',
377
+ describe: 'Supported API version for plugins',
378
+ default: DEFAULT_SUPPORTED_API_VERSION
379
+ },
380
+ 'api-url': {
381
+ alias: 'u',
382
+ describe: 'Open-VSX Registry API URL',
383
+ default: 'https://open-vsx.org/api'
384
+ },
385
+ 'parallel': {
386
+ describe: 'Download in parallel',
387
+ boolean: true,
388
+ default: true
389
+ },
390
+ 'rate-limit': {
391
+ describe: 'Amount of maximum open-vsx requests per second',
392
+ number: true,
393
+ default: OVSX_RATE_LIMIT
394
+ },
395
+ 'proxy-url': {
396
+ describe: 'Proxy URL'
397
+ },
398
+ 'proxy-authorization': {
399
+ describe: 'Proxy authorization information'
400
+ },
401
+ 'strict-ssl': {
402
+ describe: 'Whether to enable strict SSL mode',
403
+ boolean: true,
404
+ default: false
405
+ },
406
+ 'ovsx-router-config': {
407
+ describe: 'JSON configuration file for the OVSX router client',
408
+ type: 'string'
409
+ }
410
+ },
411
+ handler: async ({ apiUrl, proxyUrl, proxyAuthorization, strictSsl, ovsxRouterConfig, ...options }) => {
412
+ const requestService = new NodeRequestService();
413
+ await requestService.configure({
414
+ proxyUrl,
415
+ proxyAuthorization,
416
+ strictSSL: strictSsl
417
+ });
418
+ let client: OVSXClient | undefined;
419
+ const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
420
+ if (ovsxRouterConfig) {
421
+ const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
422
+ console.error(error);
423
+ });
424
+ if (routerConfig) {
425
+ client = await OVSXRouterClient.FromConfig(
426
+ routerConfig,
427
+ OVSXHttpClient.createClientFactory(requestService, rateLimiter),
428
+ [RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
429
+ );
430
+ }
431
+ }
432
+ if (!client) {
433
+ client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
434
+ }
435
+ await downloadPlugins(client, rateLimiter, requestService, options);
436
+ },
437
+ })
438
+ .command<{
439
+ freeApi?: boolean,
440
+ deeplKey: string,
441
+ file: string,
442
+ languages: string[],
443
+ sourceLanguage?: string
444
+ }>({
445
+ command: 'nls-localize [languages...]',
446
+ describe: 'Localize json files using the DeepL API',
447
+ builder: {
448
+ 'file': {
449
+ alias: 'f',
450
+ describe: 'The source file which should be translated',
451
+ demandOption: true
452
+ },
453
+ 'deepl-key': {
454
+ alias: 'k',
455
+ describe: 'DeepL key used for API access. See https://www.deepl.com/docs-api for more information',
456
+ demandOption: true
457
+ },
458
+ 'free-api': {
459
+ describe: 'Indicates whether the specified DeepL API key belongs to the free API',
460
+ boolean: true,
461
+ default: false,
462
+ },
463
+ 'source-language': {
464
+ alias: 's',
465
+ describe: 'The source language of the translation file'
466
+ }
467
+ },
468
+ handler: async ({ freeApi, deeplKey, file, sourceLanguage, languages = [] }) => {
469
+ const success = await localizationManager.localize({
470
+ sourceFile: file,
471
+ freeApi: freeApi ?? true,
472
+ authKey: deeplKey,
473
+ targetLanguages: languages,
474
+ sourceLanguage
475
+ });
476
+ if (!success) {
477
+ process.exit(1);
478
+ }
479
+ }
480
+ })
481
+ .command<{
482
+ root: string,
483
+ output: string,
484
+ merge: boolean,
485
+ exclude?: string,
486
+ logs?: string,
487
+ files?: string[],
488
+ quiet: boolean
489
+ }>({
490
+ command: 'nls-extract',
491
+ describe: 'Extract translation key/value pairs from source code',
492
+ builder: {
493
+ 'output': {
494
+ alias: 'o',
495
+ describe: 'Output file for the extracted translations',
496
+ demandOption: true
497
+ },
498
+ 'root': {
499
+ alias: 'r',
500
+ describe: 'The directory which contains the source code',
501
+ default: '.'
502
+ },
503
+ 'merge': {
504
+ alias: 'm',
505
+ describe: 'Whether to merge new with existing translation values',
506
+ boolean: true,
507
+ default: false
508
+ },
509
+ 'exclude': {
510
+ alias: 'e',
511
+ describe: 'Allows to exclude translation keys starting with this value'
512
+ },
513
+ 'files': {
514
+ alias: 'f',
515
+ describe: 'Glob pattern matching the files to extract from (starting from --root).',
516
+ array: true
517
+ },
518
+ 'logs': {
519
+ alias: 'l',
520
+ describe: 'File path to a log file'
521
+ },
522
+ 'quiet': {
523
+ alias: 'q',
524
+ describe: 'Prevents errors from being logged to console',
525
+ boolean: true,
526
+ default: false
527
+ }
528
+ },
529
+ handler: async options => {
530
+ await extract(options);
531
+ }
532
+ })
533
+ .command<{
534
+ testInspect: boolean,
535
+ testExtension: string[],
536
+ testFile: string[],
537
+ testIgnore: string[],
538
+ testRecursive: boolean,
539
+ testSort: boolean,
540
+ testSpec: string[],
541
+ testCoverage: boolean
542
+ theiaArgs?: (string | number)[]
543
+ }>({
544
+ command: 'test [theia-args...]',
545
+ builder: {
546
+ 'test-inspect': {
547
+ describe: 'Whether to auto-open a DevTools panel for test page.',
548
+ boolean: true,
549
+ default: false
550
+ },
551
+ 'test-extension': {
552
+ describe: 'Test file extension(s) to load',
553
+ array: true,
554
+ default: ['js']
555
+ },
556
+ 'test-file': {
557
+ describe: 'Specify test file(s) to be loaded prior to root suite execution',
558
+ array: true,
559
+ default: []
560
+ },
561
+ 'test-ignore': {
562
+ describe: 'Ignore test file(s) or glob pattern(s)',
563
+ array: true,
564
+ default: []
565
+ },
566
+ 'test-recursive': {
567
+ describe: 'Look for tests in subdirectories',
568
+ boolean: true,
569
+ default: false
570
+ },
571
+ 'test-sort': {
572
+ describe: 'Sort test files',
573
+ boolean: true,
574
+ default: false
575
+ },
576
+ 'test-spec': {
577
+ describe: 'One or more test files, directories, or globs to test',
578
+ array: true,
579
+ default: ['test']
580
+ },
581
+ 'test-coverage': {
582
+ describe: 'Report test coverage consumable by istanbul',
583
+ boolean: true,
584
+ default: false
585
+ }
586
+ },
587
+ handler: async ({ testInspect, testExtension, testFile, testIgnore, testRecursive, testSort, testSpec, testCoverage, theiaArgs }) => {
588
+ if (!process.env.THEIA_CONFIG_DIR) {
589
+ process.env.THEIA_CONFIG_DIR = temp.track().mkdirSync('theia-test-config-dir');
590
+ }
591
+ await runTest({
592
+ start: () => new Promise((resolve, reject) => {
593
+ const serverProcess = manager.start(toStringArray(theiaArgs));
594
+ serverProcess.on('message', resolve);
595
+ serverProcess.on('error', reject);
596
+ serverProcess.on('close', (code, signal) => reject(`Server process exited unexpectedly: ${code ?? signal}`));
597
+ }),
598
+ launch: {
599
+ args: ['--no-sandbox'],
600
+ // eslint-disable-next-line no-null/no-null
601
+ defaultViewport: null, // view port can take available space instead of 800x600 default
602
+ devtools: testInspect,
603
+ executablePath: executablePath()
604
+ },
605
+ files: {
606
+ extension: testExtension,
607
+ file: testFile,
608
+ ignore: testIgnore,
609
+ recursive: testRecursive,
610
+ sort: testSort,
611
+ spec: testSpec
612
+ },
613
+ coverage: testCoverage
614
+ });
615
+ }
616
+ })
617
+ .command<{
618
+ electronVersion?: string
619
+ electronDist?: string
620
+ ffmpegPath?: string
621
+ platform?: NodeJS.Platform
622
+ }>({
623
+ command: 'ffmpeg:replace [ffmpeg-path]',
624
+ describe: '',
625
+ builder: {
626
+ 'electronDist': {
627
+ description: 'Electron distribution location.',
628
+ },
629
+ 'electronVersion': {
630
+ description: 'Electron version for which to pull the "clean" ffmpeg library.',
631
+ },
632
+ 'ffmpegPath': {
633
+ description: 'Absolute path to the ffmpeg shared library.',
634
+ },
635
+ 'platform': {
636
+ description: 'Dictates where the library is located within the Electron distribution.',
637
+ choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
638
+ },
639
+ },
640
+ handler: async options => {
641
+ const ffmpeg = await import('@theia/ffmpeg');
642
+ await ffmpeg.replaceFfmpeg(options);
643
+ },
644
+ })
645
+ .command<{
646
+ electronDist?: string
647
+ ffmpegPath?: string
648
+ json?: boolean
649
+ platform?: NodeJS.Platform
650
+ }>({
651
+ command: 'ffmpeg:check [ffmpeg-path]',
652
+ describe: '(electron-only) Check that ffmpeg doesn\'t contain proprietary codecs',
653
+ builder: {
654
+ 'electronDist': {
655
+ description: 'Electron distribution location',
656
+ },
657
+ 'ffmpegPath': {
658
+ describe: 'Absolute path to the ffmpeg shared library',
659
+ },
660
+ 'json': {
661
+ description: 'Output the found codecs as JSON on stdout',
662
+ boolean: true,
663
+ },
664
+ 'platform': {
665
+ description: 'Dictates where the library is located within the Electron distribution',
666
+ choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
667
+ },
668
+ },
669
+ handler: async options => {
670
+ const ffmpeg = await import('@theia/ffmpeg');
671
+ await ffmpeg.checkFfmpeg(options);
672
+ },
673
+ })
674
+ .parserConfiguration({
675
+ 'unknown-options-as-args': true,
676
+ })
677
+ .strictCommands()
678
+ .demandCommand(1, 'Please run a command')
679
+ .fail((msg, err, cli) => {
680
+ process.exitCode = 1;
681
+ if (err) {
682
+ // One of the handlers threw an error:
683
+ console.error(err);
684
+ } else {
685
+ // Yargs detected a problem with commands and/or arguments while parsing:
686
+ cli.showHelp();
687
+ console.error(msg);
688
+ }
689
+ })
690
+ .parse();
691
+ }