deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View 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
View 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>

View 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
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/theia')

View 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"
}

File diff suppressed because one or more lines are too long

View 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();
}

View 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;
}

View 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();
}

View 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;
}

View 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();
}

View 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"
}
]
}