deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
13
dev-packages/application-manager/.eslintrc.js
Normal file
13
dev-packages/application-manager/.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'
|
||||
}
|
||||
};
|
||||
26
dev-packages/application-manager/README.md
Normal file
26
dev-packages/application-manager/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
<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 - APPLICATION-MANAGER</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## 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>
|
||||
81
dev-packages/application-manager/package.json
Normal file
81
dev-packages/application-manager/package.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@theia/application-manager",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia application manager API.",
|
||||
"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": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.10.0",
|
||||
"@babel/plugin-transform-classes": "^7.10.0",
|
||||
"@babel/plugin-transform-runtime": "^7.10.0",
|
||||
"@babel/preset-env": "^7.10.0",
|
||||
"@theia/application-package": "1.68.0",
|
||||
"@theia/ffmpeg": "1.68.0",
|
||||
"@theia/native-webpack-plugin": "1.68.0",
|
||||
"@types/fs-extra": "^4.0.2",
|
||||
"@types/semver": "^7.5.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"buffer": "^6.0.3",
|
||||
"compression-webpack-plugin": "^9.0.0",
|
||||
"copy-webpack-plugin": "^8.1.1",
|
||||
"css-loader": "^6.2.0",
|
||||
"@electron/rebuild": "^3.7.2",
|
||||
"fs-extra": "^4.0.2",
|
||||
"http-server": "^14.1.1",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"less": "^3.0.3",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"node-loader": "^2.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"semver": "^7.5.4",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-loader": "^2.0.1",
|
||||
"source-map-support": "^0.5.19",
|
||||
"style-loader": "^2.0.0",
|
||||
"tslib": "^2.6.2",
|
||||
"umd-compat-loader": "^2.1.2",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "4.7.0",
|
||||
"worker-loader": "^3.0.8",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@theia/electron": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@theia/electron": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// *****************************************************************************
|
||||
// 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 path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as cp from 'child_process';
|
||||
import * as semver from 'semver';
|
||||
import { ApplicationPackage, ApplicationPackageOptions } from '@theia/application-package';
|
||||
import { WebpackGenerator, FrontendGenerator, BackendGenerator } from './generator';
|
||||
import { ApplicationProcess } from './application-process';
|
||||
import { GeneratorOptions } from './generator/abstract-generator';
|
||||
import yargs = require('yargs');
|
||||
|
||||
// Declare missing exports from `@types/semver@7`
|
||||
declare module 'semver' {
|
||||
function minVersion(range: string): string;
|
||||
}
|
||||
|
||||
class AbortError extends Error {
|
||||
constructor(...args: Parameters<ErrorConstructor>) {
|
||||
super(...args);
|
||||
Object.setPrototypeOf(this, AbortError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApplicationPackageManager {
|
||||
|
||||
static defineGeneratorOptions<T>(cli: yargs.Argv<T>): yargs.Argv<T & {
|
||||
mode: 'development' | 'production'
|
||||
splitFrontend?: boolean
|
||||
}> {
|
||||
return cli
|
||||
.option('mode', {
|
||||
description: 'Generation mode to use',
|
||||
choices: ['development', 'production'],
|
||||
default: 'production' as const,
|
||||
})
|
||||
.option('split-frontend', {
|
||||
description: 'Split frontend modules into separate chunks. By default enabled in the `development` mode and disabled in the `production` mode.',
|
||||
type: 'boolean'
|
||||
});
|
||||
}
|
||||
|
||||
readonly pck: ApplicationPackage;
|
||||
/** application process */
|
||||
readonly process: ApplicationProcess;
|
||||
/** manager process */
|
||||
protected readonly __process: ApplicationProcess;
|
||||
|
||||
constructor(options: ApplicationPackageOptions) {
|
||||
this.pck = new ApplicationPackage(options);
|
||||
this.process = new ApplicationProcess(this.pck, options.projectPath);
|
||||
this.__process = new ApplicationProcess(this.pck, path.join(__dirname, '..'));
|
||||
}
|
||||
|
||||
protected async remove(fsPath: string): Promise<void> {
|
||||
if (await fs.pathExists(fsPath)) {
|
||||
await fs.remove(fsPath);
|
||||
}
|
||||
}
|
||||
|
||||
async clean(): Promise<void> {
|
||||
const webpackGenerator = new WebpackGenerator(this.pck);
|
||||
await Promise.all([
|
||||
this.remove(this.pck.lib()),
|
||||
this.remove(this.pck.srcGen()),
|
||||
this.remove(webpackGenerator.genConfigPath),
|
||||
this.remove(webpackGenerator.genNodeConfigPath)
|
||||
]);
|
||||
}
|
||||
|
||||
async prepare(): Promise<void> {
|
||||
if (this.pck.isElectron()) {
|
||||
await this.prepareElectron();
|
||||
}
|
||||
}
|
||||
|
||||
async generate(options: GeneratorOptions = {}): Promise<void> {
|
||||
try {
|
||||
await this.prepare();
|
||||
} catch (error) {
|
||||
if (error instanceof AbortError) {
|
||||
console.warn(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await Promise.all([
|
||||
new WebpackGenerator(this.pck, options).generate(),
|
||||
new BackendGenerator(this.pck, options).generate(),
|
||||
new FrontendGenerator(this.pck, options).generate(),
|
||||
]);
|
||||
}
|
||||
|
||||
async copy(): Promise<void> {
|
||||
await fs.ensureDir(this.pck.lib('frontend'));
|
||||
await fs.copy(this.pck.frontend('index.html'), this.pck.lib('frontend', 'index.html'));
|
||||
}
|
||||
|
||||
async build(args: string[] = [], options: GeneratorOptions = {}): Promise<void> {
|
||||
await this.generate(options);
|
||||
await this.copy();
|
||||
return this.__process.run('webpack', args);
|
||||
}
|
||||
|
||||
start(args: string[] = []): cp.ChildProcess {
|
||||
if (this.pck.isElectron()) {
|
||||
return this.startElectron(args);
|
||||
} else if (this.pck.isBrowserOnly()) {
|
||||
return this.startBrowserOnly(args);
|
||||
}
|
||||
return this.startBrowser(args);
|
||||
}
|
||||
|
||||
startBrowserOnly(args: string[]): cp.ChildProcess {
|
||||
const { command, mainArgs, options } = this.adjustBrowserOnlyArgs(args);
|
||||
return this.__process.spawnBin(command, mainArgs, options);
|
||||
}
|
||||
|
||||
adjustBrowserOnlyArgs(args: string[]): Readonly<{ command: string, mainArgs: string[]; options: cp.SpawnOptions }> {
|
||||
let { mainArgs, options } = this.adjustArgs(args);
|
||||
|
||||
// first parameter: path to generated frontend
|
||||
// second parameter: disable cache to support watching
|
||||
mainArgs = ['lib/frontend', '-c-1', ...mainArgs];
|
||||
|
||||
const portIndex = mainArgs.findIndex(v => v.startsWith('--port'));
|
||||
if (portIndex === -1) {
|
||||
mainArgs.push('--port=3000');
|
||||
}
|
||||
|
||||
return { command: 'http-server', mainArgs, options };
|
||||
}
|
||||
|
||||
startElectron(args: string[]): cp.ChildProcess {
|
||||
// If possible, pass the project root directory to electron rather than the script file so that Electron
|
||||
// can determine the app name. This requires that the package.json has a main field.
|
||||
let appPath = this.pck.projectPath;
|
||||
|
||||
if (!this.pck.pck.main) {
|
||||
// Try the bundled electron app first
|
||||
appPath = this.pck.lib('backend', 'electron-main.js');
|
||||
if (!fs.existsSync(appPath)) {
|
||||
// Fallback to the generated electron app in src-gen
|
||||
appPath = this.pck.backend('electron-main.js');
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`WARNING: ${this.pck.packagePath} does not have a "main" entry.\n` +
|
||||
'Please add the following line:\n' +
|
||||
' "main": "lib/backend/electron-main.js"'
|
||||
);
|
||||
}
|
||||
|
||||
const { mainArgs, options } = this.adjustArgs([appPath, ...args]);
|
||||
const electronCli = require.resolve('electron/cli.js', { paths: [this.pck.projectPath] });
|
||||
return this.__process.fork(electronCli, mainArgs, options);
|
||||
}
|
||||
|
||||
startBrowser(args: string[]): cp.ChildProcess {
|
||||
const { mainArgs, options } = this.adjustArgs(args);
|
||||
// The backend must be a process group leader on UNIX in order to kill the tree later.
|
||||
// See https://nodejs.org/api/child_process.html#child_process_options_detached
|
||||
options.detached = process.platform !== 'win32';
|
||||
// Try the bundled backend app first
|
||||
let mainPath = this.pck.lib('backend', 'main.js');
|
||||
if (!fs.existsSync(mainPath)) {
|
||||
// Fallback to the generated backend file in src-gen
|
||||
mainPath = this.pck.backend('main.js');
|
||||
}
|
||||
return this.__process.fork(mainPath, mainArgs, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Theia's Electron-specific dependencies into the application's package.json.
|
||||
*
|
||||
* Only overwrite the Electron range if the current minimum supported version is lower than the recommended one.
|
||||
*/
|
||||
protected async prepareElectron(): Promise<void> {
|
||||
let theiaElectron;
|
||||
try {
|
||||
theiaElectron = await import('@theia/electron');
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
throw new AbortError('Please install @theia/electron as part of your Theia Electron application');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const expectedRange = theiaElectron.electronRange;
|
||||
const appPackageJsonPath = this.pck.path('package.json');
|
||||
const appPackageJson = await fs.readJSON(appPackageJsonPath) as { devDependencies?: Record<string, string> };
|
||||
if (!appPackageJson.devDependencies) {
|
||||
appPackageJson.devDependencies = {};
|
||||
}
|
||||
const currentRange: string | undefined = appPackageJson.devDependencies.electron;
|
||||
if (!currentRange || semver.compare(semver.minVersion(currentRange), semver.minVersion(expectedRange)) < 0) {
|
||||
// Update the range with the recommended one and write it on disk.
|
||||
appPackageJson.devDependencies = this.insertAlphabetically(appPackageJson.devDependencies, 'electron', expectedRange);
|
||||
await fs.writeJSON(appPackageJsonPath, appPackageJson, { spaces: 2 });
|
||||
throw new AbortError('Updated dependencies, please run "install" again');
|
||||
}
|
||||
if (!theiaElectron.electronVersion || !semver.satisfies(theiaElectron.electronVersion, currentRange)) {
|
||||
throw new AbortError('Dependencies are out of sync, please run "install" again');
|
||||
}
|
||||
const ffmpeg = await import('@theia/ffmpeg');
|
||||
await ffmpeg.replaceFfmpeg();
|
||||
await ffmpeg.checkFfmpeg();
|
||||
}
|
||||
|
||||
protected insertAlphabetically<T extends Record<string, string>>(object: T, key: string, value: string): T {
|
||||
const updated: Record<string, unknown> = {};
|
||||
for (const property of Object.keys(object)) {
|
||||
if (property.localeCompare(key) > 0) {
|
||||
updated[key] = value;
|
||||
}
|
||||
updated[property] = object[property];
|
||||
}
|
||||
if (!(key in updated)) {
|
||||
updated[key] = value;
|
||||
}
|
||||
return updated as T;
|
||||
}
|
||||
|
||||
private adjustArgs(args: string[], forkOptions: cp.ForkOptions = {}): Readonly<{ mainArgs: string[]; options: cp.ForkOptions }> {
|
||||
const options = {
|
||||
...this.forkOptions,
|
||||
forkOptions
|
||||
};
|
||||
const mainArgs = [...args];
|
||||
const inspectIndex = mainArgs.findIndex(v => v.startsWith('--inspect'));
|
||||
if (inspectIndex !== -1) {
|
||||
const inspectArg = mainArgs.splice(inspectIndex, 1)[0];
|
||||
options.execArgv = ['--nolazy', inspectArg];
|
||||
}
|
||||
return {
|
||||
mainArgs,
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
private get forkOptions(): cp.ForkOptions {
|
||||
return {
|
||||
stdio: [0, 1, 2, 'ipc'],
|
||||
env: {
|
||||
...process.env,
|
||||
THEIA_PARENT_PID: String(process.pid)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
100
dev-packages/application-manager/src/application-process.ts
Normal file
100
dev-packages/application-manager/src/application-process.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// *****************************************************************************
|
||||
// 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 path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as cp from 'child_process';
|
||||
import { ApplicationPackage } from '@theia/application-package';
|
||||
|
||||
export class ApplicationProcess {
|
||||
|
||||
protected readonly defaultOptions = {
|
||||
cwd: this.pck.projectPath,
|
||||
env: process.env
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly pck: ApplicationPackage,
|
||||
protected readonly binProjectPath: string
|
||||
) { }
|
||||
|
||||
spawn(command: string, args?: string[], options?: cp.SpawnOptions): cp.ChildProcess {
|
||||
return cp.spawn(command, args || [], Object.assign({}, this.defaultOptions, {
|
||||
...options,
|
||||
shell: true
|
||||
}));
|
||||
}
|
||||
|
||||
fork(modulePath: string, args?: string[], options?: cp.ForkOptions): cp.ChildProcess {
|
||||
return cp.fork(modulePath, args, Object.assign({}, this.defaultOptions, options));
|
||||
}
|
||||
|
||||
canRun(command: string): boolean {
|
||||
const binPath = this.resolveBin(this.binProjectPath, command);
|
||||
return !!binPath && fs.existsSync(binPath);
|
||||
}
|
||||
|
||||
run(command: string, args: string[], options?: cp.SpawnOptions): Promise<void> {
|
||||
const commandProcess = this.spawnBin(command, args, options);
|
||||
return this.promisify(command, commandProcess);
|
||||
}
|
||||
|
||||
spawnBin(command: string, args: string[], options?: cp.SpawnOptions): cp.ChildProcess {
|
||||
const binPath = this.resolveBin(this.binProjectPath, command);
|
||||
if (!binPath) {
|
||||
throw new Error(`Could not resolve ${command} relative to ${this.binProjectPath}`);
|
||||
}
|
||||
return this.spawn(binPath, args, {
|
||||
...options,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
|
||||
protected resolveBin(rootPath: string, command: string): string | undefined {
|
||||
let commandPath = path.resolve(rootPath, 'node_modules', '.bin', command);
|
||||
if (process.platform === 'win32') {
|
||||
commandPath = commandPath + '.cmd';
|
||||
}
|
||||
if (fs.existsSync(commandPath)) {
|
||||
return commandPath;
|
||||
}
|
||||
const parentDir = path.dirname(rootPath);
|
||||
if (parentDir === rootPath) {
|
||||
return undefined;
|
||||
}
|
||||
return this.resolveBin(parentDir, command);
|
||||
}
|
||||
|
||||
protected promisify(command: string, p: cp.ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
p.stdout!.on('data', data => this.pck.log(data.toString()));
|
||||
p.stderr!.on('data', data => this.pck.error(data.toString()));
|
||||
p.on('error', reject);
|
||||
p.on('close', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`${command} exited with an unexpected signal: ${signal}.`));
|
||||
return;
|
||||
}
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${command} exited with an unexpected code: ${code}.`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
80
dev-packages/application-manager/src/expose-loader.ts
Normal file
80
dev-packages/application-manager/src/expose-loader.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import type { RawSourceMap } from 'source-map';
|
||||
import { ApplicationPackage } from '@theia/application-package/lib/application-package';
|
||||
|
||||
const modulePackages: { dir: string, name?: string }[] = [];
|
||||
for (const extensionPackage of new ApplicationPackage({ projectPath: process.cwd() }).extensionPackages) {
|
||||
modulePackages.push({
|
||||
name: extensionPackage.name,
|
||||
dir: path.dirname(extensionPackage.raw.installed!.packagePath)
|
||||
});
|
||||
}
|
||||
|
||||
function exposeModule(modulePackage: { dir: string, name?: string }, resourcePath: string, source: string): string {
|
||||
if (!modulePackage.name) {
|
||||
return source;
|
||||
}
|
||||
const { dir, name } = path.parse(resourcePath);
|
||||
let moduleName = path.join(modulePackage.name, dir.substring(modulePackage.dir.length));
|
||||
if (name !== 'index') {
|
||||
moduleName = path.join(moduleName, name);
|
||||
}
|
||||
if (path.sep !== '/') {
|
||||
moduleName = moduleName.split(path.sep).join('/');
|
||||
}
|
||||
return source + `\n;(globalThis['theia'] = globalThis['theia'] || {})['${moduleName}'] = this;\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// TODO: webpack@5.36.2 is missing a `LoaderContext` interface so we'll use any in the meantime
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export = function (this: any, source: string, sourceMap?: RawSourceMap): string | undefined {
|
||||
if (this.cacheable) {
|
||||
this.cacheable();
|
||||
}
|
||||
let modulePackage = modulePackages.find(({ dir }) => this.resourcePath.startsWith(dir + path.sep));
|
||||
if (modulePackage) {
|
||||
this.callback(undefined, exposeModule(modulePackage, this.resourcePath, source), sourceMap);
|
||||
return;
|
||||
}
|
||||
const searchString = path.sep + 'node_modules';
|
||||
const index = this.resourcePath.lastIndexOf(searchString);
|
||||
if (index !== -1) {
|
||||
const nodeModulesPath = this.resourcePath.substring(0, index + searchString.length);
|
||||
let dir = this.resourcePath;
|
||||
while ((dir = path.dirname(dir)) !== nodeModulesPath) {
|
||||
try {
|
||||
const { name } = fs.readJSONSync(path.join(dir, 'package.json'));
|
||||
modulePackage = { name, dir };
|
||||
modulePackages.push(modulePackage);
|
||||
this.callback(undefined, exposeModule(modulePackage, this.resourcePath, source), sourceMap);
|
||||
return;
|
||||
} catch {
|
||||
/** no-op */
|
||||
}
|
||||
}
|
||||
}
|
||||
this.callback(undefined, source, sourceMap);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// 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-extra';
|
||||
import { ApplicationPackage } from '@theia/application-package';
|
||||
|
||||
export interface GeneratorOptions {
|
||||
mode?: 'development' | 'production'
|
||||
splitFrontend?: boolean
|
||||
}
|
||||
|
||||
export abstract class AbstractGenerator {
|
||||
|
||||
constructor(
|
||||
protected readonly pck: ApplicationPackage,
|
||||
protected options: GeneratorOptions = {}
|
||||
) { }
|
||||
|
||||
protected ifBrowser(value: string, defaultValue: string = ''): string {
|
||||
return this.pck.ifBrowser(value, defaultValue);
|
||||
}
|
||||
|
||||
protected ifElectron(value: string, defaultValue: string = ''): string {
|
||||
return this.pck.ifElectron(value, defaultValue);
|
||||
}
|
||||
|
||||
protected ifBrowserOnly(value: string, defaultValue: string = ''): string {
|
||||
return this.pck.ifBrowserOnly(value, defaultValue);
|
||||
}
|
||||
|
||||
protected async write(path: string, content: string): Promise<void> {
|
||||
await fs.ensureFile(path);
|
||||
await fs.writeFile(path, content);
|
||||
}
|
||||
|
||||
protected ifMonaco(value: () => string, defaultValue: () => string = () => ''): string {
|
||||
return this.ifPackage([
|
||||
'@theia/monaco',
|
||||
'@theia/monaco-editor-core'
|
||||
], value, defaultValue);
|
||||
}
|
||||
|
||||
protected ifPackage(packageName: string | string[], value: string | (() => string), defaultValue: string | (() => string) = ''): string {
|
||||
const packages = Array.isArray(packageName) ? packageName : [packageName];
|
||||
if (this.pck.extensionPackages.some(e => packages.includes(e.name))) {
|
||||
return typeof value === 'string' ? value : value();
|
||||
} else {
|
||||
return typeof defaultValue === 'string' ? defaultValue : defaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
protected prettyStringify(object: object): string {
|
||||
return JSON.stringify(object, undefined, 4);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// *****************************************************************************
|
||||
// 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 { EOL } from 'os';
|
||||
import { AbstractGenerator } from './abstract-generator';
|
||||
|
||||
export class BackendGenerator extends AbstractGenerator {
|
||||
|
||||
async generate(): Promise<void> {
|
||||
if (this.pck.isBrowserOnly()) {
|
||||
// no backend generation in case of browser-only target
|
||||
return;
|
||||
}
|
||||
const backendModules = this.pck.targetBackendModules;
|
||||
await this.write(this.pck.backend('server.js'), this.compileServer(backendModules));
|
||||
await this.write(this.pck.backend('main.js'), this.compileMain(backendModules));
|
||||
if (this.pck.isElectron()) {
|
||||
await this.write(this.pck.backend('electron-main.js'), this.compileElectronMain(this.pck.targetElectronMainModules));
|
||||
}
|
||||
}
|
||||
|
||||
protected compileElectronMain(electronMainModules?: Map<string, string>): string {
|
||||
return `// @ts-check
|
||||
|
||||
require('@theia/core/shared/reflect-metadata');
|
||||
|
||||
// Workaround for https://github.com/electron/electron/issues/9225. Chrome has an issue where
|
||||
// in certain locales (e.g. PL), image metrics are wrongly computed. We explicitly set the
|
||||
// LC_NUMERIC to prevent this from happening (selects the numeric formatting category of the
|
||||
// C locale, http://en.cppreference.com/w/cpp/locale/LC_categories).
|
||||
if (process.env.LC_ALL) {
|
||||
process.env.LC_ALL = 'C';
|
||||
}
|
||||
process.env.LC_NUMERIC = 'C';
|
||||
|
||||
(async () => {
|
||||
// Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the \`$PATH\` define
|
||||
// in your dotfiles (.bashrc/.bash_profile/.zshrc/etc).
|
||||
// https://github.com/electron/electron/issues/550#issuecomment-162037357
|
||||
// https://github.com/eclipse-theia/theia/pull/3534#issuecomment-439689082
|
||||
(await require('@theia/core/electron-shared/fix-path')).default();
|
||||
|
||||
const { resolve } = require('path');
|
||||
const theiaAppProjectPath = resolve(__dirname, '..', '..');
|
||||
process.env.THEIA_APP_PROJECT_PATH = theiaAppProjectPath;
|
||||
const { default: electronMainApplicationModule } = require('@theia/core/lib/electron-main/electron-main-application-module');
|
||||
const { ElectronMainApplication, ElectronMainApplicationGlobals } = require('@theia/core/lib/electron-main/electron-main-application');
|
||||
const { Container } = require('@theia/core/shared/inversify');
|
||||
const { app } = require('electron');
|
||||
|
||||
const config = ${this.prettyStringify(this.pck.props.frontend.config)};
|
||||
const isSingleInstance = ${this.pck.props.backend.config.singleInstance === true ? 'true' : 'false'};
|
||||
|
||||
if (isSingleInstance && !app.requestSingleInstanceLock(process.argv)) {
|
||||
// There is another instance running, exit now. The other instance will request focus.
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
const container = new Container();
|
||||
container.load(electronMainApplicationModule);
|
||||
container.bind(ElectronMainApplicationGlobals).toConstantValue({
|
||||
THEIA_APP_PROJECT_PATH: theiaAppProjectPath,
|
||||
THEIA_BACKEND_MAIN_PATH: resolve(__dirname, 'main.js'),
|
||||
THEIA_FRONTEND_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'index.html'),
|
||||
THEIA_SECONDARY_WINDOW_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'secondary-window.html')
|
||||
});
|
||||
|
||||
function load(raw) {
|
||||
return Promise.resolve(raw.default).then(module =>
|
||||
container.load(module)
|
||||
);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const application = container.get(ElectronMainApplication);
|
||||
await application.start(config);
|
||||
}
|
||||
|
||||
try {
|
||||
${Array.from(electronMainModules?.values() ?? [], jsModulePath => `\
|
||||
await load(require('${jsModulePath}'));`).join(EOL)}
|
||||
await start();
|
||||
} catch (reason) {
|
||||
if (typeof reason !== 'number') {
|
||||
console.error('Failed to start the electron application.');
|
||||
if (reason) {
|
||||
console.error(reason);
|
||||
}
|
||||
}
|
||||
app.quit();
|
||||
};
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
protected compileServer(backendModules: Map<string, string>): string {
|
||||
return `// @ts-check
|
||||
require('reflect-metadata');${this.ifElectron(`
|
||||
|
||||
// Patch electron version if missing, see https://github.com/eclipse-theia/theia/pull/7361#pullrequestreview-377065146
|
||||
if (typeof process.versions.electron === 'undefined' && typeof process.env.THEIA_ELECTRON_VERSION === 'string') {
|
||||
process.versions.electron = process.env.THEIA_ELECTRON_VERSION;
|
||||
}`)}
|
||||
|
||||
// Erase the ELECTRON_RUN_AS_NODE variable from the environment, else Electron apps started using Theia will pick it up.
|
||||
if ('ELECTRON_RUN_AS_NODE' in process.env) {
|
||||
delete process.env.ELECTRON_RUN_AS_NODE;
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
process.env.THEIA_APP_PROJECT_PATH = path.resolve(__dirname, '..', '..')
|
||||
const express = require('@theia/core/shared/express');
|
||||
const { Container } = require('@theia/core/shared/inversify');
|
||||
const { BackendApplication, BackendApplicationServer, CliManager } = require('@theia/core/lib/node');
|
||||
const { backendApplicationModule } = require('@theia/core/lib/node/backend-application-module');
|
||||
const { messagingBackendModule } = require('@theia/core/lib/node/messaging/messaging-backend-module');
|
||||
const { loggerBackendModule } = require('@theia/core/lib/node/logger-backend-module');
|
||||
|
||||
const container = new Container();
|
||||
container.load(backendApplicationModule);
|
||||
container.load(messagingBackendModule);
|
||||
container.load(loggerBackendModule);
|
||||
|
||||
function defaultServeStatic(app) {
|
||||
app.use(express.static(path.resolve(__dirname, '../../lib/frontend')))
|
||||
}
|
||||
|
||||
function load(raw) {
|
||||
return Promise.resolve(raw).then(
|
||||
module => container.load(module.default)
|
||||
);
|
||||
}
|
||||
|
||||
async function start(port, host, argv = process.argv) {
|
||||
if (!container.isBound(BackendApplicationServer)) {
|
||||
container.bind(BackendApplicationServer).toConstantValue({ configure: defaultServeStatic });
|
||||
}
|
||||
let result = undefined;
|
||||
await container.get(CliManager).initializeCli(argv.slice(2),
|
||||
() => container.get(BackendApplication).configured,
|
||||
async () => {
|
||||
result = container.get(BackendApplication).start(port, host);
|
||||
});
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
return Promise.reject(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (port, host, argv) => {
|
||||
try {
|
||||
${Array.from(backendModules.values(), jsModulePath => `\
|
||||
await load(require('${jsModulePath}'));`).join(EOL)}
|
||||
return await start(port, host, argv);
|
||||
} catch (error) {
|
||||
if (typeof error !== 'number') {
|
||||
console.error('Failed to start the backend application:');
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
protected compileMain(backendModules: Map<string, string>): string {
|
||||
return `// @ts-check
|
||||
const { BackendApplicationConfigProvider } = require('@theia/core/lib/node/backend-application-config-provider');
|
||||
const main = require('@theia/core/lib/node/main');
|
||||
|
||||
BackendApplicationConfigProvider.set(${this.prettyStringify(this.pck.props.backend.config)});
|
||||
|
||||
globalThis.extensionInfo = ${this.prettyStringify(this.pck.extensionPackages.map(({ name, version }) => ({ name, version })))};
|
||||
|
||||
const serverModule = require('./server');
|
||||
const serverAddress = main.start(serverModule());
|
||||
|
||||
serverAddress.then((addressInfo) => {
|
||||
if (process && process.send && addressInfo) {
|
||||
process.send(addressInfo);
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.serverAddress = serverAddress;
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/indent */
|
||||
|
||||
import { EOL } from 'os';
|
||||
import { AbstractGenerator, GeneratorOptions } from './abstract-generator';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
|
||||
export class FrontendGenerator extends AbstractGenerator {
|
||||
|
||||
async generate(options?: GeneratorOptions): Promise<void> {
|
||||
await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(this.pck.targetFrontendModules));
|
||||
await this.write(this.pck.frontend('index.js'), this.compileIndexJs(this.pck.targetFrontendModules, this.pck.targetFrontendPreloadModules));
|
||||
await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml());
|
||||
await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules));
|
||||
if (this.pck.isElectron()) {
|
||||
await this.write(this.pck.frontend('preload.js'), this.compilePreloadJs());
|
||||
}
|
||||
}
|
||||
|
||||
protected compileIndexPreload(frontendModules: Map<string, string>): string {
|
||||
const template = this.pck.props.generator.config.preloadTemplate;
|
||||
if (!template) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Support path to html file
|
||||
if (existsSync(template)) {
|
||||
return readFileSync(template).toString();
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
protected compileIndexHtml(frontendModules: Map<string, string>): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>${this.compileIndexHead(frontendModules)}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="theia-preload">${this.compileIndexPreload(frontendModules)}</div>
|
||||
<script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
}
|
||||
|
||||
protected compileIndexHead(frontendModules: Map<string, string>): string {
|
||||
return `
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>${this.pck.props.frontend.config.applicationName}</title>
|
||||
<link rel="stylesheet" href="./vibn.css">`;
|
||||
}
|
||||
|
||||
protected compileIndexJs(frontendModules: Map<string, string>, frontendPreloadModules: Map<string, string>): string {
|
||||
return `\
|
||||
// @ts-check
|
||||
require('reflect-metadata');
|
||||
const { Container } = require('@theia/core/shared/inversify');
|
||||
const { FrontendApplicationConfigProvider } = require('@theia/core/lib/browser/frontend-application-config-provider');
|
||||
|
||||
FrontendApplicationConfigProvider.set(${this.prettyStringify(this.pck.props.frontend.config)});
|
||||
|
||||
${this.ifMonaco(() => `
|
||||
self.MonacoEnvironment = {
|
||||
getWorkerUrl: function (moduleId, label) {
|
||||
return './editor.worker.js';
|
||||
}
|
||||
}`)}
|
||||
|
||||
function load(container, jsModule) {
|
||||
return Promise.resolve(jsModule)
|
||||
.then(containerModule => container.load(containerModule.default));
|
||||
}
|
||||
|
||||
async function preload(container) {
|
||||
try {
|
||||
${Array.from(frontendPreloadModules.values(), jsModulePath => `\
|
||||
await load(container, ${this.importOrRequire()}('${jsModulePath}'));`).join(EOL)}
|
||||
const { Preloader } = require('@theia/core/lib/browser/preload/preloader');
|
||||
const preloader = container.get(Preloader);
|
||||
await preloader.initialize();
|
||||
} catch (reason) {
|
||||
console.error('Failed to run preload scripts.');
|
||||
if (reason) {
|
||||
console.error(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (async () => {
|
||||
const { messagingFrontendModule } = require('@theia/core/lib/${this.pck.isBrowser() || this.pck.isBrowserOnly()
|
||||
? 'browser/messaging/messaging-frontend-module'
|
||||
: 'electron-browser/messaging/electron-messaging-frontend-module'}');
|
||||
const container = new Container();
|
||||
container.load(messagingFrontendModule);
|
||||
${this.ifBrowserOnly(`const { messagingFrontendOnlyModule } = require('@theia/core/lib/browser-only/messaging/messaging-frontend-only-module');
|
||||
container.load(messagingFrontendOnlyModule);`)}
|
||||
|
||||
await preload(container);
|
||||
|
||||
${this.ifMonaco(() => `
|
||||
const { MonacoInit } = require('@theia/monaco/lib/browser/monaco-init');
|
||||
`)};
|
||||
|
||||
const { FrontendApplication } = require('@theia/core/lib/browser');
|
||||
const { frontendApplicationModule } = require('@theia/core/lib/browser/frontend-application-module');
|
||||
const { loggerFrontendModule } = require('@theia/core/lib/browser/logger-frontend-module');
|
||||
|
||||
container.load(frontendApplicationModule);
|
||||
${this.pck.ifBrowserOnly(`const { frontendOnlyApplicationModule } = require('@theia/core/lib/browser-only/frontend-only-application-module');
|
||||
container.load(frontendOnlyApplicationModule);`)}
|
||||
|
||||
container.load(loggerFrontendModule);
|
||||
${this.ifBrowserOnly(`const { loggerFrontendOnlyModule } = require('@theia/core/lib/browser-only/logger-frontend-only-module');
|
||||
container.load(loggerFrontendOnlyModule);`)}
|
||||
|
||||
try {
|
||||
${Array.from(frontendModules.values(), jsModulePath => `\
|
||||
await load(container, ${this.importOrRequire()}('${jsModulePath}'));`).join(EOL)}
|
||||
${this.ifMonaco(() => `
|
||||
MonacoInit.init(container);
|
||||
`)};
|
||||
await start();
|
||||
} catch (reason) {
|
||||
console.error('Failed to start the frontend application.');
|
||||
if (reason) {
|
||||
console.error(reason);
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
(window['theia'] = window['theia'] || {}).container = container;
|
||||
return container.get(FrontendApplication).start();
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
protected importOrRequire(): string {
|
||||
return this.options.mode !== 'production' ? 'import' : 'require';
|
||||
}
|
||||
|
||||
/** HTML for secondary windows that contain an extracted widget. */
|
||||
protected compileSecondaryWindowHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Theia — Secondary Window</title>
|
||||
<style>
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
head,
|
||||
body,
|
||||
.secondary-widget-root,
|
||||
#widget-host {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="./secondary-window.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="widget-host"></div>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
}
|
||||
|
||||
protected compileSecondaryIndexJs(secondaryWindowModules: Map<string, string>): string {
|
||||
return `\
|
||||
// @ts-check
|
||||
require('reflect-metadata');
|
||||
const { Container } = require('@theia/core/shared/inversify');
|
||||
|
||||
module.exports = Promise.resolve().then(() => {
|
||||
const { frontendApplicationModule } = require('@theia/core/lib/browser/frontend-application-module');
|
||||
const container = new Container();
|
||||
container.load(frontendApplicationModule);
|
||||
${Array.from(secondaryWindowModules.values(), jsModulePath => `\
|
||||
container.load(require('${jsModulePath}').default);`).join(EOL)}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
compilePreloadJs(): string {
|
||||
return `\
|
||||
// @ts-check
|
||||
${Array.from(this.pck.preloadModules.values(), path => `require('${path}').preload();`).join(EOL)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
19
dev-packages/application-manager/src/generator/index.ts
Normal file
19
dev-packages/application-manager/src/generator/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './webpack-generator';
|
||||
export * from './frontend-generator';
|
||||
export * from './backend-generator';
|
||||
@@ -0,0 +1,529 @@
|
||||
// *****************************************************************************
|
||||
// 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 paths from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { AbstractGenerator } from './abstract-generator';
|
||||
|
||||
export class WebpackGenerator extends AbstractGenerator {
|
||||
|
||||
async generate(): Promise<void> {
|
||||
await this.write(this.genConfigPath, this.compileWebpackConfig());
|
||||
if (!this.pck.isBrowserOnly()) {
|
||||
await this.write(this.genNodeConfigPath, this.compileNodeWebpackConfig());
|
||||
}
|
||||
if (await this.shouldGenerateUserWebpackConfig()) {
|
||||
await this.write(this.configPath, this.compileUserWebpackConfig());
|
||||
}
|
||||
}
|
||||
|
||||
protected async shouldGenerateUserWebpackConfig(): Promise<boolean> {
|
||||
if (!(await fs.pathExists(this.configPath))) {
|
||||
return true;
|
||||
}
|
||||
const content = await fs.readFile(this.configPath, 'utf8');
|
||||
return content.indexOf('gen-webpack') === -1;
|
||||
}
|
||||
|
||||
get configPath(): string {
|
||||
return this.pck.path('webpack.config.js');
|
||||
}
|
||||
|
||||
get genConfigPath(): string {
|
||||
return this.pck.path('gen-webpack.config.js');
|
||||
}
|
||||
|
||||
get genNodeConfigPath(): string {
|
||||
return this.pck.path('gen-webpack.node.config.js');
|
||||
}
|
||||
|
||||
protected compileWebpackConfig(): string {
|
||||
return `/**
|
||||
* Don't touch this file. It will be regenerated by theia build.
|
||||
* To customize webpack configuration change ${this.configPath}
|
||||
*/
|
||||
// @ts-check
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const yargs = require('yargs');
|
||||
const resolvePackagePath = require('resolve-package-path');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { MonacoWebpackPlugin } = require('@theia/native-webpack-plugin/lib/monaco-webpack-plugins.js');
|
||||
|
||||
const outputPath = path.resolve(__dirname, 'lib', 'frontend');
|
||||
const { mode, staticCompression } = yargs.option('mode', {
|
||||
description: "Mode to use",
|
||||
choices: ["development", "production"],
|
||||
default: "production"
|
||||
}).option('static-compression', {
|
||||
description: 'Controls whether to enable compression of static artifacts.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}).argv;
|
||||
const development = mode === 'development';
|
||||
|
||||
const plugins = [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
// copy secondary window html file to lib folder
|
||||
from: path.resolve(__dirname, 'src-gen/frontend/secondary-window.html')
|
||||
}${this.ifPackage('@theia/plugin-ext', `,
|
||||
{
|
||||
// copy webview files to lib folder
|
||||
from: path.join(resolvePackagePath('@theia/plugin-ext', __dirname), '..', 'src', 'main', 'browser', 'webview', 'pre'),
|
||||
to: path.resolve(__dirname, 'lib', 'webview', 'pre')
|
||||
}`)}
|
||||
${this.ifPackage('@theia/plugin-ext-vscode', `,
|
||||
{
|
||||
// copy frontend plugin host files
|
||||
from: path.join(resolvePackagePath('@theia/plugin-ext-vscode', __dirname), '..', 'lib', 'node', 'context', 'plugin-vscode-init-fe.js'),
|
||||
to: path.resolve(__dirname, 'lib', 'frontend', 'context', 'plugin-vscode-init-fe.js')
|
||||
}`)}
|
||||
]
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
// the Buffer class doesn't exist in the browser but some dependencies rely on it
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
}),
|
||||
new MonacoWebpackPlugin()
|
||||
];
|
||||
// it should go after copy-plugin in order to compress monaco as well
|
||||
if (staticCompression) {
|
||||
plugins.push(new CompressionPlugin({}));
|
||||
}
|
||||
|
||||
module.exports = [{
|
||||
mode,
|
||||
plugins,
|
||||
devtool: 'source-map',
|
||||
entry: {
|
||||
bundle: path.resolve(__dirname, 'src-gen/frontend/index.js'),
|
||||
${this.ifMonaco(() => "'editor.worker': '@theia/monaco-editor-core/esm/vs/editor/editor.worker.js'")}
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: outputPath,
|
||||
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
|
||||
globalObject: 'self'
|
||||
},
|
||||
target: 'web',
|
||||
cache: staticCompression,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\\.css$/,
|
||||
exclude: /materialcolors\\.css$|\\.useable\\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /materialcolors\\.css$|\\.useable\\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
options: {
|
||||
esModule: false,
|
||||
injectType: 'lazySingletonStyleTag',
|
||||
attributes: {
|
||||
id: 'theia-theme'
|
||||
}
|
||||
}
|
||||
},
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\\.(ttf|eot|svg)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10000,
|
||||
}
|
||||
},
|
||||
generator: {
|
||||
dataUrl: {
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\\.(jpg|png|gif)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[hash].[ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
// see https://github.com/eclipse-theia/theia/issues/556
|
||||
test: /source-map-support/,
|
||||
loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\\.d\\.ts$/,
|
||||
loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\\.js$/,
|
||||
enforce: 'pre',
|
||||
loader: 'source-map-loader',
|
||||
exclude: /jsonc-parser|fast-plist|onigasm/
|
||||
},
|
||||
{
|
||||
test: /\\.woff(2)?(\\?v=[0-9]\\.[0-9]\\.[0-9])?$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10000,
|
||||
}
|
||||
},
|
||||
generator: {
|
||||
dataUrl: {
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /node_modules[\\\\|\/](vscode-languageserver-types|vscode-uri|jsonc-parser|vscode-languageserver-protocol)/,
|
||||
loader: 'umd-compat-loader'
|
||||
},
|
||||
{
|
||||
test: /\\.wasm$/,
|
||||
type: 'asset/resource'
|
||||
},
|
||||
{
|
||||
test: /\\.plist$/,
|
||||
type: 'asset/resource'
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
fallback: {
|
||||
'child_process': false,
|
||||
'crypto': false,
|
||||
'net': false,
|
||||
'path': require.resolve('path-browserify'),
|
||||
'process': false,
|
||||
'os': false,
|
||||
'timers': false
|
||||
},
|
||||
extensions: ['.js']
|
||||
},
|
||||
stats: {
|
||||
warnings: true,
|
||||
children: true
|
||||
},
|
||||
ignoreWarnings: [
|
||||
// Some packages do not have source maps, that's ok
|
||||
/Failed to parse source map/,
|
||||
{
|
||||
// Monaco uses 'require' in a non-standard way
|
||||
module: /@theia\\/monaco-editor-core/,
|
||||
message: /require function is used in a way in which dependencies cannot be statically extracted/
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
mode,
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: "[name].css",
|
||||
chunkFilename: "[id].css",
|
||||
}),
|
||||
new MonacoWebpackPlugin(),
|
||||
],
|
||||
devtool: 'source-map',
|
||||
entry: {
|
||||
"secondary-window": path.resolve(__dirname, 'src-gen/frontend/secondary-index.js'),
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: outputPath,
|
||||
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
|
||||
globalObject: 'self'
|
||||
},
|
||||
target: 'web',
|
||||
cache: staticCompression,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader"]
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: 'asset/resource'
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
fallback: {
|
||||
'child_process': false,
|
||||
'crypto': false,
|
||||
'net': false,
|
||||
'path': require.resolve('path-browserify'),
|
||||
'process': false,
|
||||
'os': false,
|
||||
'timers': false
|
||||
},
|
||||
extensions: ['.js']
|
||||
},
|
||||
stats: {
|
||||
warnings: true,
|
||||
children: true
|
||||
},
|
||||
ignoreWarnings: [
|
||||
{
|
||||
// Monaco uses 'require' in a non-standard way
|
||||
module: /@theia\\/monaco-editor-core/,
|
||||
message: /require function is used in a way in which dependencies cannot be statically extracted/
|
||||
}
|
||||
]
|
||||
}${this.ifElectron(`, {
|
||||
mode,
|
||||
devtool: 'source-map',
|
||||
entry: {
|
||||
"preload": path.resolve(__dirname, 'src-gen/frontend/preload.js'),
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: outputPath,
|
||||
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
|
||||
globalObject: 'self'
|
||||
},
|
||||
target: 'electron-preload',
|
||||
cache: staticCompression,
|
||||
stats: {
|
||||
warnings: true,
|
||||
children: true
|
||||
}
|
||||
}`)}];`;
|
||||
}
|
||||
|
||||
protected compileUserWebpackConfig(): string {
|
||||
return `/**
|
||||
* This file can be edited to customize webpack configuration.
|
||||
* To reset delete this file and rerun theia build again.
|
||||
*/
|
||||
// @ts-check
|
||||
const configs = require('./${paths.basename(this.genConfigPath)}');
|
||||
${this.ifBrowserOnly('', `const nodeConfig = require('./${paths.basename(this.genNodeConfigPath)}');`)}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
configs[0].module.rules.push({
|
||||
test: /\\.js$/,
|
||||
loader: require.resolve('@theia/application-manager/lib/expose-loader')
|
||||
}); */
|
||||
|
||||
${this.ifBrowserOnly('module.exports = configs;', `module.exports = [
|
||||
...configs,
|
||||
nodeConfig.config
|
||||
];`)}
|
||||
`;
|
||||
}
|
||||
|
||||
protected compileNodeWebpackConfig(): string {
|
||||
return `/**
|
||||
* Don't touch this file. It will be regenerated by theia build.
|
||||
* To customize webpack configuration change ${this.configPath}
|
||||
*/
|
||||
// @ts-check
|
||||
const path = require('path');
|
||||
const yargs = require('yargs');
|
||||
const webpack = require('webpack');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const NativeWebpackPlugin = require('@theia/native-webpack-plugin');
|
||||
const { MonacoWebpackPlugin } = require('@theia/native-webpack-plugin/lib/monaco-webpack-plugins.js');
|
||||
|
||||
const { mode } = yargs.option('mode', {
|
||||
description: "Mode to use",
|
||||
choices: ["development", "production"],
|
||||
default: "production"
|
||||
}).argv;
|
||||
|
||||
const production = mode === 'production';
|
||||
|
||||
/** @type {import('webpack').EntryObject} */
|
||||
const commonJsLibraries = {};
|
||||
for (const [entryPointName, entryPointPath] of Object.entries({
|
||||
${this.ifPackage('@theia/plugin-ext', "'backend-init-theia': '@theia/plugin-ext/lib/hosted/node/scanners/backend-init-theia',")}
|
||||
${this.ifPackage('@theia/filesystem', "'parcel-watcher': '@theia/filesystem/lib/node/parcel-watcher',")}
|
||||
${this.ifPackage('@theia/plugin-ext-vscode', "'plugin-vscode-init': '@theia/plugin-ext-vscode/lib/node/plugin-vscode-init',")}
|
||||
${this.ifPackage('@theia/api-provider-sample', "'gotd-api-init': '@theia/api-provider-sample/lib/plugin/gotd-api-init',")}
|
||||
${this.ifPackage('@theia/git', "'git-locator-host': '@theia/git/lib/node/git-locator/git-locator-host',")}
|
||||
})) {
|
||||
commonJsLibraries[entryPointName] = {
|
||||
import: require.resolve(entryPointPath),
|
||||
library: {
|
||||
type: 'commonjs2',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ignoredResources = new Set();
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
ignoredResources.add('@vscode/windows-ca-certs');
|
||||
ignoredResources.add('@vscode/windows-ca-certs/build/Release/crypt32.node');
|
||||
}
|
||||
|
||||
const nativePlugin = new NativeWebpackPlugin({
|
||||
out: 'native',
|
||||
trash: ${this.ifPackage('@theia/filesystem', 'true', 'false')},
|
||||
ripgrep: ${this.ifPackage(['@theia/search-in-workspace', '@theia/file-search'], 'true', 'false')},
|
||||
pty: ${this.ifPackage('@theia/process', 'true', 'false')},
|
||||
nativeBindings: {
|
||||
drivelist: 'drivelist/build/Release/drivelist.node'
|
||||
}
|
||||
});
|
||||
|
||||
${this.ifPackage('@theia/process', () => `// Ensure that node-pty is correctly hoisted
|
||||
try {
|
||||
require.resolve('node-pty');
|
||||
} catch {
|
||||
console.error('"node-pty" dependency is not installed correctly. Ensure that it is available in the root node_modules directory.');
|
||||
console.error('Exiting webpack build process.');
|
||||
process.exit(1);
|
||||
}`)}
|
||||
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
mode,
|
||||
devtool: mode === 'development' ? 'source-map' : false,
|
||||
target: 'node',
|
||||
node: {
|
||||
global: false,
|
||||
__filename: false,
|
||||
__dirname: false
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.wasm', '.node'],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'lib', 'backend'),
|
||||
devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]?[loaders]',
|
||||
},${this.ifElectron(`
|
||||
externals: {
|
||||
electron: 'require("electron")'
|
||||
},`)}
|
||||
entry: {
|
||||
// Main entry point of the Theia application backend:
|
||||
'main': require.resolve('./src-gen/backend/main'),
|
||||
// Theia's IPC mechanism:
|
||||
'ipc-bootstrap': require.resolve('@theia/core/lib/node/messaging/ipc-bootstrap'),
|
||||
${this.ifPackage('@theia/plugin-ext', () => `// VS Code extension support:
|
||||
'plugin-host': require.resolve('@theia/plugin-ext/lib/hosted/node/plugin-host'),`)}
|
||||
${this.ifPackage('@theia/plugin-ext-headless', () => `// Theia Headless Plugin support:
|
||||
'plugin-host-headless': require.resolve('@theia/plugin-ext-headless/lib/hosted/node/plugin-host-headless'),`)}
|
||||
${this.ifPackage('@theia/process', () => `// Make sure the node-pty thread worker can be executed:
|
||||
'worker/conoutSocketWorker': require.resolve('node-pty/lib/worker/conoutSocketWorker'),`)}
|
||||
${this.ifElectron("'electron-main': require.resolve('./src-gen/backend/electron-main'),")}
|
||||
${this.ifPackage('@theia/dev-container', () => `// VS Code Dev-Container communication:
|
||||
'dev-container-server': require.resolve('@theia/dev-container/lib/dev-container-server/dev-container-server'),`)}
|
||||
...commonJsLibraries
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
// Make sure we can still find and load our native addons.
|
||||
{
|
||||
test: /\\.node$/,
|
||||
loader: 'node-loader',
|
||||
options: {
|
||||
name: 'native/[name].[ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\\.d\\.ts$/,
|
||||
loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\\.js$/,
|
||||
enforce: 'pre',
|
||||
loader: 'source-map-loader'
|
||||
},
|
||||
// jsonc-parser exposes its UMD implementation by default, which
|
||||
// confuses Webpack leading to missing js in the bundles.
|
||||
{
|
||||
test: /node_modules[\\/](jsonc-parser)/,
|
||||
loader: 'umd-compat-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// Some native dependencies need special handling
|
||||
nativePlugin,
|
||||
// Optional node dependencies can be safely ignored
|
||||
new webpack.IgnorePlugin({
|
||||
checkResource: resource => ignoredResources.has(resource)
|
||||
}),
|
||||
new MonacoWebpackPlugin()
|
||||
],
|
||||
optimization: {
|
||||
// Split and reuse code across the various entry points
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
},
|
||||
// Only minimize if we run webpack in production mode
|
||||
minimize: production,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
exclude: /^(lib|builtins)\\//${this.ifPackage(['@theia/scanoss', '@theia/ai-anthropic', '@theia/ai-openai'], () => `,
|
||||
terserOptions: {
|
||||
keep_classnames: /AbortSignal/
|
||||
}`)}
|
||||
})
|
||||
]
|
||||
},
|
||||
ignoreWarnings: [
|
||||
// Some packages do not have source maps, that's ok
|
||||
/Failed to parse source map/,
|
||||
// require with expressions are not supported
|
||||
/the request of a dependency is an expression/,
|
||||
// Some packages use dynamic requires, we can safely ignore them (they are handled by the native webpack plugin)
|
||||
/require function is used in a way in which dependencies cannot be statically extracted/, {
|
||||
module: /yargs/
|
||||
}, {
|
||||
module: /node-pty/
|
||||
}, {
|
||||
module: /require-main-filename/
|
||||
}, {
|
||||
module: /ws/
|
||||
}, {
|
||||
module: /express/
|
||||
}, {
|
||||
module: /cross-spawn/
|
||||
}, {
|
||||
module: /@parcel\\/watcher/
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
config,
|
||||
nativePlugin,
|
||||
ignoredResources
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
19
dev-packages/application-manager/src/index.ts
Normal file
19
dev-packages/application-manager/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 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
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './rebuild';
|
||||
export * from './application-package-manager';
|
||||
export * from './application-process';
|
||||
28
dev-packages/application-manager/src/package.spec.ts
Normal file
28
dev-packages/application-manager/src/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 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
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('application-manager package', () => {
|
||||
|
||||
it('should support code coverage statistics', () => true);
|
||||
});
|
||||
349
dev-packages/application-manager/src/rebuild.ts
Normal file
349
dev-packages/application-manager/src/rebuild.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
// *****************************************************************************
|
||||
// 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 cp = require('child_process');
|
||||
import fs = require('fs-extra');
|
||||
import path = require('path');
|
||||
import os = require('os');
|
||||
|
||||
export type RebuildTarget = 'electron' | 'browser' | 'browser-only';
|
||||
|
||||
const EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
|
||||
|
||||
interface ExitToken {
|
||||
getLastSignal(): NodeJS.Signals | undefined
|
||||
onSignal(callback: (signal: NodeJS.Signals) => void): void
|
||||
}
|
||||
|
||||
type NodeABI = string | number;
|
||||
|
||||
export const DEFAULT_MODULES = [
|
||||
'node-pty',
|
||||
'native-keymap',
|
||||
'find-git-repositories',
|
||||
'drivelist',
|
||||
'keytar',
|
||||
'ssh2',
|
||||
'cpu-features'
|
||||
];
|
||||
|
||||
export interface RebuildOptions {
|
||||
/**
|
||||
* What modules to rebuild.
|
||||
*/
|
||||
modules?: string[]
|
||||
/**
|
||||
* Folder where the module cache will be created/read from.
|
||||
*/
|
||||
cacheRoot?: string
|
||||
/**
|
||||
* In the event that `node-abi` doesn't recognize the current Electron version,
|
||||
* you can specify the Node ABI to rebuild for.
|
||||
*/
|
||||
forceAbi?: NodeABI
|
||||
}
|
||||
|
||||
/**
|
||||
* @param target What to rebuild for.
|
||||
* @param options
|
||||
*/
|
||||
export function rebuild(target: RebuildTarget, options: RebuildOptions = {}): void {
|
||||
const {
|
||||
modules = DEFAULT_MODULES,
|
||||
cacheRoot = process.cwd(),
|
||||
forceAbi,
|
||||
} = options;
|
||||
const cache = path.resolve(cacheRoot, '.browser_modules');
|
||||
const cacheExists = folderExists(cache);
|
||||
guardExit(async token => {
|
||||
if (target === 'electron' && !cacheExists) {
|
||||
process.exitCode = await rebuildElectronModules(cache, modules, forceAbi, token);
|
||||
} else if (target === 'browser' && cacheExists) {
|
||||
process.exitCode = await revertBrowserModules(cache, modules);
|
||||
} else {
|
||||
console.log(`native node modules are already rebuilt for ${target}`);
|
||||
}
|
||||
}).catch(errorOrSignal => {
|
||||
if (typeof errorOrSignal === 'string' && errorOrSignal in os.constants.signals) {
|
||||
process.kill(process.pid, errorOrSignal);
|
||||
} else {
|
||||
throw errorOrSignal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function folderExists(folder: string): boolean {
|
||||
if (fs.existsSync(folder)) {
|
||||
if (fs.statSync(folder).isDirectory()) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`"${folder}" exists but it is not a directory`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for `<browserModuleCache>/modules.json`.
|
||||
*/
|
||||
interface ModulesJson {
|
||||
[moduleName: string]: ModuleBackup
|
||||
}
|
||||
interface ModuleBackup {
|
||||
originalLocation: string
|
||||
}
|
||||
|
||||
async function rebuildElectronModules(browserModuleCache: string, modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
|
||||
const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
|
||||
const modulesJson: ModulesJson = await fs.access(modulesJsonPath).then(
|
||||
() => fs.readJson(modulesJsonPath),
|
||||
() => ({})
|
||||
);
|
||||
let success = true;
|
||||
// Backup already built browser modules.
|
||||
await Promise.all(modules.map(async module => {
|
||||
let modulePath;
|
||||
try {
|
||||
modulePath = require.resolve(`${module}/package.json`, {
|
||||
paths: [process.cwd()],
|
||||
});
|
||||
} catch (_) {
|
||||
console.debug(`Module not found: ${module}`);
|
||||
return; // Skip current module.
|
||||
}
|
||||
const src = path.dirname(modulePath);
|
||||
const dest = path.join(browserModuleCache, module);
|
||||
try {
|
||||
await fs.remove(dest);
|
||||
await fs.copy(src, dest, { overwrite: true });
|
||||
modulesJson[module] = {
|
||||
originalLocation: src,
|
||||
};
|
||||
console.debug(`Processed "${module}"`);
|
||||
} catch (error) {
|
||||
console.error(`Error while doing a backup for "${module}": ${error}`);
|
||||
success = false;
|
||||
}
|
||||
}));
|
||||
if (Object.keys(modulesJson).length === 0) {
|
||||
console.debug('No module to rebuild.');
|
||||
return 0;
|
||||
}
|
||||
// Update manifest tracking the backups' original locations.
|
||||
await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
|
||||
// If we failed to process a module then exit now.
|
||||
if (!success) {
|
||||
return 1;
|
||||
}
|
||||
const todo = modules.map(m => {
|
||||
// electron-rebuild ignores the module namespace...
|
||||
const slash = m.indexOf('/');
|
||||
return m.startsWith('@') && slash !== -1
|
||||
? m.substring(slash + 1)
|
||||
: m;
|
||||
});
|
||||
let exitCode: number | undefined;
|
||||
try {
|
||||
if (process.env.THEIA_REBUILD_NO_WORKAROUND) {
|
||||
exitCode = await runElectronRebuild(todo, forceAbi, token);
|
||||
} else {
|
||||
exitCode = await electronRebuildExtraModulesWorkaround(process.cwd(), todo, () => runElectronRebuild(todo, forceAbi, token), token);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
// If code is undefined or different from zero we need to revert back to the browser modules.
|
||||
if (exitCode !== 0) {
|
||||
await revertBrowserModules(browserModuleCache, modules);
|
||||
}
|
||||
return exitCode ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function revertBrowserModules(browserModuleCache: string, modules: string[]): Promise<number> {
|
||||
let exitCode = 0;
|
||||
const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
|
||||
const modulesJson: ModulesJson = await fs.readJson(modulesJsonPath);
|
||||
await Promise.all(Object.entries(modulesJson).map(async ([moduleName, entry]) => {
|
||||
if (!modules.includes(moduleName)) {
|
||||
return; // Skip modules that weren't requested.
|
||||
}
|
||||
const src = path.join(browserModuleCache, moduleName);
|
||||
if (!await fs.pathExists(src)) {
|
||||
delete modulesJson[moduleName];
|
||||
console.error(`Missing backup for ${moduleName}!`);
|
||||
exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const dest = entry.originalLocation;
|
||||
try {
|
||||
await fs.remove(dest);
|
||||
await fs.copy(src, dest, { overwrite: false });
|
||||
await fs.remove(src);
|
||||
delete modulesJson[moduleName];
|
||||
console.debug(`Reverted "${moduleName}"`);
|
||||
} catch (error) {
|
||||
console.error(`Error while reverting "${moduleName}": ${error}`);
|
||||
exitCode = 1;
|
||||
}
|
||||
}));
|
||||
if (Object.keys(modulesJson).length === 0) {
|
||||
// We restored everything, so we can delete the cache.
|
||||
await fs.remove(browserModuleCache);
|
||||
} else {
|
||||
// Some things were not restored, so we update the manifest.
|
||||
await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
async function runElectronRebuild(modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
|
||||
const todo = modules.join(',');
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let command = `npx --no-install electron-rebuild -f -w=${todo} -o=${todo}`;
|
||||
if (forceAbi) {
|
||||
command += ` --force-abi ${forceAbi}`;
|
||||
}
|
||||
const electronRebuild = cp.spawn(command, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
token.onSignal(signal => electronRebuild.kill(signal));
|
||||
electronRebuild.on('error', reject);
|
||||
electronRebuild.on('close', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`electron-rebuild exited with "${signal}"`));
|
||||
} else {
|
||||
resolve(code!);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `electron-rebuild` is supposed to accept a list of modules to build, even when not part of the dependencies.
|
||||
* But there is a bug that causes `electron-rebuild` to not correctly process this list of modules.
|
||||
*
|
||||
* This workaround will temporarily modify the current package.json file.
|
||||
*
|
||||
* PR with fix: https://github.com/electron/electron-rebuild/pull/888
|
||||
*
|
||||
* TODO: Remove this workaround.
|
||||
*/
|
||||
async function electronRebuildExtraModulesWorkaround<T>(cwd: string, extraModules: string[], run: (token: ExitToken) => Promise<T>, token: ExitToken): Promise<T> {
|
||||
const packageJsonPath = path.resolve(cwd, 'package.json');
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
// package.json exists: We back it up before modifying it then revert it.
|
||||
const packageJsonCopyPath = `${packageJsonPath}.copy`;
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
await fs.copy(packageJsonPath, packageJsonCopyPath);
|
||||
await throwIfSignal(token, async () => {
|
||||
await fs.unlink(packageJsonCopyPath);
|
||||
});
|
||||
if (typeof packageJson.dependencies !== 'object') {
|
||||
packageJson.dependencies = {};
|
||||
}
|
||||
for (const extraModule of extraModules) {
|
||||
if (!packageJson.dependencies[extraModule]) {
|
||||
packageJson.dependencies[extraModule] = '*';
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
await throwIfSignal(token);
|
||||
return await run(token);
|
||||
} finally {
|
||||
await fs.move(packageJsonCopyPath, packageJsonPath, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
// package.json does not exist: We create one then remove it.
|
||||
const packageJson = {
|
||||
name: 'theia-rebuild-workaround',
|
||||
version: '0.0.0',
|
||||
dependencies: {} as Record<string, string>,
|
||||
};
|
||||
for (const extraModule of extraModules) {
|
||||
packageJson.dependencies[extraModule] = '*';
|
||||
}
|
||||
try {
|
||||
await fs.writeJson(packageJsonPath, packageJson);
|
||||
await throwIfSignal(token);
|
||||
return await run(token);
|
||||
} finally {
|
||||
await fs.unlink(packageJsonPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily install hooks to **try** to prevent the process from exiting while `run` is running.
|
||||
*
|
||||
* Note that it is still possible to kill the process and prevent cleanup logic (e.g. SIGKILL, computer forced shutdown, etc).
|
||||
*/
|
||||
async function guardExit<T>(run: (token: ExitToken) => Promise<T>): Promise<T> {
|
||||
const token = new ExitTokenImpl();
|
||||
const signalListener = (signal: NodeJS.Signals) => token._emitSignal(signal);
|
||||
for (const signal of EXIT_SIGNALS) {
|
||||
process.on(signal, signalListener);
|
||||
}
|
||||
try {
|
||||
return await run(token);
|
||||
} finally {
|
||||
for (const signal of EXIT_SIGNALS) {
|
||||
// FIXME we have a type clash here between Node, Electron and Mocha.
|
||||
// Typescript is resolving here to Electron's Process interface which extends the NodeJS.EventEmitter interface
|
||||
// However instead of the actual NodeJS.EventEmitter interface it resolves to an empty stub of Mocha
|
||||
// Therefore it can't find the correct "off" signature and throws an error
|
||||
// By casting to the NodeJS.EventEmitter ourselves, we short circuit the resolving and it succeeds
|
||||
(process as NodeJS.EventEmitter).off(signal, signalListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExitTokenImpl implements ExitToken {
|
||||
|
||||
protected _listeners = new Set<(signal: NodeJS.Signals) => void>();
|
||||
protected _lastSignal?: NodeJS.Signals;
|
||||
|
||||
onSignal(callback: (signal: NodeJS.Signals) => void): void {
|
||||
this._listeners.add(callback);
|
||||
}
|
||||
|
||||
getLastSignal(): NodeJS.Signals | undefined {
|
||||
return this._lastSignal;
|
||||
}
|
||||
|
||||
_emitSignal(signal: NodeJS.Signals): void {
|
||||
this._lastSignal = signal;
|
||||
for (const listener of this._listeners) {
|
||||
listener(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw `signal` if one was received, runs `cleanup` before doing so.
|
||||
*/
|
||||
async function throwIfSignal(token: ExitToken, cleanup?: () => Promise<void>): Promise<void> {
|
||||
if (token.getLastSignal()) {
|
||||
try {
|
||||
await cleanup?.();
|
||||
} finally {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw token.getLastSignal()!;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
dev-packages/application-manager/tsconfig.json
Normal file
22
dev-packages/application-manager/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../application-package"
|
||||
},
|
||||
{
|
||||
"path": "../ffmpeg"
|
||||
},
|
||||
{
|
||||
"path": "../native-webpack-plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user