Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Plugin Class and Introduce New Plugins Command #7839

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions scopes/harmony/aspect-loader/aspect-loader.cmd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AspectLoaderMain } from '@teambit/aspect-loader';
import { Command, CommandOptions } from '@teambit/cli';
import { CLITable } from '@teambit/cli-table';
GiladShoham marked this conversation as resolved.
Show resolved Hide resolved

export class PluginsCmd implements Command {
name = 'plugins';
alias = 'plugin';
description = 'Manage and retrieve information about plugins';
shortDescription = 'Manage and retrieve information about plugins';
group = 'development';

options = [['p', 'patterns', 'Retrieve patterns used by plugins']] as CommandOptions;

constructor(private aspectLoader: AspectLoaderMain) {}

async report(args: any, { patterns }: { patterns?: boolean }): Promise<string> {
if (patterns) {
return this.getPatternsTable();
}
return 'Usage:\n bit plugins --patterns\n';
}

private getPatternsTable(): string {
const patterns = this.aspectLoader.getPluginDefsPatterns();
const header = [{ value: 'patterns' }];
const tableData = patterns.map((pattern) => ({ patterns: pattern }));
const table = CLITable.fromObject(header, tableData as unknown as Record<string, string>[]);
return table.render();
}
}
32 changes: 20 additions & 12 deletions scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Graph, Node, Edge } from '@teambit/graph.cleargraph';
import { BitId } from '@teambit/legacy-bit-id';
import LegacyScope from '@teambit/legacy/dist/scope/scope';
import { GLOBAL_SCOPE, DEFAULT_DIST_DIRNAME } from '@teambit/legacy/dist/constants';
import { MainRuntime } from '@teambit/cli';
import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli';
import { ExtensionManifest, Harmony, Aspect, SlotRegistry, Slot } from '@teambit/harmony';
import { BitError } from '@teambit/bit-error';
import type { LoggerMain } from '@teambit/logger';
Expand All @@ -24,6 +24,7 @@ import { UNABLE_TO_LOAD_EXTENSION, UNABLE_TO_LOAD_EXTENSION_FROM_LIST } from './
import { CannotLoadExtension } from './exceptions';
import { getAspectDef } from './core-aspects';
import { Plugins } from './plugins';
import { PluginsCmd } from './aspect-loader.cmd';

export type PluginDefinitionSlot = SlotRegistry<PluginDefinition[]>;

Expand Down Expand Up @@ -120,7 +121,8 @@ export class AspectLoaderMain {
private harmony: Harmony,
private onAspectLoadErrorSlot: OnAspectLoadErrorSlot,
private onLoadRequireableExtensionSlot: OnLoadRequireableExtensionSlot,
private pluginSlot: PluginDefinitionSlot
private pluginSlot: PluginDefinitionSlot,
private cli: CLIMain
) {}

private getCompiler(component: Component) {
Expand Down Expand Up @@ -481,6 +483,13 @@ export class AspectLoaderMain {
return updatedManifest;
}

getPluginDefsPatterns() {
const patternsSet = new Set();
this.pluginSlot.values().flatMap((val) => val.map((def) => patternsSet.add(def.pattern)));
const uniquePatterns = [...patternsSet];
return uniquePatterns;
}

getPluginDefs() {
return flatten(this.pluginSlot.values());
}
Expand All @@ -502,12 +511,12 @@ export class AspectLoaderMain {

getPluginFiles(component: Component, componentPath: string): string[] {
const defs = this.getPluginDefs();
return Plugins.files(component, defs, this.pluginFileResolver.call(this, component, componentPath));
return Plugins.files(component, defs, this.logger, this.pluginFileResolver.call(this, component, componentPath));
}

hasPluginFiles(component: Component): boolean {
const defs = this.getPluginDefs();
const files = Plugins.files(component, defs);
const files = Plugins.files(component, defs, this.logger);
return !isEmpty(files);
}

Expand Down Expand Up @@ -791,7 +800,6 @@ export class AspectLoaderMain {

public async resolveLocalAspects(ids: string[], runtime?: string): Promise<AspectDefinition[]> {
const dirs = this.parseLocalAspect(ids);

return dirs.map((dir) => {
const srcRuntimeManifest = runtime ? this.findRuntime(dir, runtime) : undefined;
const srcAspectFilePath = runtime ? this.findAspectFile(dir) : undefined;
Expand All @@ -807,37 +815,37 @@ export class AspectLoaderMain {
const files = readdirSync(join(dirPath, 'dist'));
return files.find((path) => path.includes(`.aspect.js`));
}

static runtime = MainRuntime;
static dependencies = [LoggerAspect, EnvsAspect];
static dependencies = [LoggerAspect, EnvsAspect, CLIAspect];
static slots = [
Slot.withType<OnAspectLoadError>(),
Slot.withType<OnLoadRequireableExtension>(),
Slot.withType<PluginDefinition[]>(),
];

static async provider(
[loggerExt, envs]: [LoggerMain, EnvsMain],
[loggerExt, envs, cli]: [LoggerMain, EnvsMain, CLIMain],
config,
[onAspectLoadErrorSlot, onLoadRequireableExtensionSlot, pluginSlot]: [
OnAspectLoadErrorSlot,
OnLoadRequireableExtensionSlot,
PluginDefinitionSlot
],
harmony: Harmony
) {
): Promise<AspectLoaderMain> {
const logger = loggerExt.createLogger(AspectLoaderAspect.id);
const aspectLoader = new AspectLoaderMain(
logger,
envs,
harmony,
onAspectLoadErrorSlot,
onLoadRequireableExtensionSlot,
pluginSlot
pluginSlot,
cli
);

const pluginsCmd = new PluginsCmd(aspectLoader);
cli.register(pluginsCmd);
aspectLoader.registerPlugins([envs.getEnvPlugin()]);

return aspectLoader;
}
}
Expand Down
136 changes: 76 additions & 60 deletions scopes/harmony/aspect-loader/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import path from 'path';
import { Component } from '@teambit/component';
import { Logger } from '@teambit/logger';
import { Aspect } from '@teambit/harmony';
import { Logger } from '@teambit/logger';
import chalk from 'chalk';
import { PluginDefinition } from './plugin-definition';
import { Plugin } from './plugin';
import { OnAspectLoadErrorHandler } from './aspect-loader.main.runtime';

export type PluginMap = { [filePath: string]: PluginDefinition };

export class Plugins {
constructor(
readonly component: Component,
Expand All @@ -15,21 +15,17 @@ export class Plugins {
private logger: Logger
) {}

// computeDependencies(runtime: string): Aspect[] {
// const inRuntime = this.getByRuntime(runtime);
// return inRuntime.flatMap((plugin) => {
// return plugin.def.dependencies;
// });
// }
private static pluginCache: Map<string, Plugin[]> = new Map();
private static nonPluginComponentsCache: Set<string> = new Set();

getByRuntime(runtime: string) {
return this.plugins.filter((plugin) => {
return plugin.supportsRuntime(runtime);
return plugin?.supportsRuntime(runtime);
});
}

async load(runtime: string) {
const plugins = this.getByRuntime(runtime);
const plugins = this?.getByRuntime(runtime) || [];
const aspect = Aspect.create({
id: this.component.id.toString(),
});
Expand All @@ -41,50 +37,36 @@ export class Plugins {
return this.registerPluginWithTryCatch(plugin, aspect);
})
);
// Return an empty object so haromny will have something in the extension instance
// otherwise it will throw an error when trying to access the extension instance (harmony.get)
return {};
},
runtime,
// dependencies: this.computeDependencies(runtime)
dependencies: [],
});

return aspect;
}

async registerPluginWithTryCatch(plugin: Plugin, aspect: Aspect) {
let isPluginLoadedSuccessfully = false;

try {
return plugin.register(aspect);
plugin.register(aspect);
isPluginLoadedSuccessfully = true;
} catch (firstErr: any) {
this.logger.warn(
`failed loading plugin with pattern "${
plugin.def.pattern
}", in component ${this.component.id.toString()}, will try to fix and reload`,
firstErr
);
const isFixed = await this.triggerOnAspectLoadError(firstErr, this.component);
let errAfterReLoad;
if (isFixed) {
this.logger.info(
`the loading issue might be fixed now, re-loading plugin with pattern "${
plugin.def.pattern
}", in component ${this.component.id.toString()}`
);
try {
return plugin.register(aspect);
plugin.register(aspect);
isPluginLoadedSuccessfully = true;
} catch (err: any) {
this.logger.warn(
`re-load of the plugin with pattern "${
plugin.def.pattern
}", in component ${this.component.id.toString()} failed as well`,
err
);
errAfterReLoad = err;
this.logger.warn(`Error: ${err} while loading plugin file`);
}
}
const error = errAfterReLoad || firstErr;
throw error;
}

if (!isPluginLoadedSuccessfully) {
this.logger.error('Plugin loading failed after all attempts.');
throw new Error('Plugin loading failed after all attempts.');
}
}

Expand All @@ -98,35 +80,69 @@ export class Plugins {
triggerOnAspectLoadError: OnAspectLoadErrorHandler,
logger: Logger,
resolvePath?: (path: string) => string
) {
): Plugins {
const componentId = component.id.toString();

if (this.nonPluginComponentsCache.has(componentId)) {
return new Plugins(component, [], triggerOnAspectLoadError, logger);
}

const plugins = defs.flatMap((pluginDef) => {
const files =
typeof pluginDef.pattern === 'string'
? component.filesystem.byGlob([pluginDef.pattern])
: component.filesystem.byRegex(pluginDef.pattern);

return files.map((file) => {
return new Plugin(pluginDef, resolvePath ? resolvePath(file.relative) : file.path);
});
const cachedPlugins = Plugins.pluginCache.get(pluginDef.pattern.toString());
if (cachedPlugins) {
return cachedPlugins;
}

const files = Plugins.getFileMatches(component, pluginDef);
if (files.length > 0) {
const loadedPlugins = files.map((file) => {
let resolvedPath = file.path;
if (resolvePath) {
resolvedPath = resolvePath(file.relative);
}
if (component.filesystem.files.some((f) => f.relative === '.bit-capsule-ready')) {
resolvedPath = path.join(resolvedPath, 'dist');
}
return new Plugin(pluginDef, resolvedPath);
});
Plugins.pluginCache.set(pluginDef.pattern.toString(), loadedPlugins);
return loadedPlugins;
}

return [];
});

if (!plugins.length) {
this.nonPluginComponentsCache.add(componentId);
const warningMessage = this.constructNoPluginFileWarningMessage(component);
logger.consoleWarning(warningMessage);
}

return new Plugins(component, plugins, triggerOnAspectLoadError, logger);
}

/**
* Get the plugin files from the component.
*/
static files(component: Component, defs: PluginDefinition[], resolvePath?: (path: string) => string): string[] {
const files = defs.flatMap((pluginDef) => {
const matches =
typeof pluginDef.pattern === 'string'
? component.filesystem.byGlob([pluginDef.pattern])
: component.filesystem.byRegex(pluginDef.pattern);

return matches.map((file) => {
return resolvePath ? resolvePath(file.relative) : file.path;
});
static files(
component: Component,
defs: PluginDefinition[],
logger: Logger,
resolvePath?: (path: string) => string
): string[] {
return defs.flatMap((pluginDef) => {
const matches = this.getFileMatches(component, pluginDef);
return matches.map((file) => (resolvePath ? resolvePath(file.relative) : file.path));
});
return files;
}

private static getFileMatches(component: Component, pluginDef: PluginDefinition): any[] {
return typeof pluginDef.pattern === 'string'
? component.filesystem.byGlob([pluginDef.pattern])
: component.filesystem.byRegex(pluginDef.pattern);
}

private static constructNoPluginFileWarningMessage(component: Component): string {
return `plugin file from env with id: ${chalk.blue(component.id.toString())} could not be loaded.
Ensure the env has a plugin file with the correct file pattern.
Example: ${chalk.cyan('*.bit-env.*')}
Run: ${chalk.cyan('bit plugins --patterns')} to see all available plugin patterns.`;
}
}