@theia/cli 1.18.0-next.d3501165 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ declare global {
26
26
  import { OVSXClient } from '@theia/ovsx-client/lib/ovsx-client';
27
27
  import { green, red, yellow } from 'colors/safe';
28
28
  import * as decompress from 'decompress';
29
- import { createWriteStream, existsSync, promises as fs } from 'fs';
29
+ import { createWriteStream, promises as fs } from 'fs';
30
30
  import { HttpsProxyAgent } from 'https-proxy-agent';
31
31
  import fetch, { RequestInit, Response } from 'node-fetch';
32
32
  import * as path from 'path';
@@ -34,13 +34,12 @@ import { getProxyForUrl } from 'proxy-from-env';
34
34
  import * as stream from 'stream';
35
35
  import * as temp from 'temp';
36
36
  import { promisify } from 'util';
37
+ import { DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package/lib/api';
37
38
 
38
39
  const pipelineAsPromised = promisify(stream.pipeline);
39
40
 
40
41
  temp.track();
41
42
 
42
- export const extensionPackCacheName = '.packs';
43
-
44
43
  /**
45
44
  * Available options when downloading.
46
45
  */
@@ -73,7 +72,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions =
73
72
  const {
74
73
  packed = false,
75
74
  ignoreErrors = false,
76
- apiVersion = '1.50.0',
75
+ apiVersion = DEFAULT_SUPPORTED_API_VERSION,
77
76
  apiUrl = 'https://open-vsx.org/api'
78
77
  } = options;
79
78
 
@@ -96,32 +95,22 @@ export default async function downloadPlugins(options: DownloadPluginsOptions =
96
95
  return;
97
96
  }
98
97
  try {
99
- // Retrieve the cached extension-packs in order to not re-download them.
100
- const extensionPackCachePath = path.resolve(pluginsDir, extensionPackCacheName);
101
- const cachedExtensionPacks = new Set<string>(
102
- existsSync(extensionPackCachePath)
103
- ? await fs.readdir(extensionPackCachePath)
104
- : []
105
- );
106
98
  console.warn('--- downloading plugins ---');
107
99
  // Download the raw plugins defined by the `theiaPlugins` property.
108
100
  // This will include both "normal" plugins as well as "extension packs".
109
101
  const downloads = [];
110
102
  for (const [plugin, pluginUrl] of Object.entries(pck.theiaPlugins)) {
111
- // Skip extension packs that were moved to `.packs`:
112
- if (cachedExtensionPacks.has(plugin) || typeof pluginUrl !== 'string') {
103
+ if (typeof pluginUrl !== 'string') {
113
104
  continue;
114
105
  }
115
106
  downloads.push(downloadPluginAsync(failures, plugin, pluginUrl, pluginsDir, packed));
116
107
  }
117
108
  await Promise.all(downloads);
109
+
118
110
  console.warn('--- collecting extension-packs ---');
119
111
  const extensionPacks = await collectExtensionPacks(pluginsDir, excludedIds);
120
112
  if (extensionPacks.size > 0) {
121
- console.warn(`--- found ${extensionPacks.size} extension-packs ---`);
122
- // Move extension-packs to `.packs`
123
- await cacheExtensionPacks(pluginsDir, extensionPacks);
124
- console.warn('--- resolving extension-packs ---');
113
+ console.warn(`--- resolving ${extensionPacks.size} extension-packs ---`);
125
114
  const client = new OVSXClient({ apiVersion, apiUrl });
126
115
  // De-duplicate extension ids to only download each once:
127
116
  const ids = new Set<string>(Array.from(extensionPacks.values()).flat());
@@ -129,10 +118,27 @@ export default async function downloadPlugins(options: DownloadPluginsOptions =
129
118
  const extension = await client.getLatestCompatibleExtensionVersion(id);
130
119
  const downloadUrl = extension?.files.download;
131
120
  if (downloadUrl) {
132
- await downloadPluginAsync(failures, id, downloadUrl, pluginsDir, packed);
121
+ await downloadPluginAsync(failures, id, downloadUrl, pluginsDir, packed, extension?.version);
133
122
  }
134
123
  }));
135
124
  }
125
+
126
+ console.warn('--- collecting extension dependencies ---');
127
+ const pluginDependencies = await collectPluginDependencies(pluginsDir, excludedIds);
128
+ if (pluginDependencies.length > 0) {
129
+ console.warn(`--- resolving ${pluginDependencies.length} extension dependencies ---`);
130
+ const client = new OVSXClient({ apiVersion, apiUrl });
131
+ // De-duplicate extension ids to only download each once:
132
+ const ids = new Set<string>(pluginDependencies);
133
+ await Promise.all(Array.from(ids, async id => {
134
+ const extension = await client.getLatestCompatibleExtensionVersion(id);
135
+ const downloadUrl = extension?.files.download;
136
+ if (downloadUrl) {
137
+ await downloadPluginAsync(failures, id, downloadUrl, pluginsDir, packed, extension?.version);
138
+ }
139
+ }));
140
+ }
141
+
136
142
  } finally {
137
143
  temp.cleanupSync();
138
144
  }
@@ -153,7 +159,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions =
153
159
  * @param packed whether to decompress or not.
154
160
  * @param cachedExtensionPacks the list of cached extension packs already downloaded.
155
161
  */
156
- async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl: string, pluginsDir: string, packed: boolean): Promise<void> {
162
+ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl: string, pluginsDir: string, packed: boolean, version?: string): Promise<void> {
157
163
  if (!plugin) {
158
164
  return;
159
165
  }
@@ -162,6 +168,8 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl
162
168
  fileExt = '.tar.gz';
163
169
  } else if (pluginUrl.endsWith('vsix')) {
164
170
  fileExt = '.vsix';
171
+ } else if (pluginUrl.endsWith('theia')) {
172
+ fileExt = '.theia'; // theia plugins.
165
173
  } else {
166
174
  failures.push(red(`error: '${plugin}' has an unsupported file type: '${pluginUrl}'`));
167
175
  return;
@@ -210,7 +218,7 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl
210
218
  return;
211
219
  }
212
220
 
213
- if (fileExt === '.vsix' && packed === true) {
221
+ if ((fileExt === '.vsix' || fileExt === '.theia') && packed === true) {
214
222
  // Download .vsix without decompressing.
215
223
  const file = createWriteStream(targetPath);
216
224
  await pipelineAsPromised(response.body, file);
@@ -221,7 +229,7 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl
221
229
  await decompress(tempFile.path, targetPath);
222
230
  }
223
231
 
224
- console.warn(green(`+ ${plugin}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`));
232
+ console.warn(green(`+ ${plugin}${version ? `@${version}` : ''}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`));
225
233
  }
226
234
 
227
235
  /**
@@ -257,8 +265,7 @@ async function collectPackageJsonPaths(pluginDir: string): Promise<string[]> {
257
265
  // Recursively fetch the list of extension `package.json` files.
258
266
  for (const file of files) {
259
267
  const filePath = path.join(pluginDir, file);
260
- // Exclude the `.packs` folder used to store extension-packs after being resolved.
261
- if (!filePath.startsWith(extensionPackCacheName) && (await fs.stat(filePath)).isDirectory()) {
268
+ if ((await fs.stat(filePath)).isDirectory()) {
262
269
  packageJsonPathList.push(...await collectPackageJsonPaths(filePath));
263
270
  } else if (path.basename(filePath) === 'package.json' && !path.dirname(filePath).includes('node_modules')) {
264
271
  packageJsonPathList.push(filePath);
@@ -280,7 +287,7 @@ async function collectExtensionPacks(pluginDir: string, excludedIds: Set<string>
280
287
  await Promise.all(packageJsonPaths.map(async packageJsonPath => {
281
288
  const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
282
289
  const extensionPack: unknown = json.extensionPack;
283
- if (extensionPack && Array.isArray(extensionPack)) {
290
+ if (Array.isArray(extensionPack)) {
284
291
  extensionPackPaths.set(packageJsonPath, extensionPack.filter(id => {
285
292
  if (excludedIds.has(id)) {
286
293
  console.log(yellow(`'${id}' referenced by '${json.name}' (ext pack) is excluded because of 'theiaPluginsExcludeIds'`));
@@ -294,50 +301,27 @@ async function collectExtensionPacks(pluginDir: string, excludedIds: Set<string>
294
301
  }
295
302
 
296
303
  /**
297
- * Move extension-packs downloaded from `pluginsDir/x` to `pluginsDir/.packs/x`.
298
- *
299
- * The issue we are trying to solve is the following:
300
- * We may skip some extensions declared in a pack due to the `theiaPluginsExcludeIds` list. But once we start
301
- * a Theia application the plugin system will detect the pack and install the missing extensions.
302
- *
303
- * By moving the packs to a subdirectory it should make it invisible to the plugin system, only leaving
304
- * the plugins that were installed under `pluginsDir` directly.
305
- *
306
- * @param extensionPacksPaths the list of extension-pack paths.
304
+ * Get the mapping of paths and their included plugin ids.
305
+ * - If an extension-pack references an explicitly excluded `id` the `id` will be omitted.
306
+ * @param pluginDir the plugin directory.
307
+ * @param excludedIds the list of plugin ids to exclude.
308
+ * @returns the mapping of extension-pack paths and their included plugin ids.
307
309
  */
308
- async function cacheExtensionPacks(pluginsDir: string, extensionPacks: Map<string, unknown>): Promise<void> {
309
- const packsFolderPath = path.resolve(pluginsDir, extensionPackCacheName);
310
- await fs.mkdir(packsFolderPath, { recursive: true });
311
- await Promise.all(Array.from(extensionPacks.entries(), async ([extensionPackPath, value]) => {
312
- extensionPackPath = path.resolve(extensionPackPath);
313
- // Skip entries found in `.packs`
314
- if (extensionPackPath.startsWith(packsFolderPath)) {
315
- return; // skip
316
- }
317
- try {
318
- const oldPath = getExtensionRoot(pluginsDir, extensionPackPath);
319
- const newPath = path.resolve(packsFolderPath, path.basename(oldPath));
320
- if (!existsSync(newPath)) {
321
- await fs.rename(oldPath, newPath);
310
+ async function collectPluginDependencies(pluginDir: string, excludedIds: Set<string>): Promise<string[]> {
311
+ const dependencyIds: string[] = [];
312
+ const packageJsonPaths = await collectPackageJsonPaths(pluginDir);
313
+ await Promise.all(packageJsonPaths.map(async packageJsonPath => {
314
+ const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
315
+ const extensionDependencies: unknown = json.extensionDependencies;
316
+ if (Array.isArray(extensionDependencies)) {
317
+ for (const dependency of extensionDependencies) {
318
+ if (excludedIds.has(dependency)) {
319
+ console.log(yellow(`'${dependency}' referenced by '${json.name}' is excluded because of 'theiaPluginsExcludeIds'`));
320
+ } else {
321
+ dependencyIds.push(dependency);
322
+ }
322
323
  }
323
- } catch (error) {
324
- console.error(error);
325
324
  }
326
325
  }));
327
- }
328
-
329
- /**
330
- * Walk back to the root of an extension starting from its `package.json`. e.g.
331
- *
332
- * ```ts
333
- * getExtensionRoot('/a/b/c', '/a/b/c/EXT/d/e/f/package.json') === '/a/b/c/EXT'
334
- * ```
335
- */
336
- function getExtensionRoot(root: string, packageJsonPath: string): string {
337
- root = path.resolve(root);
338
- packageJsonPath = path.resolve(packageJsonPath);
339
- if (!packageJsonPath.startsWith(root)) {
340
- throw new Error(`unexpected paths:\n root: ${root}\n package.json: ${packageJsonPath}`);
341
- }
342
- return packageJsonPath.substr(0, packageJsonPath.indexOf(path.sep, root.length + 1));
326
+ return dependencyIds;
343
327
  }
package/src/theia.ts CHANGED
@@ -18,10 +18,11 @@ import * as temp from 'temp';
18
18
  import * as yargs from 'yargs';
19
19
  import yargsFactory = require('yargs/yargs');
20
20
  import { ApplicationPackageManager, rebuild } from '@theia/application-manager';
21
- import { ApplicationProps } from '@theia/application-package';
21
+ import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package';
22
22
  import checkHoisted from './check-hoisting';
23
23
  import downloadPlugins from './download-plugins';
24
24
  import runTest from './run-test';
25
+ import { extract } from '@theia/localization-manager';
25
26
 
26
27
  process.on('unhandledRejection', (reason, promise) => {
27
28
  throw reason;
@@ -45,17 +46,46 @@ function toStringArray(argv?: (string | number)[]): string[] | undefined {
45
46
  : argv.map(arg => String(arg));
46
47
  }
47
48
 
48
- function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule<unknown, { modules: string[] }> {
49
+ function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule<unknown, {
50
+ modules: string[]
51
+ cacheRoot?: string
52
+ force?: boolean
53
+ }> {
49
54
  return {
50
55
  command,
51
- describe: `Rebuild native node modules for the ${target}`,
56
+ describe: `Rebuild/revert native node modules for "${target}"`,
52
57
  builder: {
58
+ 'cacheRoot': {
59
+ type: 'string',
60
+ describe: 'Root folder where to store the .browser_modules cache'
61
+ },
53
62
  'modules': {
54
- array: true,
63
+ alias: 'm',
64
+ array: true, // === `--modules/-m` can be specified multiple times
65
+ describe: 'List of modules to rebuild/revert'
55
66
  },
67
+ 'force': {
68
+ alias: 'f',
69
+ boolean: true,
70
+ describe: 'Rebuild modules for Electron anyway',
71
+ }
56
72
  },
57
- handler: args => {
58
- rebuild(target, args.modules);
73
+ handler: ({ cacheRoot, modules, force }) => {
74
+ // Note: `modules` is actually `string[] | undefined`.
75
+ if (modules) {
76
+ // It is ergonomic to pass arguments as --modules="a,b,c,..."
77
+ // but yargs doesn't parse it this way by default.
78
+ const flattened: string[] = [];
79
+ for (const value of modules) {
80
+ if (value.includes(',')) {
81
+ flattened.push(...value.split(',').map(mod => mod.trim()));
82
+ } else {
83
+ flattened.push(value);
84
+ }
85
+ }
86
+ modules = flattened;
87
+ }
88
+ rebuild(target, { cacheRoot, modules, force });
59
89
  }
60
90
  };
61
91
  }
@@ -183,7 +213,7 @@ function theiaCli(): void {
183
213
  'api-version': {
184
214
  alias: 'v',
185
215
  describe: 'Supported API version for plugins',
186
- default: '1.50.0'
216
+ default: DEFAULT_SUPPORTED_API_VERSION
187
217
  },
188
218
  'api-url': {
189
219
  alias: 'u',
@@ -194,7 +224,53 @@ function theiaCli(): void {
194
224
  handler: async ({ packed }) => {
195
225
  await downloadPlugins({ packed });
196
226
  },
197
- }).command<{
227
+ })
228
+ .command<{
229
+ root: string,
230
+ output: string,
231
+ merge: boolean,
232
+ exclude?: string,
233
+ logs?: string,
234
+ files?: string[]
235
+ }>({
236
+ command: 'nls-extract',
237
+ describe: 'Extract translation key/value pairs from source code',
238
+ builder: {
239
+ 'output': {
240
+ alias: 'o',
241
+ describe: 'Output file for the extracted translations',
242
+ demandOption: true
243
+ },
244
+ 'root': {
245
+ alias: 'r',
246
+ describe: 'The directory which contains the source code',
247
+ default: '.'
248
+ },
249
+ 'merge': {
250
+ alias: 'm',
251
+ describe: 'Whether to merge new with existing translation values',
252
+ boolean: true,
253
+ default: false
254
+ },
255
+ 'exclude': {
256
+ alias: 'e',
257
+ describe: 'Allows to exclude translation keys starting with this value'
258
+ },
259
+ 'files': {
260
+ alias: 'f',
261
+ describe: 'Glob pattern matching the files to extract from (starting from --root).',
262
+ array: true
263
+ },
264
+ 'logs': {
265
+ alias: 'l',
266
+ describe: 'File path to a log file'
267
+ }
268
+ },
269
+ handler: async options => {
270
+ await extract(options);
271
+ }
272
+ })
273
+ .command<{
198
274
  testInspect: boolean,
199
275
  testExtension: string[],
200
276
  testFile: string[],