From 0c70808c3dd1b561068b6865a48caa51a61384c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Tue, 6 Apr 2021 17:25:30 -0400 Subject: [PATCH] cli: filter Theia extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our generators currently collect any Theia extension installed in node_modules and mount them into your application without leaving you much of a choice. An alternative could be to create your own generators, but this is a lot of maintenance work. This commit simplifies the process of controlling what ends up in your applications by adding new loading strategies: - all: Like before, use everything found in node_modules - explicitDependenciesOnly: only use what is defined in your dependencies - includeList: use a regex list to pick what to include - excludeList: use a regex list to pick what to exclude This is configurable from an application package.json through the package.theia.extensions.loading field. Note that preventing an extension from having its inversify modules won't prevent it from being included in your bundles. Bundling should also mostly work no matter what you exclude, but if another extension was relying on a given Symbol it will most likely break at runtime. In such a case it is your responsability to bind the missing symbols using a custom Theia extension, specific to your use-cases. Signed-off-by: Paul Maréchal --- .../src/generator/abstract-generator.ts | 14 ++--- .../src/generator/backend-generator.ts | 2 +- .../src/generator/frontend-generator.ts | 4 +- .../src/application-package.ts | 61 ++++++++++++++++++- .../src/application-props.ts | 48 +++++++++++++++ 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/dev-packages/application-manager/src/generator/abstract-generator.ts b/dev-packages/application-manager/src/generator/abstract-generator.ts index 326e59a3ee38a..5c4737758f8a8 100644 --- a/dev-packages/application-manager/src/generator/abstract-generator.ts +++ b/dev-packages/application-manager/src/generator/abstract-generator.ts @@ -54,13 +54,13 @@ export abstract class AbstractGenerator { if (modules.size === 0) { return ''; } - const lines = Array.from(modules.keys()).map(moduleName => { - const invocation = `${fn}('${modules.get(moduleName)}')`; - if (fn === 'require') { - return `Promise.resolve(${invocation})`; - } - return invocation; - }).map(statement => ` .then(function () { return ${statement}.then(load) })`); + const lines = Array.from(modules.values(), modulePath => { + const invocation = `${fn}('${modulePath}')`; + const promiseStatement = fn === 'require' + ? `Promise.resolve(${invocation})` + : invocation; // the `import` function already returns a Promise + return ` .then(function () { return ${promiseStatement}.then(load); })`; + }); return os.EOL + lines.join(os.EOL); } diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts index 62304ebaa5637..2d901527b9e3e 100644 --- a/dev-packages/application-manager/src/generator/backend-generator.ts +++ b/dev-packages/application-manager/src/generator/backend-generator.ts @@ -19,7 +19,7 @@ import { AbstractGenerator } from './abstract-generator'; export class BackendGenerator extends AbstractGenerator { async generate(): Promise { - const backendModules = this.pck.targetBackendModules; + const backendModules = this.pck.getTargetBackendModulesFiltered(); await this.write(this.pck.backend('server.js'), this.compileServer(backendModules)); await this.write(this.pck.backend('main.js'), this.compileMain(backendModules)); } diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index d734168f9fc7f..70d74e9cf37b9 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -22,11 +22,11 @@ import { existsSync, readFileSync } from 'fs'; export class FrontendGenerator extends AbstractGenerator { async generate(): Promise { - const frontendModules = this.pck.targetFrontendModules; + const frontendModules = this.pck.getTargetFrontendModulesFiltered(); await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules)); await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules)); if (this.pck.isElectron()) { - const electronMainModules = this.pck.targetElectronMainModules; + const electronMainModules = this.pck.getTargetElectronMainModulesFiltered(); await this.write(this.pck.frontend('electron-main.js'), this.compileElectronMain(electronMainModules)); } } diff --git a/dev-packages/application-package/src/application-package.ts b/dev-packages/application-package/src/application-package.ts index 79c81a30c9f24..b3e6a9e97d3e3 100644 --- a/dev-packages/application-package/src/application-package.ts +++ b/dev-packages/application-package/src/application-package.ts @@ -240,15 +240,70 @@ export class ApplicationPackage { } get targetBackendModules(): Map { - return this.ifBrowser(this.backendModules, this.backendElectronModules); + return this.isBrowser() ? this.backendModules : this.backendElectronModules; } get targetFrontendModules(): Map { - return this.ifBrowser(this.frontendModules, this.frontendElectronModules); + return this.isBrowser() ? this.frontendModules : this.frontendElectronModules; } get targetElectronMainModules(): Map { - return this.ifElectron(this.electronMainModules, new Map()); + return this.isElectron() ? this.electronMainModules : new Map(); + } + + getTargetBackendModulesFiltered(): Map { + return this.filterModules(this.targetBackendModules); + } + + getTargetFrontendModulesFiltered(): Map { + return this.filterModules(this.targetFrontendModules); + } + + getTargetElectronMainModulesFiltered(): Map { + return this.filterModules(this.electronMainModules); + } + + protected filterModules(modules: Map): Map { + const { extensions } = this.props; + if (extensions.loading.strategy === 'all') { + return modules; + } + const filtered = new Map(); + if (extensions.loading.strategy === 'explicitDependenciesOnly') { + for (const [name, path] of modules.entries()) { + if (this.getPackageName(path) in (this.pck.dependencies ?? {})) { + filtered.set(name, path); + } + }; + } else if (extensions.loading.strategy === 'includeList') { + for (const [name, path] of modules.entries()) { + if (extensions.loading.includes.some(include => new RegExp(include).test(path))) { + filtered.set(name, path); + } + } + } else if (extensions.loading.strategy === 'excludeList') { + for (const [name, path] of modules.entries()) { + if (!extensions.loading.excludes.some(exclude => new RegExp(exclude).test(path))) { + filtered.set(name, path); + } + } + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + throw new Error(`unknown theia.extensions.bundling option: ${(extensions as any).bundling}`); + } + return filtered; + } + + /** + * Only keep the first two parts of the package name e.g., + * - `@a/b/c/...` => `@a/b` + * - `a/b/c/...` => `a` + */ + protected getPackageName(mod: string): string { + const slice = mod.startsWith('@') ? 2 : 1; + return mod.split('/', slice + 1) + .slice(0, slice) + .join('/'); } setDependency(name: string, version: string | undefined): boolean { diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts index 1c78feacccb0b..fcb9dfada1263 100644 --- a/dev-packages/application-package/src/application-props.ts +++ b/dev-packages/application-package/src/application-props.ts @@ -49,6 +49,11 @@ export interface ApplicationProps extends NpmRegistryProps { */ readonly target: ApplicationProps.Target; + /** + * Theia extensions related properties. + */ + readonly extensions: Readonly; + /** * Frontend related properties. */ @@ -75,6 +80,11 @@ export namespace ApplicationProps { export const DEFAULT: ApplicationProps = { ...NpmRegistryProps.DEFAULT, target: 'browser', + extensions: { + loading: { + strategy: 'all' + } + }, backend: { config: {} }, @@ -94,6 +104,44 @@ export namespace ApplicationProps { } +/** + * Configure how Theia handles extensions. + */ +export interface ExtensionsConfig { + + /** + * Specificy which inversify modules to mount in your application. + * + * Note that the `loadingStrategy` only affects which inversify module will be mounted + * to your application, but an ignored extension might still end up in your bundles. + */ + loading: ExtensionLoadingStrategy; +} + +export type ExtensionLoadingStrategy = { + /** + * Use all Theia extensions found installed under `node_modules`. + */ + strategy: 'all'; +} | { + /** + * Only use Theia extensions explicitly defined as dependencies in your application. + */ + strategy: 'explicitDependenciesOnly'; +} | { + /** + * Use a list of regexes to filter in Theia extensions, will be matched against the modules file path. + */ + strategy: 'includeList'; + includes: string[] +} | { + /** + * Use a list of regexes to filter out Theia extensions, will be matched against the modules file path. + */ + strategy: 'excludeList'; + excludes: string[] +}; + /** * Base configuration for the Theia application. */