Skip to content

Commit

Permalink
cli: filter Theia extensions
Browse files Browse the repository at this point in the history
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 <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Apr 6, 2021
1 parent fd91f21 commit 0c70808
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { AbstractGenerator } from './abstract-generator';
export class BackendGenerator extends AbstractGenerator {

async generate(): Promise<void> {
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import { existsSync, readFileSync } from 'fs';
export class FrontendGenerator extends AbstractGenerator {

async generate(): Promise<void> {
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));
}
}
Expand Down
61 changes: 58 additions & 3 deletions dev-packages/application-package/src/application-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,70 @@ export class ApplicationPackage {
}

get targetBackendModules(): Map<string, string> {
return this.ifBrowser(this.backendModules, this.backendElectronModules);
return this.isBrowser() ? this.backendModules : this.backendElectronModules;
}

get targetFrontendModules(): Map<string, string> {
return this.ifBrowser(this.frontendModules, this.frontendElectronModules);
return this.isBrowser() ? this.frontendModules : this.frontendElectronModules;
}

get targetElectronMainModules(): Map<string, string> {
return this.ifElectron(this.electronMainModules, new Map());
return this.isElectron() ? this.electronMainModules : new Map();
}

getTargetBackendModulesFiltered(): Map<string, string> {
return this.filterModules(this.targetBackendModules);
}

getTargetFrontendModulesFiltered(): Map<string, string> {
return this.filterModules(this.targetFrontendModules);
}

getTargetElectronMainModulesFiltered(): Map<string, string> {
return this.filterModules(this.electronMainModules);
}

protected filterModules(modules: Map<string, string>): Map<string, string> {
const { extensions } = this.props;
if (extensions.loading.strategy === 'all') {
return modules;
}
const filtered = new Map<string, string>();
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 {
Expand Down
48 changes: 48 additions & 0 deletions dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface ApplicationProps extends NpmRegistryProps {
*/
readonly target: ApplicationProps.Target;

/**
* Theia extensions related properties.
*/
readonly extensions: Readonly<ExtensionsConfig>;

/**
* Frontend related properties.
*/
Expand All @@ -75,6 +80,11 @@ export namespace ApplicationProps {
export const DEFAULT: ApplicationProps = {
...NpmRegistryProps.DEFAULT,
target: 'browser',
extensions: {
loading: {
strategy: 'all'
}
},
backend: {
config: {}
},
Expand All @@ -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.
*/
Expand Down

0 comments on commit 0c70808

Please sign in to comment.