deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
13
dev-packages/cli/.eslintrc.js
Normal file
13
dev-packages/cli/.eslintrc.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
},
|
||||
rules: {
|
||||
'import/no-dynamic-require': 'off'
|
||||
}
|
||||
};
|
||||
370
dev-packages/cli/README.md
Normal file
370
dev-packages/cli/README.md
Normal file
@@ -0,0 +1,370 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - CLI</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Outline
|
||||
|
||||
- [Outline](#outline)
|
||||
- [Description](#description)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Configure](#configure)
|
||||
- [Application Properties](#application-properties)
|
||||
- [Default Preferences](#default-preferences)
|
||||
- [Default Theme](#default-theme)
|
||||
- [Build Target](#build-target)
|
||||
- [Electron Frontend Application Config](#electron-frontend-application-config)
|
||||
- [Using Latest Builds](#using-latest-builds)
|
||||
- [Building](#building)
|
||||
- [Build](#build)
|
||||
- [Watch](#watch)
|
||||
- [Clean](#clean)
|
||||
- [Rebuilding Native Modules](#rebuilding-native-modules)
|
||||
- [Running](#running)
|
||||
- [Debugging](#debugging)
|
||||
- [Testing](#testing)
|
||||
- [Enabling Tests](#enabling-tests)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Configuring Tests](#configuring-tests)
|
||||
- [Inspecting Tests](#inspecting-tests)
|
||||
- [Reporting Test Coverage](#reporting-test-coverage)
|
||||
- [Downloading Plugins](#downloading-plugins)
|
||||
- [Autogenerated Application](#autogenerated-application)
|
||||
- [Additional Information](#additional-information)
|
||||
- [License](#license)
|
||||
- [Trademark](#trademark)
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/cli` package provides helpful scripts and commands for extension and application development.
|
||||
The contributed `theia`, is a command line tool to manage Theia-based applications.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install `@theia/cli` as a dev dependency in your application.
|
||||
|
||||
With yarn:
|
||||
|
||||
```bash
|
||||
yarn add @theia/cli@next --dev
|
||||
```
|
||||
|
||||
With npm:
|
||||
|
||||
```bash
|
||||
npm install @theia/cli@next --save-dev
|
||||
```
|
||||
|
||||
## Configure
|
||||
|
||||
A Theia-based application can be configured via the `theia` property as described in the application's `package.json`.
|
||||
|
||||
### Application Properties
|
||||
|
||||
It is possible `Application Properties` for a given application.\
|
||||
For example, an application can define it's `applicationName` using the following syntax:
|
||||
|
||||
```json
|
||||
"theia": {
|
||||
"frontend": {
|
||||
"config": {
|
||||
"applicationName": "Custom Application Name",
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Default Preferences
|
||||
|
||||
If required, an application can define for a given preference, the default value.
|
||||
For example, an application can update the preference value for `files.enableTrash` based on it's requirements:
|
||||
|
||||
```json
|
||||
|
||||
"theia": {
|
||||
"frontend": {
|
||||
"config": {
|
||||
"preferences": {
|
||||
"files.enableTrash": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Default Theme
|
||||
|
||||
Default color and icon themes can be configured in `theia.frontend.config` section:
|
||||
|
||||
```json
|
||||
"theia": {
|
||||
"frontend": {
|
||||
"config": {
|
||||
"defaultTheme": "light",
|
||||
"defaultIconTheme": "vs-seti"
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Build Target
|
||||
|
||||
The following targets are supported: `browser` and `electron`. By default `browser` target is used.
|
||||
The target can be configured in the `package.json` via `theia/target` property, e.g:
|
||||
|
||||
```json
|
||||
{
|
||||
"theia": {
|
||||
"target": "electron"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/electron": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For `electron` target applications, is it mandatory to include **Electron** runtime dependencies. The `@theia/electron` package is the easiest way to install the necessary dependencies.
|
||||
|
||||
### Electron Frontend Application Config
|
||||
|
||||
The `electron` frontend application configuration provides configuration options for the `electron` target.\
|
||||
The currently supported configurations are:
|
||||
|
||||
- `disallowReloadKeybinding`: if set to `true`, reloading the current browser window won't be possible with the <kbd>Ctrl/Cmd</kbd> + <kbd>r</kbd> keybinding. It is `false` by default. Has no effect if not in an electron environment.
|
||||
- `windowOptions`: override or add properties to the electron `windowOptions`.
|
||||
|
||||
```json
|
||||
{
|
||||
"theia": {
|
||||
"target": "electron",
|
||||
"frontend": {
|
||||
"config": {
|
||||
"electron": {
|
||||
"disallowReloadKeybinding": true,
|
||||
"windowOptions": {
|
||||
"titleBarStyle": "hidden",
|
||||
"webPreferences": {
|
||||
"webSecurity": false,
|
||||
"nodeIntegration": true,
|
||||
"webviewTag": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Latest Builds
|
||||
|
||||
If you set `next` in your theia config, then Theia will prefer `next` over `latest` as the latest tag.
|
||||
|
||||
```json
|
||||
{
|
||||
"theia": {
|
||||
"next": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Build
|
||||
|
||||
The following command can be used to build the application:
|
||||
|
||||
**Development**
|
||||
|
||||
theia build --mode development
|
||||
|
||||
**Production**
|
||||
|
||||
theia build
|
||||
|
||||
### Watch
|
||||
|
||||
The following command can be used to rebuild the application on each change:
|
||||
|
||||
theia build --watch --mode development
|
||||
|
||||
### Clean
|
||||
|
||||
The following command can be used to clean up the build result:
|
||||
|
||||
In order to clean up the build result:
|
||||
|
||||
theia clean
|
||||
|
||||
Arguments are passed directly to [webpack](https://webpack.js.org/). Use `--help` to learn which options are supported.
|
||||
|
||||
## Rebuilding Native Modules
|
||||
|
||||
In order to run Electron targets, one should rebuild native node modules for an electron version:
|
||||
|
||||
theia rebuild
|
||||
|
||||
To rollback native modules, change the target to `browser` and run the command again.
|
||||
|
||||
## Running
|
||||
|
||||
To run the backend server:
|
||||
|
||||
theia start
|
||||
|
||||
For the browser target a server is started on <http://localhost:3000> by default.
|
||||
For the electron target a server is started on `localhost` host with the dynamically allocated port by default.
|
||||
|
||||
Arguments are passed directly to a server, use `--help` to learn which options are supported.
|
||||
|
||||
## Debugging
|
||||
|
||||
To debug the backend server:
|
||||
|
||||
theia start --inspect
|
||||
|
||||
Theia CLI accepts `--inspect` node flag: <https://nodejs.org/en/docs/inspector/#command-line-options>.
|
||||
|
||||
## Testing
|
||||
|
||||
### Enabling Tests
|
||||
|
||||
First enable `expose-loader` in `webpack.config.js`
|
||||
to expose modules from bundled code to tests
|
||||
by un-commenting:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Expose bundled modules on window.theia.moduleName namespace, e.g.
|
||||
* window['theia']['@theia/core/lib/common/uri'].
|
||||
* Such syntax can be used by external code, for instance, for testing.
|
||||
config.module.rules.push({
|
||||
test: /\.js$/,
|
||||
loader: require.resolve('@theia/application-manager/lib/expose-loader')
|
||||
}); */
|
||||
```
|
||||
|
||||
After that run `theia build` again to expose modules in generated bundle files.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
See [API Integration Testing](../../doc/api-testing.md) docs.
|
||||
|
||||
### Running Tests
|
||||
|
||||
To start the backend server and run API tests against it:
|
||||
|
||||
theia test
|
||||
|
||||
After running test this command terminates. It accepts the same arguments as `start` command,
|
||||
but as well additional arguments to specify test files, enable inspection or generate test coverage.
|
||||
|
||||
### Configuring Tests
|
||||
|
||||
To specify test files:
|
||||
|
||||
theia test . --test-spec=./test/*.spec.js --plugins=./plugins
|
||||
|
||||
This command starts the application with a current directory as a workspace,
|
||||
load VS Code extensions from `./plugins`
|
||||
and run test files matching `./test/*.spec.js` glob.
|
||||
|
||||
Use `theia test --help` to learn more options. Test specific options start with `--test-`.
|
||||
|
||||
### Inspecting Tests
|
||||
|
||||
To inspect tests:
|
||||
|
||||
theia test . --test-spec=./test/*.spec.js --test-inspect --inspect
|
||||
|
||||
This command starts the application server in the debug mode
|
||||
as well as open the Chrome devtools to debug frontend code and test files.
|
||||
One can reload/rerun code and tests by simply reloading the page.
|
||||
|
||||
> Important! Since tests are relying on focus, while running tests keep the page focused.
|
||||
|
||||
### Reporting Test Coverage
|
||||
|
||||
To report test coverage:
|
||||
|
||||
theia test . --test-spec=./test/*.spec.js --test-coverage
|
||||
|
||||
This command executes tests and generate test coverage files consumable by [Istanbul](https://github.com/istanbuljs/istanbuljs).
|
||||
|
||||
## Downloading Plugins
|
||||
|
||||
The `@theia/cli` package provides a utility for applications to define and download a list of plugins it requires as part of their application using the command:
|
||||
|
||||
theia download:plugins
|
||||
|
||||
This utility works by declaring in the `package.json` a location to store downloaded plugins, as well as defining each plugin the application wishes to download.
|
||||
|
||||
The property `theiaPluginsDir` describes the location of which to download plugins (relative to the `package.json`), for example:
|
||||
|
||||
```json
|
||||
"theiaPluginsDir": "plugins",
|
||||
```
|
||||
|
||||
The property `theiaPlugins` describes the list of plugins to download, for example:
|
||||
|
||||
```json
|
||||
"theiaPlugins": {
|
||||
"vscode.theme-defaults": "https://open-vsx.org/api/vscode/theme-defaults/1.62.3/file/vscode.theme-defaults-1.62.3.vsix",
|
||||
"vscode-builtin-extension-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.50.0/file/eclipse-theia.builtin-extension-pack-1.50.0.vsix",
|
||||
"vscode-editorconfig": "https://open-vsx.org/api/EditorConfig/EditorConfig/0.14.4/file/EditorConfig.EditorConfig-0.14.4.vsix",
|
||||
"vscode-eslint": "https://open-vsx.org/api/dbaeumer/vscode-eslint/2.1.1/file/dbaeumer.vscode-eslint-2.1.1.vsix",
|
||||
"rust-analyzer": "https://open-vsx.org/api/rust-lang/rust-analyzer/${targetPlatform}/0.4.1473/file/rust-lang.rust-analyzer-0.4.1473@${targetPlatform}.vsix"
|
||||
}
|
||||
```
|
||||
|
||||
As seen in the `rust-analyzer` entry we can use placeholders in the URLs. Supported placeholders are:
|
||||
|
||||
- The `${targetPlatform}` Placeholder, which resolves to a string like `win32-x64` describing the local system and architecture. This is useful for adding non-universal plugins.
|
||||
|
||||
Please note that in order to use `extensionPacks` properly you should use `namespace.name` as the `id` you give extensions so that when resolving the pack we do not re-download an existing plugin under a different name.
|
||||
|
||||
The property `theiaPluginsExcludeIds` can be used to declare the list of plugin `ids` to exclude when using extension-packs.
|
||||
The `ids` referenced by the property will not be downloaded when resolving extension-packs, and can be used to omit extensions which are problematic or unwanted. The format of the property is as follows:
|
||||
|
||||
```json
|
||||
"theiaPluginsExcludeIds": [
|
||||
"vscode.cpp"
|
||||
]
|
||||
```
|
||||
|
||||
## Autogenerated Application
|
||||
|
||||
This package can auto-generate application code for both the backend and frontend, as well as webpack configuration files.
|
||||
|
||||
When targeting Electron, the `electron-main.js` script will spawn the backend process in a Node.js sub-process, where Electron's API won't be available. To prevent the generated application from forking the backend, you can pass a `--no-cluster` flag. This flag is mostly useful/used for debugging.
|
||||
|
||||
```sh
|
||||
# when developing a Theia application with @theia/cli:
|
||||
yarn theia start --no-cluster
|
||||
|
||||
# when starting a bundled application made using @theia/cli:
|
||||
bundled-application.exe --no-cluster
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
37
dev-packages/cli/bin/theia-patch.js
Executable file
37
dev-packages/cli/bin/theia-patch.js
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const patchPackage = require.resolve('patch-package');
|
||||
console.log(`patch-package = ${patchPackage}`);
|
||||
|
||||
const patchesDir = path.join('.', 'node_modules', '@theia', 'cli', 'patches');
|
||||
|
||||
console.log(`patchesdir = ${patchesDir}`);
|
||||
|
||||
const env = Object.assign({}, process.env);
|
||||
|
||||
const scriptProcess = cp.exec(`node "${patchPackage}" --patch-dir "${patchesDir}"`, {
|
||||
cwd: process.cwd(),
|
||||
env
|
||||
});
|
||||
|
||||
scriptProcess.stdout.pipe(process.stdout);
|
||||
scriptProcess.stderr.pipe(process.stderr);
|
||||
2
dev-packages/cli/bin/theia.js
Executable file
2
dev-packages/cli/bin/theia.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../lib/theia')
|
||||
68
dev-packages/cli/package.json
Normal file
68
dev-packages/cli/package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@theia/cli",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia CLI.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"bin",
|
||||
"lib",
|
||||
"src",
|
||||
"patches"
|
||||
],
|
||||
"bin": {
|
||||
"theia": "./bin/theia.js",
|
||||
"theia-patch": "./bin/theia-patch.js"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"build": "theiaext build",
|
||||
"watch": "theiaext watch",
|
||||
"clean": "theiaext clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/application-manager": "1.68.0",
|
||||
"@theia/application-package": "1.68.0",
|
||||
"@theia/ffmpeg": "1.68.0",
|
||||
"@theia/localization-manager": "1.68.0",
|
||||
"@theia/ovsx-client": "1.68.0",
|
||||
"@theia/request": "1.68.0",
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "4.0.0",
|
||||
"decompress": "^4.2.1",
|
||||
"escape-string-regexp": "4.0.0",
|
||||
"glob": "^8.0.3",
|
||||
"http-server": "^14.1.1",
|
||||
"limiter": "^2.1.0",
|
||||
"log-update": "^4.0.0",
|
||||
"mocha": "^10.1.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"puppeteer": "23.1.0",
|
||||
"puppeteer-core": "23.1.0",
|
||||
"puppeteer-to-istanbul": "1.4.0",
|
||||
"temp": "^0.9.1",
|
||||
"tslib": "^2.6.2",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/proxy-from-env": "^1.0.1"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
449
dev-packages/cli/patches/@lumino+widgets+2.7.2.patch
Normal file
449
dev-packages/cli/patches/@lumino+widgets+2.7.2.patch
Normal file
File diff suppressed because one or more lines are too long
328
dev-packages/cli/src/check-dependencies.ts
Normal file
328
dev-packages/cli/src/check-dependencies.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { create as logUpdater } from 'log-update';
|
||||
import * as chalk from 'chalk';
|
||||
|
||||
const NODE_MODULES = 'node_modules';
|
||||
const PACKAGE_JSON = 'package.json';
|
||||
|
||||
const logUpdate = logUpdater(process.stdout);
|
||||
|
||||
interface CheckDependenciesOptions {
|
||||
workspaces: string[] | undefined,
|
||||
include: string[],
|
||||
exclude: string[],
|
||||
skipHoisted: boolean,
|
||||
skipUniqueness: boolean,
|
||||
skipSingleTheiaVersion: boolean,
|
||||
onlyTheiaExtensions: boolean,
|
||||
suppress: boolean
|
||||
}
|
||||
|
||||
/** NPM package */
|
||||
interface Package {
|
||||
/** Name of the package, e.g. `@theia/core`. */
|
||||
name: string,
|
||||
/** Actual resolved version of the package, e.g. `1.27.0`. */
|
||||
version: string,
|
||||
/** Path of the package relative to the workspace, e.g. `node_modules/@theia/core`. */
|
||||
path: string,
|
||||
/** Whether the package is hoisted or not, i.e., whether it is contained in the root `node_modules`. */
|
||||
hoisted: boolean,
|
||||
/** Workspace location in which the package was found. */
|
||||
dependent: string | undefined,
|
||||
/** Whether the package is a Theia extension or not */
|
||||
isTheiaExtension?: boolean,
|
||||
}
|
||||
|
||||
/** Issue found with a specific package. */
|
||||
interface DependencyIssue {
|
||||
/** Type of the issue. */
|
||||
issueType: 'not-hoisted' | 'multiple-versions' | 'theia-version-mix',
|
||||
/** Package with issue. */
|
||||
package: Package,
|
||||
/** Packages related to this issue. */
|
||||
relatedPackages: Package[],
|
||||
/** Severity */
|
||||
severity: 'warning' | 'error'
|
||||
}
|
||||
|
||||
export default function checkDependencies(options: CheckDependenciesOptions): void {
|
||||
const workspaces = deriveWorkspaces(options);
|
||||
logUpdate(`✅ Found ${workspaces.length} workspaces`);
|
||||
|
||||
console.log('🔍 Collecting dependencies...');
|
||||
const dependencies = findAllDependencies(workspaces, options);
|
||||
logUpdate(`✅ Found ${dependencies.length} dependencies`);
|
||||
|
||||
console.log('🔍 Analyzing dependencies...');
|
||||
const issues = analyzeDependencies(dependencies, options);
|
||||
if (issues.length <= 0) {
|
||||
logUpdate('✅ No issues were found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
logUpdate('🟠 Found ' + issues.length + ' issues');
|
||||
printIssues(issues);
|
||||
printHints(issues);
|
||||
process.exit(options.suppress ? 0 : 1);
|
||||
}
|
||||
|
||||
function deriveWorkspaces(options: CheckDependenciesOptions): string[] {
|
||||
const wsGlobs = options.workspaces ?? readWorkspaceGlobsFromPackageJson();
|
||||
const workspaces: string[] = [];
|
||||
for (const wsGlob of wsGlobs) {
|
||||
workspaces.push(...glob.sync(wsGlob + '/'));
|
||||
}
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
function readWorkspaceGlobsFromPackageJson(): string[] {
|
||||
const rootPackageJson = path.join(process.cwd(), PACKAGE_JSON);
|
||||
if (!fs.existsSync(rootPackageJson)) {
|
||||
console.error('Directory does not contain a package.json with defined workspaces');
|
||||
console.info('Run in the root of a Theia project or specify them via --workspaces');
|
||||
process.exit(1);
|
||||
}
|
||||
return require(rootPackageJson).workspaces ?? [];
|
||||
}
|
||||
|
||||
function findAllDependencies(workspaces: string[], options: CheckDependenciesOptions): Package[] {
|
||||
const dependencies: Package[] = [];
|
||||
dependencies.push(...findDependencies('.', options));
|
||||
for (const workspace of workspaces) {
|
||||
dependencies.push(...findDependencies(workspace, options));
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
function findDependencies(workspace: string, options: CheckDependenciesOptions): Package[] {
|
||||
const dependent = getPackageName(path.join(process.cwd(), workspace, PACKAGE_JSON));
|
||||
const nodeModulesDir = path.join(workspace, NODE_MODULES);
|
||||
const matchingPackageJsons: Package[] = [];
|
||||
options.include.forEach(include =>
|
||||
glob.sync(`${include}/${PACKAGE_JSON}`, {
|
||||
cwd: nodeModulesDir,
|
||||
ignore: [
|
||||
`**/${NODE_MODULES}/**`, // node_modules folders within dependencies
|
||||
`[^@]*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and not in an @org)
|
||||
`@*/*/*/**/${PACKAGE_JSON}`, // package.json that isn't at the package root (and in an @org)
|
||||
...options.exclude] // user-specified exclude patterns
|
||||
}).forEach(packageJsonPath => {
|
||||
const dependency = toDependency(packageJsonPath, nodeModulesDir, dependent);
|
||||
if (!options.onlyTheiaExtensions || dependency.isTheiaExtension) {
|
||||
matchingPackageJsons.push(dependency);
|
||||
}
|
||||
const childNodeModules: string = path.join(nodeModulesDir, packageJsonPath, '..');
|
||||
matchingPackageJsons.push(...findDependencies(childNodeModules, options));
|
||||
})
|
||||
);
|
||||
return matchingPackageJsons;
|
||||
}
|
||||
|
||||
function toDependency(packageJsonPath: string, nodeModulesDir: string, dependent?: string): Package {
|
||||
const fullPackageJsonPath = path.join(process.cwd(), nodeModulesDir, packageJsonPath);
|
||||
const name = getPackageName(fullPackageJsonPath);
|
||||
const version = getPackageVersion(fullPackageJsonPath);
|
||||
return {
|
||||
name: name ?? packageJsonPath.replace('/' + PACKAGE_JSON, ''),
|
||||
version: version ?? 'unknown',
|
||||
path: path.relative(process.cwd(), fullPackageJsonPath),
|
||||
hoisted: nodeModulesDir === NODE_MODULES,
|
||||
dependent: dependent,
|
||||
isTheiaExtension: isTheiaExtension(fullPackageJsonPath)
|
||||
};
|
||||
}
|
||||
|
||||
function getPackageVersion(fullPackageJsonPath: string): string | undefined {
|
||||
try {
|
||||
return require(fullPackageJsonPath).version;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getPackageName(fullPackageJsonPath: string): string | undefined {
|
||||
try {
|
||||
return require(fullPackageJsonPath).name;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isTheiaExtension(fullPackageJsonPath: string): boolean {
|
||||
try {
|
||||
const theiaExtension = require(fullPackageJsonPath).theiaExtensions;
|
||||
return theiaExtension ? true : false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function analyzeDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
|
||||
const issues: DependencyIssue[] = [];
|
||||
if (!options.skipHoisted) {
|
||||
issues.push(...findNotHoistedDependencies(packages, options));
|
||||
}
|
||||
if (!options.skipUniqueness) {
|
||||
issues.push(...findDuplicateDependencies(packages, options));
|
||||
}
|
||||
if (!options.skipSingleTheiaVersion) {
|
||||
issues.push(...findTheiaVersionMix(packages, options));
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function findNotHoistedDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
|
||||
const issues: DependencyIssue[] = [];
|
||||
const nonHoistedPackages = packages.filter(p => p.hoisted === false);
|
||||
for (const nonHoistedPackage of nonHoistedPackages) {
|
||||
issues.push(createNonHoistedPackageIssue(nonHoistedPackage, options));
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function createNonHoistedPackageIssue(nonHoistedPackage: Package, options: CheckDependenciesOptions): DependencyIssue {
|
||||
return {
|
||||
issueType: 'not-hoisted',
|
||||
package: nonHoistedPackage,
|
||||
relatedPackages: [getHoistedPackageByName(nonHoistedPackage.name)],
|
||||
severity: options.suppress ? 'warning' : 'error'
|
||||
};
|
||||
}
|
||||
|
||||
function getHoistedPackageByName(name: string): Package {
|
||||
return toDependency(path.join(name, PACKAGE_JSON), NODE_MODULES);
|
||||
}
|
||||
|
||||
function findDuplicateDependencies(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
|
||||
const duplicates: string[] = [];
|
||||
const packagesGroupedByName = new Map<string, Package[]>();
|
||||
for (const currentPackage of packages) {
|
||||
const name = currentPackage.name;
|
||||
if (!packagesGroupedByName.has(name)) {
|
||||
packagesGroupedByName.set(name, []);
|
||||
}
|
||||
const currentPackages = packagesGroupedByName.get(name)!;
|
||||
currentPackages.push(currentPackage);
|
||||
if (currentPackages.length > 1 && duplicates.indexOf(name) === -1) {
|
||||
duplicates.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
duplicates.sort();
|
||||
const issues: DependencyIssue[] = [];
|
||||
for (const duplicate of duplicates) {
|
||||
const duplicatePackages = packagesGroupedByName.get(duplicate);
|
||||
if (duplicatePackages && duplicatePackages.length > 0) {
|
||||
issues.push({
|
||||
issueType: 'multiple-versions',
|
||||
package: duplicatePackages.pop()!,
|
||||
relatedPackages: duplicatePackages,
|
||||
severity: options.suppress ? 'warning' : 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function findTheiaVersionMix(packages: Package[], options: CheckDependenciesOptions): DependencyIssue[] {
|
||||
// @theia/monaco-editor-core is following the versions of Monaco so it can't be part of this check
|
||||
const theiaPackages = packages.filter(p => p.name.startsWith('@theia/') && !p.name.startsWith('@theia/monaco-editor-core'));
|
||||
let theiaVersion = undefined;
|
||||
let referenceTheiaPackage = undefined;
|
||||
const packagesWithOtherVersion: Package[] = [];
|
||||
for (const theiaPackage of theiaPackages) {
|
||||
if (!theiaVersion && theiaPackage.version) {
|
||||
theiaVersion = theiaPackage.version;
|
||||
referenceTheiaPackage = theiaPackage;
|
||||
} else if (theiaVersion !== theiaPackage.version) {
|
||||
packagesWithOtherVersion.push(theiaPackage);
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceTheiaPackage && packagesWithOtherVersion.length > 0) {
|
||||
return [{
|
||||
issueType: 'theia-version-mix',
|
||||
package: referenceTheiaPackage,
|
||||
relatedPackages: packagesWithOtherVersion,
|
||||
severity: 'error'
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function printIssues(issues: DependencyIssue[]): void {
|
||||
console.log();
|
||||
const indent = issues.length.toString().length;
|
||||
issues.forEach((issue, index) => {
|
||||
printIssue(issue, index + 1, indent);
|
||||
});
|
||||
}
|
||||
|
||||
function printIssue(issue: DependencyIssue, issueNumber: number, indent: number): void {
|
||||
const remainingIndent = indent - issueNumber.toString().length;
|
||||
const indentString = ' '.repeat(remainingIndent + 1);
|
||||
console.log(issueTitle(issue, issueNumber, indentString));
|
||||
console.log(issueDetails(issue, ' ' + ' '.repeat(indent)));
|
||||
console.log();
|
||||
}
|
||||
|
||||
function issueTitle(issue: DependencyIssue, issueNumber: number, indent: string): string {
|
||||
const dependent = issue.package.dependent ? ` in ${chalk.blueBright(issue.package.dependent ?? 'unknown')}` : '';
|
||||
return chalk.bgWhiteBright.bold.black(`#${issueNumber}${indent}`) + ' ' + chalk.cyanBright(issue.package.name)
|
||||
+ dependent + chalk.dim(` [${issue.issueType}]`);
|
||||
}
|
||||
|
||||
function issueDetails(issue: DependencyIssue, indent: string): string {
|
||||
return indent + severity(issue) + ' ' + issueMessage(issue) + '\n'
|
||||
+ indent + versionLine(issue.package) + '\n'
|
||||
+ issue.relatedPackages.map(p => indent + versionLine(p)).join('\n');
|
||||
}
|
||||
|
||||
function issueMessage(issue: DependencyIssue): string {
|
||||
if (issue.issueType === 'multiple-versions') {
|
||||
return `Multiple versions of dependency ${chalk.bold(issue.package.name)} found.`;
|
||||
} else if (issue.issueType === 'theia-version-mix') {
|
||||
return `Mix of ${chalk.bold('@theia/*')} versions found.`;
|
||||
} else {
|
||||
return `Dependency ${chalk.bold(issue.package.name)} is not hoisted.`;
|
||||
}
|
||||
}
|
||||
|
||||
function severity(issue: DependencyIssue): string {
|
||||
return issue.severity === 'error' ? chalk.red('error') : chalk.yellow('warning');
|
||||
}
|
||||
|
||||
function versionLine(pckg: Package): string {
|
||||
return chalk.bold(pckg.version) + ' in ' + pckg.path;
|
||||
}
|
||||
|
||||
function printHints(issues: DependencyIssue[]): void {
|
||||
console.log();
|
||||
if (issues.find(i => i.issueType === 'theia-version-mix')) {
|
||||
console.log('⛔ A mix of Theia versions is very likely leading to a broken application.');
|
||||
}
|
||||
console.log(`ℹ️ Use ${chalk.bold('npm ls <package-name>')} to find out why those multiple versions of a package are pulled.`);
|
||||
console.log('ℹ️ Try to resolve those issues by finding package versions along the dependency chain that depend on compatible versions.');
|
||||
console.log(`ℹ️ Use ${chalk.bold('overrides')} in your root package.json to force specific versions as a last resort.`);
|
||||
console.log();
|
||||
}
|
||||
421
dev-packages/cli/src/download-plugins.ts
Normal file
421
dev-packages/cli/src/download-plugins.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { OVSXApiFilterImpl, OVSXClient, VSXTargetPlatform } from '@theia/ovsx-client';
|
||||
import * as chalk from 'chalk';
|
||||
import * as decompress from 'decompress';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as temp from 'temp';
|
||||
import { DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package/lib/api';
|
||||
import { RequestContext, RequestService } from '@theia/request';
|
||||
import { RateLimiter } from 'limiter';
|
||||
import escapeStringRegexp = require('escape-string-regexp');
|
||||
|
||||
temp.track();
|
||||
|
||||
/**
|
||||
* Available options when downloading.
|
||||
*/
|
||||
export interface DownloadPluginsOptions {
|
||||
/**
|
||||
* Determines if a plugin should be unpacked.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
packed?: boolean;
|
||||
|
||||
/**
|
||||
* Determines if failures while downloading plugins should be ignored.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
ignoreErrors?: boolean;
|
||||
|
||||
/**
|
||||
* The supported vscode API version.
|
||||
* Used to determine extension compatibility.
|
||||
*/
|
||||
apiVersion?: string;
|
||||
|
||||
/**
|
||||
* Fetch plugins in parallel
|
||||
*/
|
||||
parallel?: boolean;
|
||||
}
|
||||
|
||||
interface PluginDownload {
|
||||
id: string,
|
||||
downloadUrl: string,
|
||||
version?: string | undefined
|
||||
}
|
||||
|
||||
export default async function downloadPlugins(
|
||||
ovsxClient: OVSXClient,
|
||||
rateLimiter: RateLimiter,
|
||||
requestService: RequestService,
|
||||
options: DownloadPluginsOptions = {}
|
||||
): Promise<void> {
|
||||
const {
|
||||
packed = false,
|
||||
ignoreErrors = false,
|
||||
apiVersion = DEFAULT_SUPPORTED_API_VERSION,
|
||||
parallel = true
|
||||
} = options;
|
||||
|
||||
const apiFilter = new OVSXApiFilterImpl(ovsxClient, apiVersion);
|
||||
|
||||
// Collect the list of failures to be appended at the end of the script.
|
||||
const failures: string[] = [];
|
||||
|
||||
// Resolve the `package.json` at the current working directory.
|
||||
const pck = JSON.parse(await fs.readFile(path.resolve('package.json'), 'utf8'));
|
||||
|
||||
// Resolve the directory for which to download the plugins.
|
||||
const pluginsDir = pck.theiaPluginsDir || 'plugins';
|
||||
|
||||
// Excluded extension ids.
|
||||
const excludedIds = new Set<string>(pck.theiaPluginsExcludeIds || []);
|
||||
|
||||
const parallelOrSequence = async (tasks: (() => unknown)[]) => {
|
||||
if (parallel) {
|
||||
await Promise.all(tasks.map(task => task()));
|
||||
} else {
|
||||
for (const task of tasks) {
|
||||
await task();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Downloader wrapper
|
||||
const downloadPlugin = async (plugin: PluginDownload): Promise<void> => {
|
||||
await downloadPluginAsync(requestService, rateLimiter, failures, plugin.id, plugin.downloadUrl, pluginsDir, packed, excludedIds, plugin.version);
|
||||
};
|
||||
|
||||
const downloader = async (plugins: PluginDownload[]) => {
|
||||
await parallelOrSequence(plugins.map(plugin => () => downloadPlugin(plugin)));
|
||||
};
|
||||
|
||||
await fs.mkdir(pluginsDir, { recursive: true });
|
||||
|
||||
if (!pck.theiaPlugins) {
|
||||
console.log(chalk.red('error: missing mandatory \'theiaPlugins\' property.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.warn('--- downloading plugins ---');
|
||||
// Download the raw plugins defined by the `theiaPlugins` property.
|
||||
// This will include both "normal" plugins as well as "extension packs".
|
||||
const pluginsToDownload = Object.entries(pck.theiaPlugins)
|
||||
.filter((entry: [string, unknown]): entry is [string, string] => typeof entry[1] === 'string')
|
||||
.map(([id, url]) => ({ id, downloadUrl: resolveDownloadUrlPlaceholders(url) }));
|
||||
await downloader(pluginsToDownload);
|
||||
|
||||
const handleDependencyList = async (dependencies: (string | string[])[]) => {
|
||||
// De-duplicate extension ids to only download each once:
|
||||
const ids = new Set<string>(dependencies.flat());
|
||||
await parallelOrSequence(Array.from(ids, id => async () => {
|
||||
try {
|
||||
await rateLimiter.removeTokens(1);
|
||||
const extension = await apiFilter.findLatestCompatibleExtension({
|
||||
extensionId: id,
|
||||
includeAllVersions: true,
|
||||
targetPlatform
|
||||
});
|
||||
const version = extension?.version;
|
||||
const downloadUrl = extension?.files.download;
|
||||
if (downloadUrl) {
|
||||
await rateLimiter.removeTokens(1);
|
||||
await downloadPlugin({ id, downloadUrl, version });
|
||||
} else {
|
||||
failures.push(`No download url for extension pack ${id} (${version})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
failures.push(err.message);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
console.warn('--- collecting extension-packs ---');
|
||||
const extensionPacks = await collectExtensionPacks(pluginsDir, excludedIds);
|
||||
if (extensionPacks.size > 0) {
|
||||
console.warn(`--- resolving ${extensionPacks.size} extension-packs ---`);
|
||||
await handleDependencyList(Array.from(extensionPacks.values()));
|
||||
}
|
||||
|
||||
console.warn('--- collecting extension dependencies ---');
|
||||
const pluginDependencies = await collectPluginDependencies(pluginsDir, excludedIds);
|
||||
if (pluginDependencies.length > 0) {
|
||||
console.warn(`--- resolving ${pluginDependencies.length} extension dependencies ---`);
|
||||
await handleDependencyList(pluginDependencies);
|
||||
}
|
||||
|
||||
} finally {
|
||||
temp.cleanupSync();
|
||||
}
|
||||
for (const failure of failures) {
|
||||
console.error(failure);
|
||||
}
|
||||
if (!ignoreErrors && failures.length > 0) {
|
||||
throw new Error('Errors downloading some plugins. To make these errors non fatal, re-run with --ignore-errors');
|
||||
}
|
||||
}
|
||||
|
||||
const targetPlatform = `${process.platform}-${process.arch}` as VSXTargetPlatform;
|
||||
|
||||
const placeholders: Record<string, string> = {
|
||||
targetPlatform
|
||||
};
|
||||
function resolveDownloadUrlPlaceholders(url: string): string {
|
||||
for (const [name, value] of Object.entries(placeholders)) {
|
||||
url = url.replace(new RegExp(escapeStringRegexp(`\${${name}}`), 'g'), value);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a plugin, will make multiple attempts before actually failing.
|
||||
* @param requestService
|
||||
* @param failures reference to an array storing all failures.
|
||||
* @param plugin plugin short name.
|
||||
* @param pluginUrl url to download the plugin at.
|
||||
* @param target where to download the plugin in.
|
||||
* @param packed whether to decompress or not.
|
||||
*/
|
||||
async function downloadPluginAsync(
|
||||
requestService: RequestService,
|
||||
rateLimiter: RateLimiter,
|
||||
failures: string[],
|
||||
plugin: string,
|
||||
pluginUrl: string,
|
||||
pluginsDir: string,
|
||||
packed: boolean,
|
||||
excludedIds: Set<string>,
|
||||
version?: string
|
||||
): Promise<void> {
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
let fileExt: string;
|
||||
if (pluginUrl.endsWith('tar.gz')) {
|
||||
fileExt = '.tar.gz';
|
||||
} else if (pluginUrl.endsWith('vsix')) {
|
||||
fileExt = '.vsix';
|
||||
} else if (pluginUrl.endsWith('theia')) {
|
||||
fileExt = '.theia'; // theia plugins.
|
||||
} else {
|
||||
failures.push(chalk.red(`error: '${plugin}' has an unsupported file type: '${pluginUrl}'`));
|
||||
return;
|
||||
}
|
||||
const targetPath = path.resolve(pluginsDir, `${plugin}${packed === true ? fileExt : ''}`);
|
||||
|
||||
// Skip plugins which have previously been downloaded.
|
||||
if (await isDownloaded(targetPath)) {
|
||||
console.warn('- ' + plugin + ': already downloaded - skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxAttempts = 5;
|
||||
const retryDelay = 2000;
|
||||
|
||||
let attempts: number;
|
||||
let lastError: Error | undefined;
|
||||
let response: RequestContext | undefined;
|
||||
|
||||
for (attempts = 0; attempts < maxAttempts; attempts++) {
|
||||
if (attempts > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
lastError = undefined;
|
||||
try {
|
||||
await rateLimiter.removeTokens(1);
|
||||
response = await requestService.request({
|
||||
url: pluginUrl
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
continue;
|
||||
}
|
||||
const status = response.res.statusCode;
|
||||
const retry = status && (status === 429 || status === 439 || status >= 500);
|
||||
if (!retry) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
failures.push(chalk.red(`x ${plugin}: failed to download, last error:\n ${lastError}`));
|
||||
return;
|
||||
}
|
||||
if (typeof response === 'undefined') {
|
||||
failures.push(chalk.red(`x ${plugin}: failed to download (unknown reason)`));
|
||||
return;
|
||||
}
|
||||
if (response.res.statusCode !== 200) {
|
||||
failures.push(chalk.red(`x ${plugin}: failed to download with: ${response.res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((fileExt === '.vsix' || fileExt === '.theia')) {
|
||||
if (packed) {
|
||||
// Download .vsix without decompressing.
|
||||
await fs.writeFile(targetPath, response.buffer);
|
||||
} else {
|
||||
await decompressVsix(targetPath, response.buffer);
|
||||
}
|
||||
console.warn(chalk.green(`+ ${plugin}${version ? `@${version}` : ''}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`));
|
||||
} else if (fileExt === '.tar.gz') {
|
||||
// Write the downloaded tar.gz to a temporary file for decompression.
|
||||
const tempFile = temp.path('theia-plugin-download');
|
||||
await fs.writeFile(tempFile, response.buffer);
|
||||
// Decompress to inspect archive contents and determine handling strategy.
|
||||
const files = await decompress(tempFile);
|
||||
// Check if the archive is a bundle containing only .vsix files.
|
||||
const allVsix = files.length > 0 && files.every(file => file.path.endsWith('.vsix'));
|
||||
|
||||
if (allVsix) {
|
||||
// Handle pure vsix bundle: process each vsix individually.
|
||||
for (const file of files) {
|
||||
const vsixName = path.basename(file.path);
|
||||
const pluginId = vsixName.replace(/\.vsix$/, '');
|
||||
if (excludedIds.has(pluginId)) {
|
||||
console.log(chalk.yellow(`'${pluginId}' referred to by '${plugin}' (tar.gz) is excluded because of 'theiaPluginsExcludeIds'`));
|
||||
continue;
|
||||
}
|
||||
const vsixTargetPath = packed
|
||||
? path.join(pluginsDir, vsixName)
|
||||
: path.join(pluginsDir, pluginId);
|
||||
if (await isDownloaded(vsixTargetPath)) {
|
||||
console.warn('- ' + pluginId + ': already downloaded - skipping');
|
||||
continue;
|
||||
}
|
||||
if (packed) {
|
||||
// Download .vsix without decompressing.
|
||||
await fs.writeFile(vsixTargetPath, file.data);
|
||||
} else {
|
||||
await decompressVsix(vsixTargetPath, file.data);
|
||||
}
|
||||
|
||||
console.warn(chalk.green(`+ ${pluginId}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`));
|
||||
}
|
||||
} else {
|
||||
// Handle regular tar.gz: decompress directly to target directory.
|
||||
await fs.mkdir(targetPath, { recursive: true });
|
||||
await decompress(tempFile, targetPath);
|
||||
console.warn(chalk.green(`+ ${plugin}${version ? `@${version}` : ''}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`));
|
||||
}
|
||||
await fs.unlink(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompresses a VSIX plugin archive to a target directory.
|
||||
*
|
||||
* Creates the target directory if it doesn't exist, writes the buffer content
|
||||
* to a temporary file, and then extracts the archive contents to the target path.
|
||||
*
|
||||
* @param targetPath the directory path where the VSIX contents will be extracted.
|
||||
* @param buffer the VSIX file content as a binary buffer or string.
|
||||
*/
|
||||
async function decompressVsix(targetPath: string, buffer: Uint8Array | string): Promise<void> {
|
||||
await fs.mkdir(targetPath, { recursive: true });
|
||||
const tempFile = temp.path('theia-plugin-download');
|
||||
await fs.writeFile(tempFile, buffer);
|
||||
await decompress(tempFile, targetPath);
|
||||
await fs.unlink(tempFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the resource for the given path is already downloaded.
|
||||
* @param filePath the resource path.
|
||||
*
|
||||
* @returns `true` if the resource is already downloaded, else `false`.
|
||||
*/
|
||||
async function isDownloaded(filePath: string): Promise<boolean> {
|
||||
return fs.stat(filePath).then(() => true, () => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the plugin directory and collect available extension paths.
|
||||
* @param pluginDir the plugin directory.
|
||||
* @returns the list of all available extension paths.
|
||||
*/
|
||||
async function collectPackageJsonPaths(pluginDir: string): Promise<string[]> {
|
||||
const packageJsonPathList: string[] = [];
|
||||
const files = await fs.readdir(pluginDir);
|
||||
// Recursively fetch the list of extension `package.json` files.
|
||||
for (const file of files) {
|
||||
const filePath = path.join(pluginDir, file);
|
||||
if ((await fs.stat(filePath)).isDirectory()) {
|
||||
packageJsonPathList.push(...await collectPackageJsonPaths(filePath));
|
||||
} else if (path.basename(filePath) === 'package.json' && !path.dirname(filePath).includes('node_modules')) {
|
||||
packageJsonPathList.push(filePath);
|
||||
}
|
||||
}
|
||||
return packageJsonPathList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping of extension-pack paths and their included plugin ids.
|
||||
* - If an extension-pack references an explicitly excluded `id` the `id` will be omitted.
|
||||
* @param pluginDir the plugin directory.
|
||||
* @param excludedIds the list of plugin ids to exclude.
|
||||
* @returns the mapping of extension-pack paths and their included plugin ids.
|
||||
*/
|
||||
async function collectExtensionPacks(pluginDir: string, excludedIds: Set<string>): Promise<Map<string, string[]>> {
|
||||
const extensionPackPaths = new Map<string, string[]>();
|
||||
const packageJsonPaths = await collectPackageJsonPaths(pluginDir);
|
||||
await Promise.all(packageJsonPaths.map(async packageJsonPath => {
|
||||
const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
const extensionPack: unknown = json.extensionPack;
|
||||
if (Array.isArray(extensionPack)) {
|
||||
extensionPackPaths.set(packageJsonPath, extensionPack.filter(id => {
|
||||
if (excludedIds.has(id)) {
|
||||
console.log(chalk.yellow(`'${id}' referred to by '${json.name}' (ext pack) is excluded because of 'theiaPluginsExcludeIds'`));
|
||||
return false; // remove
|
||||
}
|
||||
return true; // keep
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return extensionPackPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping of paths and their included plugin ids.
|
||||
* - If an extension-pack references an explicitly excluded `id` the `id` will be omitted.
|
||||
* @param pluginDir the plugin directory.
|
||||
* @param excludedIds the list of plugin ids to exclude.
|
||||
* @returns the mapping of extension-pack paths and their included plugin ids.
|
||||
*/
|
||||
async function collectPluginDependencies(pluginDir: string, excludedIds: Set<string>): Promise<string[]> {
|
||||
const dependencyIds: string[] = [];
|
||||
const packageJsonPaths = await collectPackageJsonPaths(pluginDir);
|
||||
await Promise.all(packageJsonPaths.map(async packageJsonPath => {
|
||||
const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
const extensionDependencies: unknown = json.extensionDependencies;
|
||||
if (Array.isArray(extensionDependencies)) {
|
||||
for (const dependency of extensionDependencies) {
|
||||
if (excludedIds.has(dependency)) {
|
||||
console.log(chalk.yellow(`'${dependency}' referred to by '${json.name}' is excluded because of 'theiaPluginsExcludeIds'`));
|
||||
} else {
|
||||
dependencyIds.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
return dependencyIds;
|
||||
}
|
||||
88
dev-packages/cli/src/run-test.ts
Normal file
88
dev-packages/cli/src/run-test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as net from 'net';
|
||||
import * as puppeteer from 'puppeteer-core';
|
||||
import newTestPage, { TestFileOptions } from './test-page';
|
||||
|
||||
export interface TestOptions {
|
||||
start: () => Promise<net.AddressInfo>
|
||||
launch?: puppeteer.PuppeteerLaunchOptions
|
||||
files?: Partial<TestFileOptions>
|
||||
coverage?: boolean
|
||||
}
|
||||
|
||||
export default async function runTest(options: TestOptions): Promise<void> {
|
||||
const { start, launch } = options;
|
||||
const exit = !(launch && launch.devtools);
|
||||
|
||||
const testPage = await newTestPage({
|
||||
files: options.files,
|
||||
matchAppUrl: () => true, // all urls are application urls
|
||||
newPage: async () => {
|
||||
const browser = await puppeteer.launch(launch);
|
||||
// re-use empty tab
|
||||
const [tab] = await browser.pages();
|
||||
return tab;
|
||||
},
|
||||
onWillRun: async () => {
|
||||
const promises = [];
|
||||
if (options.coverage) {
|
||||
promises.push(testPage.coverage.startJSCoverage());
|
||||
promises.push(testPage.coverage.startCSSCoverage());
|
||||
}
|
||||
// When launching in non-headless mode (with a UI and dev-tools open), make sure
|
||||
// the app has focus, to avoid failures of tests that query the UI's state.
|
||||
if (launch && launch.devtools) {
|
||||
promises.push(testPage.waitForSelector('#theia-app-shell.lm-Widget.theia-ApplicationShell')
|
||||
.then(e => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
if (e !== null) {
|
||||
e.click();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Clear application's local storage to avoid reusing previous state
|
||||
promises.push(testPage.evaluate(() => localStorage.clear()));
|
||||
await Promise.all(promises);
|
||||
},
|
||||
onDidRun: async failures => {
|
||||
if (options.coverage) {
|
||||
console.log('collecting test coverage...');
|
||||
const [jsCoverage, cssCoverage] = await Promise.all([
|
||||
testPage.coverage.stopJSCoverage(),
|
||||
testPage.coverage.stopCSSCoverage(),
|
||||
]);
|
||||
require('puppeteer-to-istanbul').write([...jsCoverage, ...cssCoverage]);
|
||||
}
|
||||
if (exit) {
|
||||
// allow a bit of time to finish printing-out test results
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await testPage.close();
|
||||
process.exit(failures > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
const { address, port } = await start();
|
||||
const url = net.isIPv6(address)
|
||||
? `http://[${address}]:${port}`
|
||||
: `http://${address}:${port}`;
|
||||
await testPage.goto(url);
|
||||
await testPage.bringToFront();
|
||||
}
|
||||
138
dev-packages/cli/src/test-page.ts
Normal file
138
dev-packages/cli/src/test-page.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as puppeteer from 'puppeteer-core';
|
||||
const collectFiles: (options: TestFileOptions) => { files: string[] } = require('mocha/lib/cli/collect-files');
|
||||
|
||||
export interface TestFileOptions {
|
||||
ignore: string[]
|
||||
extension: string[]
|
||||
file: string[]
|
||||
recursive: boolean
|
||||
sort: boolean
|
||||
spec: string[]
|
||||
}
|
||||
|
||||
export interface TestPageOptions {
|
||||
files?: Partial<TestFileOptions>
|
||||
newPage: () => Promise<puppeteer.Page>
|
||||
matchAppUrl?: (url: string) => boolean
|
||||
onWillRun?: () => Promise<void>
|
||||
onDidRun?: (failures: number) => Promise<void>
|
||||
}
|
||||
|
||||
export default async function newTestPage(options: TestPageOptions): Promise<puppeteer.Page> {
|
||||
const { newPage, matchAppUrl, onWillRun, onDidRun } = options;
|
||||
|
||||
const fileOptions: TestFileOptions = {
|
||||
ignore: options.files && options.files.ignore || [],
|
||||
extension: options.files && options.files.extension || [],
|
||||
file: options.files && options.files.file || [],
|
||||
spec: options.files && options.files.spec || [],
|
||||
recursive: options.files && options.files.recursive || false,
|
||||
sort: options.files && options.files.sort || false
|
||||
};
|
||||
|
||||
// quick check whether test files exist
|
||||
const files = collectFiles(fileOptions);
|
||||
|
||||
const page = await newPage();
|
||||
page.on('dialog', dialog => dialog.dismiss());
|
||||
page.on('pageerror', console.error);
|
||||
|
||||
let theiaLoaded = false;
|
||||
page.exposeFunction('fireDidUnloadTheia', () => theiaLoaded = false);
|
||||
const preLoad = (frame: puppeteer.Frame) => {
|
||||
const frameUrl = frame.url();
|
||||
if (matchAppUrl && !matchAppUrl(frameUrl)) {
|
||||
return;
|
||||
}
|
||||
if (theiaLoaded) {
|
||||
return;
|
||||
}
|
||||
console.log('loading chai...');
|
||||
theiaLoaded = true;
|
||||
page.addScriptTag({ path: require.resolve('chai/chai.js') });
|
||||
page.evaluate(() =>
|
||||
window.addEventListener('beforeunload', () => (window as any)['fireDidUnloadTheia']())
|
||||
);
|
||||
};
|
||||
page.on('frameattached', preLoad);
|
||||
page.on('framenavigated', preLoad);
|
||||
|
||||
page.on('load', async () => {
|
||||
if (matchAppUrl && !matchAppUrl(page.url())) {
|
||||
return;
|
||||
}
|
||||
console.log('loading mocha...');
|
||||
// replace console.log by theia logger for mocha
|
||||
await page.waitForFunction(() => !!(window as any)['theia']?.['@theia/core/lib/common/logger']?.logger, {
|
||||
timeout: 30 * 1000
|
||||
});
|
||||
await page.addScriptTag({ path: require.resolve('mocha/mocha.js') });
|
||||
await page.waitForFunction(() => !!(window as any)['chai'] && !!(window as any)['mocha'] && !!(window as any)['theia'].container, { timeout: 30 * 1000 });
|
||||
|
||||
console.log('loading Theia...');
|
||||
await page.evaluate(() => {
|
||||
const { FrontendApplicationStateService } = (window as any)['theia']['@theia/core/lib/browser/frontend-application-state'];
|
||||
const { PreferenceService } = (window as any)['theia']['@theia/core/lib/common/preferences/preference-service'];
|
||||
const { WorkspaceService } = (window as any)['theia']['@theia/workspace/lib/browser/workspace-service'];
|
||||
|
||||
const container = (window as any)['theia'].container;
|
||||
const frontendApplicationState = container.get(FrontendApplicationStateService);
|
||||
const preferenceService = container.get(PreferenceService);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
|
||||
return Promise.all([
|
||||
frontendApplicationState.reachedState('ready'),
|
||||
preferenceService.ready,
|
||||
workspaceService.roots
|
||||
]);
|
||||
});
|
||||
|
||||
console.log('loading test files...');
|
||||
await page.evaluate(() => {
|
||||
// replace require to load modules from theia namespace
|
||||
(window as any)['require'] = (moduleName: string) => (window as any)['theia'][moduleName];
|
||||
mocha.setup({
|
||||
reporter: 'spec',
|
||||
ui: 'bdd',
|
||||
color: true,
|
||||
retries: 0,
|
||||
timeout: 10000
|
||||
});
|
||||
});
|
||||
|
||||
if (onWillRun) {
|
||||
await onWillRun();
|
||||
}
|
||||
|
||||
for (const file of files.files) {
|
||||
await page.addScriptTag({ path: file });
|
||||
}
|
||||
|
||||
console.log('running test files...');
|
||||
const failures = await page.evaluate(() =>
|
||||
new Promise<number>(resolve => mocha.run(resolve))
|
||||
);
|
||||
if (onDidRun) {
|
||||
await onDidRun(failures);
|
||||
}
|
||||
});
|
||||
return page;
|
||||
}
|
||||
704
dev-packages/cli/src/theia.ts
Normal file
704
dev-packages/cli/src/theia.ts
Normal file
@@ -0,0 +1,704 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as temp from 'temp';
|
||||
import * as yargs from 'yargs';
|
||||
import yargsFactory = require('yargs/yargs');
|
||||
import { ApplicationPackageManager, rebuild } from '@theia/application-manager';
|
||||
import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package';
|
||||
import checkDependencies from './check-dependencies';
|
||||
import downloadPlugins from './download-plugins';
|
||||
import runTest from './run-test';
|
||||
import { RateLimiter } from 'limiter';
|
||||
import { LocalizationManager, extract } from '@theia/localization-manager';
|
||||
import { NodeRequestService } from '@theia/request/lib/node-request-service';
|
||||
import { ExtensionIdMatchesFilterFactory, OVSX_RATE_LIMIT, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
|
||||
|
||||
const { executablePath } = require('puppeteer');
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
throw reason;
|
||||
});
|
||||
process.on('uncaughtException', error => {
|
||||
if (error) {
|
||||
console.error('Uncaught Exception: ', error.toString());
|
||||
if (error.stack) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
theiaCli();
|
||||
|
||||
function toStringArray(argv: (string | number)[]): string[];
|
||||
function toStringArray(argv?: (string | number)[]): string[] | undefined;
|
||||
function toStringArray(argv?: (string | number)[]): string[] | undefined {
|
||||
return argv?.map(arg => String(arg));
|
||||
}
|
||||
|
||||
function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule<unknown, {
|
||||
modules: string[]
|
||||
cacheRoot?: string
|
||||
forceAbi?: number,
|
||||
}> {
|
||||
return {
|
||||
command,
|
||||
describe: `Rebuild/revert native node modules for "${target}"`,
|
||||
builder: {
|
||||
'cacheRoot': {
|
||||
type: 'string',
|
||||
describe: 'Root folder where to store the .browser_modules cache'
|
||||
},
|
||||
'modules': {
|
||||
alias: 'm',
|
||||
type: 'array', // === `--modules/-m` can be specified multiple times
|
||||
describe: 'List of modules to rebuild/revert'
|
||||
},
|
||||
'forceAbi': {
|
||||
type: 'number',
|
||||
describe: 'The Node ABI version to rebuild for'
|
||||
}
|
||||
},
|
||||
handler: ({ cacheRoot, modules, forceAbi }) => {
|
||||
// Note: `modules` is actually `string[] | undefined`.
|
||||
if (modules) {
|
||||
// It is ergonomic to pass arguments as --modules="a,b,c,..."
|
||||
// but yargs doesn't parse it this way by default.
|
||||
const flattened: string[] = [];
|
||||
for (const value of modules) {
|
||||
if (value.includes(',')) {
|
||||
flattened.push(...value.split(',').map(mod => mod.trim()));
|
||||
} else {
|
||||
flattened.push(value);
|
||||
}
|
||||
}
|
||||
modules = flattened;
|
||||
}
|
||||
rebuild(target, { cacheRoot, modules, forceAbi });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function defineCommonOptions<T>(cli: yargs.Argv<T>): yargs.Argv<T & {
|
||||
appTarget?: 'browser' | 'electron' | 'browser-only'
|
||||
}> {
|
||||
return cli
|
||||
.option('app-target', {
|
||||
description: 'The target application type. Overrides `theia.target` in the application\'s package.json',
|
||||
choices: ['browser', 'electron', 'browser-only'] as const,
|
||||
});
|
||||
}
|
||||
|
||||
async function theiaCli(): Promise<void> {
|
||||
const { version } = await fs.promises.readFile(path.join(__dirname, '../package.json'), 'utf8').then(JSON.parse);
|
||||
yargs.scriptName('theia').version(version);
|
||||
const projectPath = process.cwd();
|
||||
// Create a sub `yargs` parser to read `app-target` without
|
||||
// affecting the global `yargs` instance used by the CLI.
|
||||
const { appTarget } = defineCommonOptions(yargsFactory()).help(false).parse();
|
||||
const manager = new ApplicationPackageManager({ projectPath, appTarget });
|
||||
const localizationManager = new LocalizationManager();
|
||||
const { target } = manager.pck;
|
||||
defineCommonOptions(yargs)
|
||||
.command<{
|
||||
theiaArgs?: (string | number)[]
|
||||
}>({
|
||||
command: 'start [theia-args...]',
|
||||
describe: `Start the ${target} backend`,
|
||||
// Disable this command's `--help` option so that it is forwarded to Theia's CLI
|
||||
builder: cli => cli.help(false) as yargs.Argv,
|
||||
handler: async ({ theiaArgs }) => {
|
||||
manager.start(toStringArray(theiaArgs));
|
||||
}
|
||||
})
|
||||
.command({
|
||||
command: 'clean',
|
||||
describe: `Clean for the ${target} target`,
|
||||
handler: async () => {
|
||||
await manager.clean();
|
||||
}
|
||||
})
|
||||
.command({
|
||||
command: 'copy',
|
||||
describe: 'Copy various files from `src-gen` to `lib`',
|
||||
handler: async () => {
|
||||
await manager.copy();
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
mode: 'development' | 'production',
|
||||
splitFrontend?: boolean
|
||||
}>({
|
||||
command: 'generate',
|
||||
describe: `Generate various files for the ${target} target`,
|
||||
builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli),
|
||||
handler: async ({ mode, splitFrontend }) => {
|
||||
await manager.generate({ mode, splitFrontend });
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
mode: 'development' | 'production',
|
||||
webpackHelp: boolean
|
||||
splitFrontend?: boolean
|
||||
webpackArgs?: (string | number)[]
|
||||
}>({
|
||||
command: 'build [webpack-args...]',
|
||||
describe: `Generate and bundle the ${target} frontend using webpack`,
|
||||
builder: cli => ApplicationPackageManager.defineGeneratorOptions(cli)
|
||||
.option('webpack-help' as 'webpackHelp', {
|
||||
boolean: true,
|
||||
description: 'Display Webpack\'s help',
|
||||
default: false
|
||||
}),
|
||||
handler: async ({ mode, splitFrontend, webpackHelp, webpackArgs = [] }) => {
|
||||
await manager.build(
|
||||
webpackHelp
|
||||
? ['--help']
|
||||
: [
|
||||
// Forward the `mode` argument to Webpack too:
|
||||
'--mode', mode,
|
||||
...toStringArray(webpackArgs)
|
||||
],
|
||||
{ mode, splitFrontend }
|
||||
);
|
||||
}
|
||||
})
|
||||
.command(rebuildCommand('rebuild', target))
|
||||
.command(rebuildCommand('rebuild:browser', 'browser'))
|
||||
.command(rebuildCommand('rebuild:electron', 'electron'))
|
||||
.command<{
|
||||
suppress: boolean
|
||||
}>({
|
||||
command: 'check:hoisted',
|
||||
describe: 'Check that all dependencies are hoisted',
|
||||
builder: {
|
||||
'suppress': {
|
||||
alias: 's',
|
||||
describe: 'Suppress exiting with failure code',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: ({ suppress }) => {
|
||||
checkDependencies({
|
||||
workspaces: ['packages/*'],
|
||||
include: ['**'],
|
||||
exclude: ['.bin/**', '.cache/**'],
|
||||
skipHoisted: false,
|
||||
skipUniqueness: true,
|
||||
skipSingleTheiaVersion: true,
|
||||
onlyTheiaExtensions: false,
|
||||
suppress
|
||||
});
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
suppress: boolean
|
||||
}>({
|
||||
command: 'check:theia-version',
|
||||
describe: 'Check that all dependencies have been resolved to the same Theia version',
|
||||
builder: {
|
||||
'suppress': {
|
||||
alias: 's',
|
||||
describe: 'Suppress exiting with failure code',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: ({ suppress }) => {
|
||||
checkDependencies({
|
||||
workspaces: undefined,
|
||||
include: ['@theia/**'],
|
||||
exclude: [],
|
||||
skipHoisted: true,
|
||||
skipUniqueness: false,
|
||||
skipSingleTheiaVersion: false,
|
||||
onlyTheiaExtensions: false,
|
||||
suppress
|
||||
});
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
suppress: boolean
|
||||
}>({
|
||||
command: 'check:theia-extensions',
|
||||
describe: 'Check uniqueness of Theia extension versions or whether they are hoisted',
|
||||
builder: {
|
||||
'suppress': {
|
||||
alias: 's',
|
||||
describe: 'Suppress exiting with failure code',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: ({ suppress }) => {
|
||||
checkDependencies({
|
||||
workspaces: undefined,
|
||||
include: ['**'],
|
||||
exclude: [],
|
||||
skipHoisted: true,
|
||||
skipUniqueness: false,
|
||||
skipSingleTheiaVersion: true,
|
||||
onlyTheiaExtensions: true,
|
||||
suppress
|
||||
});
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
workspaces: string[] | undefined,
|
||||
include: string[],
|
||||
exclude: string[],
|
||||
skipHoisted: boolean,
|
||||
skipUniqueness: boolean,
|
||||
skipSingleTheiaVersion: boolean,
|
||||
onlyTheiaExtensions: boolean,
|
||||
suppress: boolean
|
||||
}>({
|
||||
command: 'check:dependencies',
|
||||
describe: 'Check uniqueness of dependency versions or whether they are hoisted',
|
||||
builder: {
|
||||
'workspaces': {
|
||||
alias: 'w',
|
||||
describe: 'Glob patterns of workspaces to analyze, relative to `cwd`',
|
||||
array: true,
|
||||
defaultDescription: 'All glob patterns listed in the package.json\'s workspaces',
|
||||
demandOption: false
|
||||
},
|
||||
'include': {
|
||||
alias: 'i',
|
||||
describe: 'Glob pattern of dependencies\' package names to be included, e.g. -i "@theia/**"',
|
||||
array: true,
|
||||
default: ['**']
|
||||
},
|
||||
'exclude': {
|
||||
alias: 'e',
|
||||
describe: 'Glob pattern of dependencies\' package names to be excluded',
|
||||
array: true,
|
||||
defaultDescription: 'None',
|
||||
default: []
|
||||
},
|
||||
'skip-hoisted': {
|
||||
alias: 'h',
|
||||
describe: 'Skip checking whether dependencies are hoisted',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'skip-uniqueness': {
|
||||
alias: 'u',
|
||||
describe: 'Skip checking whether all dependencies are resolved to a unique version',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'skip-single-theia-version': {
|
||||
alias: 't',
|
||||
describe: 'Skip checking whether all @theia/* dependencies are resolved to a single version',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'only-theia-extensions': {
|
||||
alias: 'o',
|
||||
describe: 'Only check dependencies which are Theia extensions',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'suppress': {
|
||||
alias: 's',
|
||||
describe: 'Suppress exiting with failure code',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: ({
|
||||
workspaces,
|
||||
include,
|
||||
exclude,
|
||||
skipHoisted,
|
||||
skipUniqueness,
|
||||
skipSingleTheiaVersion,
|
||||
onlyTheiaExtensions,
|
||||
suppress
|
||||
}) => {
|
||||
checkDependencies({
|
||||
workspaces,
|
||||
include,
|
||||
exclude,
|
||||
skipHoisted,
|
||||
skipUniqueness,
|
||||
skipSingleTheiaVersion,
|
||||
onlyTheiaExtensions,
|
||||
suppress
|
||||
});
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
packed: boolean
|
||||
ignoreErrors: boolean
|
||||
apiVersion: string
|
||||
apiUrl: string
|
||||
parallel: boolean
|
||||
proxyUrl?: string
|
||||
proxyAuthorization?: string
|
||||
strictSsl: boolean
|
||||
rateLimit: number
|
||||
ovsxRouterConfig?: string
|
||||
}>({
|
||||
command: 'download:plugins',
|
||||
describe: 'Download defined external plugins',
|
||||
builder: {
|
||||
'packed': {
|
||||
alias: 'p',
|
||||
describe: 'Controls whether to pack or unpack plugins',
|
||||
boolean: true,
|
||||
default: false,
|
||||
},
|
||||
'ignore-errors': {
|
||||
alias: 'i',
|
||||
describe: 'Ignore errors while downloading plugins',
|
||||
boolean: true,
|
||||
default: false,
|
||||
},
|
||||
'api-version': {
|
||||
alias: 'v',
|
||||
describe: 'Supported API version for plugins',
|
||||
default: DEFAULT_SUPPORTED_API_VERSION
|
||||
},
|
||||
'api-url': {
|
||||
alias: 'u',
|
||||
describe: 'Open-VSX Registry API URL',
|
||||
default: 'https://open-vsx.org/api'
|
||||
},
|
||||
'parallel': {
|
||||
describe: 'Download in parallel',
|
||||
boolean: true,
|
||||
default: true
|
||||
},
|
||||
'rate-limit': {
|
||||
describe: 'Amount of maximum open-vsx requests per second',
|
||||
number: true,
|
||||
default: OVSX_RATE_LIMIT
|
||||
},
|
||||
'proxy-url': {
|
||||
describe: 'Proxy URL'
|
||||
},
|
||||
'proxy-authorization': {
|
||||
describe: 'Proxy authorization information'
|
||||
},
|
||||
'strict-ssl': {
|
||||
describe: 'Whether to enable strict SSL mode',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'ovsx-router-config': {
|
||||
describe: 'JSON configuration file for the OVSX router client',
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
handler: async ({ apiUrl, proxyUrl, proxyAuthorization, strictSsl, ovsxRouterConfig, ...options }) => {
|
||||
const requestService = new NodeRequestService();
|
||||
await requestService.configure({
|
||||
proxyUrl,
|
||||
proxyAuthorization,
|
||||
strictSSL: strictSsl
|
||||
});
|
||||
let client: OVSXClient | undefined;
|
||||
const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
|
||||
if (ovsxRouterConfig) {
|
||||
const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
|
||||
console.error(error);
|
||||
});
|
||||
if (routerConfig) {
|
||||
client = await OVSXRouterClient.FromConfig(
|
||||
routerConfig,
|
||||
OVSXHttpClient.createClientFactory(requestService, rateLimiter),
|
||||
[RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!client) {
|
||||
client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
|
||||
}
|
||||
try {
|
||||
await downloadPlugins(client, rateLimiter, requestService, options);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
},
|
||||
})
|
||||
.command<{
|
||||
freeApi?: boolean,
|
||||
deeplKey: string,
|
||||
file: string,
|
||||
languages: string[],
|
||||
sourceLanguage?: string
|
||||
}>({
|
||||
command: 'nls-localize [languages...]',
|
||||
describe: 'Localize json files using the DeepL API',
|
||||
builder: {
|
||||
'file': {
|
||||
alias: 'f',
|
||||
describe: 'The source file which should be translated',
|
||||
demandOption: true
|
||||
},
|
||||
'deepl-key': {
|
||||
alias: 'k',
|
||||
describe: 'DeepL key used for API access. See https://www.deepl.com/docs-api for more information',
|
||||
demandOption: true
|
||||
},
|
||||
'free-api': {
|
||||
describe: 'Indicates whether the specified DeepL API key belongs to the free API',
|
||||
boolean: true,
|
||||
default: false,
|
||||
},
|
||||
'source-language': {
|
||||
alias: 's',
|
||||
describe: 'The source language of the translation file'
|
||||
}
|
||||
},
|
||||
handler: async ({ freeApi, deeplKey, file, sourceLanguage, languages = [] }) => {
|
||||
const success = await localizationManager.localize({
|
||||
sourceFile: file,
|
||||
freeApi: freeApi ?? true,
|
||||
authKey: deeplKey,
|
||||
targetLanguages: languages,
|
||||
sourceLanguage
|
||||
});
|
||||
if (!success) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
root: string,
|
||||
output: string,
|
||||
merge: boolean,
|
||||
exclude?: string,
|
||||
logs?: string,
|
||||
files?: string[],
|
||||
quiet: boolean
|
||||
}>({
|
||||
command: 'nls-extract',
|
||||
describe: 'Extract translation key/value pairs from source code',
|
||||
builder: {
|
||||
'output': {
|
||||
alias: 'o',
|
||||
describe: 'Output file for the extracted translations',
|
||||
demandOption: true
|
||||
},
|
||||
'root': {
|
||||
alias: 'r',
|
||||
describe: 'The directory which contains the source code',
|
||||
default: '.'
|
||||
},
|
||||
'merge': {
|
||||
alias: 'm',
|
||||
describe: 'Whether to merge new with existing translation values',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'exclude': {
|
||||
alias: 'e',
|
||||
describe: 'Allows to exclude translation keys starting with this value'
|
||||
},
|
||||
'files': {
|
||||
alias: 'f',
|
||||
describe: 'Glob pattern matching the files to extract from (starting from --root).',
|
||||
array: true
|
||||
},
|
||||
'logs': {
|
||||
alias: 'l',
|
||||
describe: 'File path to a log file'
|
||||
},
|
||||
'quiet': {
|
||||
alias: 'q',
|
||||
describe: 'Prevents errors from being logged to console',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: async options => {
|
||||
await extract(options);
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
testInspect: boolean,
|
||||
testExtension: string[],
|
||||
testFile: string[],
|
||||
testIgnore: string[],
|
||||
testRecursive: boolean,
|
||||
testSort: boolean,
|
||||
testSpec: string[],
|
||||
testCoverage: boolean
|
||||
theiaArgs?: (string | number)[]
|
||||
}>({
|
||||
command: 'test [theia-args...]',
|
||||
builder: {
|
||||
'test-inspect': {
|
||||
describe: 'Whether to auto-open a DevTools panel for test page.',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'test-extension': {
|
||||
describe: 'Test file extension(s) to load',
|
||||
array: true,
|
||||
default: ['js']
|
||||
},
|
||||
'test-file': {
|
||||
describe: 'Specify test file(s) to be loaded prior to root suite execution',
|
||||
array: true,
|
||||
default: []
|
||||
},
|
||||
'test-ignore': {
|
||||
describe: 'Ignore test file(s) or glob pattern(s)',
|
||||
array: true,
|
||||
default: []
|
||||
},
|
||||
'test-recursive': {
|
||||
describe: 'Look for tests in subdirectories',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'test-sort': {
|
||||
describe: 'Sort test files',
|
||||
boolean: true,
|
||||
default: false
|
||||
},
|
||||
'test-spec': {
|
||||
describe: 'One or more test files, directories, or globs to test',
|
||||
array: true,
|
||||
default: ['test']
|
||||
},
|
||||
'test-coverage': {
|
||||
describe: 'Report test coverage consumable by istanbul',
|
||||
boolean: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
handler: async ({ testInspect, testExtension, testFile, testIgnore, testRecursive, testSort, testSpec, testCoverage, theiaArgs }) => {
|
||||
if (!process.env.THEIA_CONFIG_DIR) {
|
||||
process.env.THEIA_CONFIG_DIR = temp.track().mkdirSync('theia-test-config-dir');
|
||||
}
|
||||
const args = ['--no-sandbox'];
|
||||
if (!testInspect) {
|
||||
args.push('--headless=old');
|
||||
}
|
||||
await runTest({
|
||||
start: () => new Promise((resolve, reject) => {
|
||||
const serverProcess = manager.start(toStringArray(theiaArgs));
|
||||
serverProcess.on('message', resolve);
|
||||
serverProcess.on('error', reject);
|
||||
serverProcess.on('close', (code, signal) => reject(`Server process exited unexpectedly: ${code ?? signal}`));
|
||||
}),
|
||||
launch: {
|
||||
args: args,
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
defaultViewport: null, // view port can take available space instead of 800x600 default
|
||||
devtools: testInspect,
|
||||
headless: testInspect ? false : 'shell',
|
||||
executablePath: executablePath(),
|
||||
protocolTimeout: 600000,
|
||||
timeout: 60000
|
||||
},
|
||||
files: {
|
||||
extension: testExtension,
|
||||
file: testFile,
|
||||
ignore: testIgnore,
|
||||
recursive: testRecursive,
|
||||
sort: testSort,
|
||||
spec: testSpec
|
||||
},
|
||||
coverage: testCoverage
|
||||
});
|
||||
}
|
||||
})
|
||||
.command<{
|
||||
electronVersion?: string
|
||||
electronDist?: string
|
||||
ffmpegPath?: string
|
||||
platform?: NodeJS.Platform
|
||||
}>({
|
||||
command: 'ffmpeg:replace [ffmpeg-path]',
|
||||
describe: '',
|
||||
builder: {
|
||||
'electronDist': {
|
||||
description: 'Electron distribution location.',
|
||||
},
|
||||
'electronVersion': {
|
||||
description: 'Electron version for which to pull the "clean" ffmpeg library.',
|
||||
},
|
||||
'ffmpegPath': {
|
||||
description: 'Absolute path to the ffmpeg shared library.',
|
||||
},
|
||||
'platform': {
|
||||
description: 'Dictates where the library is located within the Electron distribution.',
|
||||
choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
|
||||
},
|
||||
},
|
||||
handler: async options => {
|
||||
const ffmpeg = await import('@theia/ffmpeg');
|
||||
await ffmpeg.replaceFfmpeg(options);
|
||||
},
|
||||
})
|
||||
.command<{
|
||||
electronDist?: string
|
||||
ffmpegPath?: string
|
||||
json?: boolean
|
||||
platform?: NodeJS.Platform
|
||||
}>({
|
||||
command: 'ffmpeg:check [ffmpeg-path]',
|
||||
describe: '(electron-only) Check that ffmpeg doesn\'t contain proprietary codecs',
|
||||
builder: {
|
||||
'electronDist': {
|
||||
description: 'Electron distribution location',
|
||||
},
|
||||
'ffmpegPath': {
|
||||
describe: 'Absolute path to the ffmpeg shared library',
|
||||
},
|
||||
'json': {
|
||||
description: 'Output the found codecs as JSON on stdout',
|
||||
boolean: true,
|
||||
},
|
||||
'platform': {
|
||||
description: 'Dictates where the library is located within the Electron distribution',
|
||||
choices: ['darwin', 'linux', 'win32'] as NodeJS.Platform[],
|
||||
},
|
||||
},
|
||||
handler: async options => {
|
||||
const ffmpeg = await import('@theia/ffmpeg');
|
||||
await ffmpeg.checkFfmpeg(options);
|
||||
},
|
||||
})
|
||||
.parserConfiguration({
|
||||
'unknown-options-as-args': true,
|
||||
})
|
||||
.strictCommands()
|
||||
.demandCommand(1, 'Please run a command')
|
||||
.fail((msg, err, cli) => {
|
||||
process.exitCode = 1;
|
||||
if (err) {
|
||||
// One of the handlers threw an error:
|
||||
console.error(err);
|
||||
} else {
|
||||
// Yargs detected a problem with commands and/or arguments while parsing:
|
||||
cli.showHelp();
|
||||
console.error(msg);
|
||||
}
|
||||
})
|
||||
.parse();
|
||||
}
|
||||
31
dev-packages/cli/tsconfig.json
Normal file
31
dev-packages/cli/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../application-manager"
|
||||
},
|
||||
{
|
||||
"path": "../application-package"
|
||||
},
|
||||
{
|
||||
"path": "../ffmpeg"
|
||||
},
|
||||
{
|
||||
"path": "../localization-manager"
|
||||
},
|
||||
{
|
||||
"path": "../ovsx-client"
|
||||
},
|
||||
{
|
||||
"path": "../request"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user